mirror of
https://github.com/savoirfairelinux/opendht.git
synced 2026-11-11 05:47:17 +00:00
Compare commits
13
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
dac5f0fc05 | ||
|
|
c58c14d49a | ||
|
|
1b5005603a | ||
|
|
289ea964a2 | ||
|
|
9a796e507e | ||
|
|
6700049f3a | ||
|
|
8bdfe00804 | ||
|
|
34156afefd | ||
|
|
0f707a1667 | ||
|
|
38ef4c01cf | ||
|
|
068ce1451c | ||
|
|
96456a86ae | ||
|
|
e42fb54e3a |
No files matched your search
@@ -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-cxx-dev libargon2-dev"
|
||||
DEBIAN13_DEPS: "git libtool pkg-config wget cmake libncurses5-dev libreadline-dev nettle-dev libasio-dev libgnutls28-dev libuv1-dev python3-pip python3-dev python3-setuptools libcppunit-dev libjsoncpp-dev libfmt-dev libmsgpack-dev libargon2-dev"
|
||||
|
||||
on: [push, pull_request]
|
||||
|
||||
|
||||
@@ -36,6 +36,7 @@ jobs:
|
||||
name: Build wheels on Windows
|
||||
runs-on: windows-latest
|
||||
env:
|
||||
VCPKG_VERSION: "2026.06.01"
|
||||
VCPKG_INSTALLATION_ROOT: "C:\\vcpkg"
|
||||
VCPKG_DISABLE_METRICS: "1"
|
||||
VCPKG_DEFAULT_TRIPLET: x64-windows-release
|
||||
@@ -52,10 +53,7 @@ jobs:
|
||||
path: |
|
||||
${{ env.VCPKG_INSTALLED_DIR }}
|
||||
${{ env.VCPKG_DEFAULT_BINARY_CACHE }}
|
||||
key: ${{ runner.os }}-vcpkg-${{ env.VCPKG_DEFAULT_TRIPLET }}-${{ hashFiles('**/vcpkg.json', '**/vcpkg-configuration.json') }}-${{ hashFiles('.github/workflows/python-wheels.yml') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-vcpkg-${{ env.VCPKG_DEFAULT_TRIPLET }}-${{ hashFiles('**/vcpkg.json', '**/vcpkg-configuration.json') }}
|
||||
${{ runner.os }}-vcpkg-${{ env.VCPKG_DEFAULT_TRIPLET }}-
|
||||
key: ${{ runner.os }}-vcpkg-${{ env.VCPKG_VERSION }}-${{ env.VCPKG_DEFAULT_TRIPLET }}-${{ hashFiles('**/vcpkg.json', '**/vcpkg-configuration.json') }}-${{ hashFiles('.github/workflows/windows-build.yml') }}
|
||||
|
||||
- name: Setup vcpkg and install dependencies
|
||||
if: steps.cache-vcpkg.outputs.cache-hit != 'true'
|
||||
|
||||
@@ -309,6 +309,7 @@ jobs:
|
||||
name: Build Windows MSI Installer
|
||||
runs-on: windows-latest
|
||||
env:
|
||||
VCPKG_VERSION: "2026.06.01"
|
||||
VCPKG_INSTALLATION_ROOT: "C:\\vcpkg"
|
||||
VCPKG_DISABLE_METRICS: "1"
|
||||
VCPKG_DEFAULT_TRIPLET: x64-windows
|
||||
@@ -338,10 +339,7 @@ jobs:
|
||||
path: |
|
||||
${{ env.VCPKG_INSTALLED_DIR }}
|
||||
${{ env.VCPKG_DEFAULT_BINARY_CACHE }}
|
||||
key: ${{ runner.os }}-vcpkg-x64-shared-${{ hashFiles('**/vcpkg.json', '**/vcpkg-configuration.json') }}-${{ hashFiles('.github/workflows/release-package.yml') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-vcpkg-x64-shared-${{ hashFiles('**/vcpkg.json', '**/vcpkg-configuration.json') }}
|
||||
${{ runner.os }}-vcpkg-x64-shared-
|
||||
key: ${{ runner.os }}-vcpkg-${{ env.VCPKG_VERSION }}-x64-shared-${{ hashFiles('**/vcpkg.json', '**/vcpkg-configuration.json') }}-${{ hashFiles('.github/workflows/windows-build.yml') }}
|
||||
|
||||
- name: Setup vcpkg and install dependencies
|
||||
if: steps.cache-vcpkg.outputs.cache-hit != 'true'
|
||||
@@ -415,6 +413,7 @@ jobs:
|
||||
name: Build Windows dhtnode
|
||||
runs-on: windows-latest
|
||||
env:
|
||||
VCPKG_VERSION: "2026.06.01"
|
||||
VCPKG_INSTALLATION_ROOT: "C:\\vcpkg"
|
||||
VCPKG_DISABLE_METRICS: "1"
|
||||
VCPKG_DEFAULT_TRIPLET: x64-windows-static-release
|
||||
@@ -438,10 +437,10 @@ jobs:
|
||||
path: |
|
||||
${{ env.VCPKG_INSTALLED_DIR }}
|
||||
${{ env.VCPKG_DEFAULT_BINARY_CACHE }}
|
||||
key: ${{ runner.os }}-vcpkg-x64-static-${{ hashFiles('**/vcpkg.json', '**/vcpkg-configuration.json') }}-${{ hashFiles('.github/workflows/release-package.yml') }}
|
||||
key: ${{ runner.os }}-vcpkg-${{ env.VCPKG_VERSION }}-x64-static-${{ hashFiles('**/vcpkg.json', '**/vcpkg-configuration.json') }}-${{ hashFiles('.github/workflows/windows-build.yml') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-vcpkg-x64-static-${{ hashFiles('**/vcpkg.json', '**/vcpkg-configuration.json') }}
|
||||
${{ runner.os }}-vcpkg-x64-static-
|
||||
${{ runner.os }}-vcpkg-${{ env.VCPKG_VERSION }}-x64-static-${{ hashFiles('**/vcpkg.json', '**/vcpkg-configuration.json') }}
|
||||
${{ runner.os }}-vcpkg-${{ env.VCPKG_VERSION }}-x64-static-
|
||||
|
||||
- name: Setup vcpkg and install static dependencies
|
||||
if: steps.cache-vcpkg-static.outputs.cache-hit != 'true'
|
||||
|
||||
@@ -9,6 +9,7 @@ on:
|
||||
branches: [ main, master ]
|
||||
|
||||
env:
|
||||
VCPKG_VERSION: "2026.06.01"
|
||||
VCPKG_INSTALLATION_ROOT: "C:\\vcpkg"
|
||||
VCPKG_DISABLE_METRICS: "1"
|
||||
VCPKG_INSTALLED_DIR: ${{ github.workspace }}\vcpkg_installed
|
||||
@@ -35,7 +36,7 @@ jobs:
|
||||
id: generate_cache_key
|
||||
shell: pwsh
|
||||
run: |
|
||||
$key_value = "${{ runner.os }}-vcpkg-${{ matrix.arch }}-shared-${{ hashFiles('**/vcpkg.json', '**/vcpkg-configuration.json') }}-${{ hashFiles('.github/workflows/windows-build.yml') }}"
|
||||
$key_value = "${{ runner.os }}-vcpkg-${{ env.VCPKG_VERSION }}-${{ matrix.arch }}-shared-${{ hashFiles('**/vcpkg.json', '**/vcpkg-configuration.json') }}-${{ hashFiles('.github/workflows/windows-build.yml') }}"
|
||||
echo "key=$key_value" >> $env:GITHUB_OUTPUT
|
||||
|
||||
- name: Cache vcpkg installed packages
|
||||
@@ -47,8 +48,8 @@ jobs:
|
||||
${{ env.VCPKG_DEFAULT_BINARY_CACHE }}
|
||||
key: ${{ steps.generate_cache_key.outputs.key }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-vcpkg-${{ matrix.arch }}-shared-${{ hashFiles('**/vcpkg.json', '**/vcpkg-configuration.json') }}
|
||||
${{ runner.os }}-vcpkg-${{ matrix.arch }}-shared-
|
||||
${{ runner.os }}-vcpkg-${{ env.VCPKG_VERSION }}-${{ matrix.arch }}-shared-${{ hashFiles('**/vcpkg.json', '**/vcpkg-configuration.json') }}
|
||||
${{ runner.os }}-vcpkg-${{ env.VCPKG_VERSION }}-${{ matrix.arch }}-shared-
|
||||
|
||||
- name: Setup vcpkg and install dependencies
|
||||
if: steps.cache-vcpkg.outputs.cache-hit != 'true'
|
||||
@@ -83,7 +84,7 @@ jobs:
|
||||
id: generate_static_cache_key
|
||||
shell: pwsh
|
||||
run: |
|
||||
$key_value = "${{ runner.os }}-vcpkg-${{ matrix.arch }}-static-${{ hashFiles('**/vcpkg.json', '**/vcpkg-configuration.json') }}-${{ hashFiles('.github/workflows/windows-build.yml') }}"
|
||||
$key_value = "${{ runner.os }}-vcpkg-${{ env.VCPKG_VERSION }}-${{ matrix.arch }}-static-${{ hashFiles('**/vcpkg.json', '**/vcpkg-configuration.json') }}-${{ hashFiles('.github/workflows/windows-build.yml') }}"
|
||||
echo "key=$key_value" >> $env:GITHUB_OUTPUT
|
||||
|
||||
- name: Cache vcpkg static installed packages
|
||||
@@ -95,8 +96,8 @@ jobs:
|
||||
${{ env.VCPKG_DEFAULT_BINARY_CACHE }}
|
||||
key: ${{ steps.generate_static_cache_key.outputs.key }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-vcpkg-${{ matrix.arch }}-static-${{ hashFiles('**/vcpkg.json', '**/vcpkg-configuration.json') }}
|
||||
${{ runner.os }}-vcpkg-${{ matrix.arch }}-static-
|
||||
${{ runner.os }}-vcpkg-${{ env.VCPKG_VERSION }}-${{ matrix.arch }}-static-${{ hashFiles('**/vcpkg.json', '**/vcpkg-configuration.json') }}
|
||||
${{ runner.os }}-vcpkg-${{ env.VCPKG_VERSION }}-${{ matrix.arch }}-static-
|
||||
|
||||
- name: Setup vcpkg and install static dependencies
|
||||
if: steps.cache-vcpkg-static.outputs.cache-hit != 'true'
|
||||
@@ -138,8 +139,8 @@ jobs:
|
||||
${{ env.VCPKG_DEFAULT_BINARY_CACHE }}
|
||||
key: ${{ needs.prepare-dependencies.outputs.vcpkg_cache_key }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-vcpkg-${{ matrix.arch }}-shared-${{ hashFiles('**/vcpkg.json', '**/vcpkg-configuration.json') }}
|
||||
${{ runner.os }}-vcpkg-${{ matrix.arch }}-shared-
|
||||
${{ runner.os }}-vcpkg-${{ env.VCPKG_VERSION }}-${{ matrix.arch }}-shared-${{ hashFiles('**/vcpkg.json', '**/vcpkg-configuration.json') }}
|
||||
${{ runner.os }}-vcpkg-${{ env.VCPKG_VERSION }}-${{ matrix.arch }}-shared-
|
||||
|
||||
- name: Configure CMake
|
||||
shell: pwsh
|
||||
@@ -215,8 +216,8 @@ jobs:
|
||||
${{ env.VCPKG_DEFAULT_BINARY_CACHE }}
|
||||
key: ${{ needs.prepare-static-dependencies.outputs.vcpkg_static_cache_key }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-vcpkg-${{ matrix.arch }}-static-${{ hashFiles('**/vcpkg.json', '**/vcpkg-configuration.json') }}
|
||||
${{ runner.os }}-vcpkg-${{ matrix.arch }}-static-
|
||||
${{ runner.os }}-vcpkg-${{ env.VCPKG_VERSION }}-${{ matrix.arch }}-static-${{ hashFiles('**/vcpkg.json', '**/vcpkg-configuration.json') }}
|
||||
${{ runner.os }}-vcpkg-${{ env.VCPKG_VERSION }}-${{ matrix.arch }}-static-
|
||||
|
||||
- name: Configure CMake for Static Build
|
||||
shell: pwsh
|
||||
|
||||
+11
-8
@@ -9,8 +9,8 @@ if(POLICY CMP0167)
|
||||
cmake_policy(SET CMP0167 NEW)
|
||||
endif()
|
||||
|
||||
set (opendht_VERSION_MAJOR 5)
|
||||
set (opendht_VERSION_MINOR 0.0)
|
||||
set (opendht_VERSION_MAJOR 4)
|
||||
set (opendht_VERSION_MINOR 3.0)
|
||||
set (opendht_VERSION ${opendht_VERSION_MAJOR}.${opendht_VERSION_MINOR})
|
||||
set (PACKAGE_VERSION ${opendht_VERSION})
|
||||
set (VERSION "${opendht_VERSION}")
|
||||
@@ -502,7 +502,11 @@ list (APPEND opendht_HEADERS
|
||||
)
|
||||
|
||||
if (OPENDHT_PEER_DISCOVERY)
|
||||
list (APPEND opendht_SOURCES src/peer_discovery.cpp)
|
||||
list (APPEND opendht_SOURCES
|
||||
src/peer_discovery/mdns.cpp
|
||||
src/peer_discovery/dns_sd.cpp
|
||||
src/peer_discovery/mdns_transport.cpp
|
||||
src/peer_discovery/peer_discovery.cpp)
|
||||
list (APPEND opendht_HEADERS include/opendht/peer_discovery.h)
|
||||
add_definitions(-DOPENDHT_PEER_DISCOVERY)
|
||||
endif()
|
||||
@@ -589,6 +593,7 @@ if (MSVC)
|
||||
simdutf::simdutf
|
||||
wsock32
|
||||
ws2_32
|
||||
iphlpapi
|
||||
)
|
||||
if (OPENDHT_TOOLS)
|
||||
target_link_libraries(opendht PUBLIC unofficial::readline-win32::readline)
|
||||
@@ -672,7 +677,7 @@ else()
|
||||
# System-specific linker pipelines
|
||||
if (WIN32 AND MINGW) # MSYS2
|
||||
message(STATUS "Build pipeline: Windows/MSYS2-MinGW")
|
||||
target_link_libraries(opendht PUBLIC wsock32 ws2_32)
|
||||
target_link_libraries(opendht PUBLIC wsock32 ws2_32 iphlpapi)
|
||||
if(OPENDHT_PROXY_OPENSSL)
|
||||
target_link_libraries(opendht PUBLIC Crypt32)
|
||||
endif()
|
||||
@@ -870,10 +875,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
|
||||
@@ -892,6 +893,8 @@ if (BUILD_TESTING)
|
||||
endif()
|
||||
if (OPENDHT_PEER_DISCOVERY)
|
||||
list (APPEND test_FILES
|
||||
tests/test_mdns.h
|
||||
tests/test_mdns.cpp
|
||||
tests/test_peerdiscovery.h
|
||||
tests/test_peerdiscovery.cpp
|
||||
)
|
||||
|
||||
@@ -23,8 +23,6 @@ Values are distributed over the network, with redundancy.
|
||||
## Documentation
|
||||
See the wiki: <https://github.com/savoirfairelinux/opendht/wiki>
|
||||
|
||||
Push subscription recovery protocol: [doc/proxy-sync.md](doc/proxy-sync.md).
|
||||
|
||||
#### How to run a node
|
||||
|
||||
You can help contribute to the public network by running a stable node with a public IP address.
|
||||
|
||||
@@ -1,178 +0,0 @@
|
||||
# Push Subscription Synchronization
|
||||
|
||||
Revision-aware clients require an updated proxy. Old clients remain supported:
|
||||
requests without `sync` retain the original subscription response format.
|
||||
|
||||
## Validation and Snapshots
|
||||
|
||||
A `SUBSCRIBE /key/<hash>` request contains the existing push registration fields
|
||||
and a `sync` object. An empty object requests a full snapshot. A synchronized
|
||||
client instead supplies its last reconciled token:
|
||||
|
||||
```json
|
||||
{"sync":{"epoch":"<listener-epoch>","revision":7}}
|
||||
```
|
||||
|
||||
The epoch is unique to the server-side listener. New and restored listeners get
|
||||
fresh epochs. Each addition, edit, or expiration reported by the DHT listener
|
||||
increments its revision. Epoch/revision state is not restored from disk.
|
||||
|
||||
When the token matches, the response contains only a completion record. Otherwise,
|
||||
the response contains newline-delimited value objects followed by this record:
|
||||
|
||||
```json
|
||||
{"sync":{"epoch":"<listener-epoch>","revision":8,"snapshot":true,"complete":true}}
|
||||
```
|
||||
|
||||
The listener retains its current values, including edits and expirations, under
|
||||
the same mutex as its revision. The server copies both atomically; it does not
|
||||
run a GET for a synchronization request. New listeners wait for initial readiness
|
||||
before responding. `snapshot:false` means validation found the client's cache
|
||||
unchanged. Successful responses always have `complete:true`; initial search
|
||||
failure returns HTTP 503. The deprecated `retry` field is not used. Every
|
||||
subscription request renews the lease, independently of whether values are sent.
|
||||
|
||||
Clients buffer snapshot values and reconcile only after a valid completion
|
||||
record and successful HTTP framing. Reconciliation delivers new values and
|
||||
higher-sequence edits, suppresses duplicates and older versions, and expires
|
||||
cached IDs absent from the snapshot. Failed or invalidated responses never
|
||||
remove cached values. Snapshot deletion is authoritative and does not depend
|
||||
on wall-clock timestamps.
|
||||
|
||||
## Push Ordering and Recovery
|
||||
|
||||
Value pushes include string fields `epoch` and `revision`. A push no newer than
|
||||
the reconciled revision is ignored. Revisions must be unsigned decimal integers
|
||||
without signs, whitespace, trailing characters, or overflow. A contiguous
|
||||
expiration revision removes the listed IDs locally without a download. The
|
||||
`expired:"true"` field identifies batched expirations using `ids`; the original
|
||||
single-value `exp` field remains supported. Other same-epoch changes and revision
|
||||
gaps require a cached snapshot. Unknown epochs first validate against the proxy,
|
||||
so delayed pushes from old epochs do not force a value download. Revisions cover
|
||||
edits as well as additions and deletions.
|
||||
|
||||
Clients allow one sync request per listener and coalesce overlapping triggers.
|
||||
Request generations reject callbacks from superseded requests. Completion state
|
||||
changes run under the listener's owning lock after checking the generation.
|
||||
Same-epoch push observations can invalidate an older snapshot. Confirmed retired
|
||||
epochs are remembered in a bounded, push-token-scoped set; other unknown epochs
|
||||
are validated without changing cached values. Returning to an earlier push token
|
||||
remains valid.
|
||||
Token rotation during a pending request is applied after that request completes.
|
||||
Clients require an updated proxy server. Value pushes without both epoch and
|
||||
revision are malformed and return `Error`; they do not mutate the listener cache
|
||||
or trigger synchronization requests. The proxy adds these fields independently
|
||||
of whether its upstream DHT peers support version-2 native LISTEN.
|
||||
Pushes for another client session return `IgnoredWrongSession` without changing
|
||||
subscriptions, cached values, or connectivity state. Their application-level
|
||||
purpose is to wake the mobile app; session creation and desired subscriptions
|
||||
are independent actions. A new session requests its own full initial snapshot
|
||||
and does not inherit synchronization state from the rejected push.
|
||||
|
||||
Connectivity changes and existing lease refreshes validate the last reconciled
|
||||
token; unchanged data is not downloaded again. Successful lease renewal alone
|
||||
does not clear pending recovery. Failed recovery retries use the existing lease
|
||||
timer with exponential delays from one to sixty seconds; successful recovery
|
||||
restores the normal lease schedule. Stale and malformed responses also use this
|
||||
backoff, rather than retrying immediately. Each subscription request has a
|
||||
30-second steady-clock deadline covering DNS, connection, and response body
|
||||
receipt. Expired requests release the in-flight slot and enter recovery. No
|
||||
healthy-state polling timer is added.
|
||||
|
||||
Without another event or polling, a silently dropped final push cannot be
|
||||
detected immediately. The next connectivity validation or existing lease refresh
|
||||
detects a revision mismatch. Guarantees concern reconciliation with the proxy's
|
||||
observed DHT state, not linearizable global DHT storage.
|
||||
|
||||
## Listener Readiness
|
||||
|
||||
The existing four-argument `listen` API remains available. The new overload adds
|
||||
a one-shot `DoneCallbackSimple` as its fifth argument:
|
||||
|
||||
```cpp
|
||||
runner.listen(key, valuesCallback, {}, {}, readyCallback);
|
||||
```
|
||||
|
||||
`readyCallback(true)` follows completed search/listener synchronization for each
|
||||
enabled address family. Version-2 native sources complete after initial delivery
|
||||
and contiguous updates through their observed high-water mark. Legacy sources
|
||||
complete when LISTEN registration is acknowledged; initial values can arrive
|
||||
after readiness. Isolated families with no peers are ready after delivering local
|
||||
values. Initial search or registration failure reports `false`; cancellation
|
||||
does not promise a callback.
|
||||
This is an initial completion notification, not a global consistency guarantee.
|
||||
It does not start a GET or add polling. Attaching to a synchronized cache delivers
|
||||
cached values before notifying readiness.
|
||||
Mixed-version and entirely legacy networks can initialize successfully without
|
||||
GET fallback or polling. A legacy acknowledgment confirms registration, not
|
||||
complete baseline delivery or absence of values. Late values remain ordinary
|
||||
listener updates: the proxy advances its revision and push clients reconcile
|
||||
through the existing subscription path. A client snapshot is authoritative for
|
||||
the proxy's observed cache, not proof of a complete upstream DHT baseline. A
|
||||
legacy update never received by the proxy cannot be detected by proxy revision
|
||||
validation. Proxy readiness callbacks run outside the client's search lock;
|
||||
cancellation suppresses queued notifications.
|
||||
|
||||
Streaming proxy clients request `/key/<hash>/listen?sync=1`. Updated proxies send
|
||||
`{"sync":{"complete":true}}` after initial values, or `complete:false` on initial
|
||||
failure. Requests without this opt-in retain the original stream format.
|
||||
|
||||
## Native Initial Delivery
|
||||
|
||||
Native clients request LISTEN version `ve:2` with an `ls` tuple:
|
||||
|
||||
```text
|
||||
[registration, epoch, sequence, snapshotEnd]
|
||||
```
|
||||
|
||||
The registration is a fresh nonzero client nonce; a LISTEN request sends zeros
|
||||
for the other fields. The server explicitly acknowledges version 2, assigns a
|
||||
nonzero stream epoch, and declares the number of initial UPDATE batches in
|
||||
`snapshotEnd`. An empty baseline declares zero batches. Each UPDATE carries the
|
||||
same registration, epoch, and snapshotEnd with a consecutive sequence starting
|
||||
at one. The initial batches occupy sequences 1 through snapshotEnd; live changes
|
||||
follow them. The acknowledgment's sequence is the server's current high-water
|
||||
mark. All tuple fields are unsigned 64-bit integers.
|
||||
|
||||
The server registers the listener and serializes the initial batches on its DHT
|
||||
thread. Existing UPDATE fragmentation and acknowledgments deliver each initial
|
||||
value once per source registration on a loss-free connection. There is no
|
||||
additional GET or second value download. Retries resend the same immutable
|
||||
request bytes. Repeated registrations with the same nonce reuse the baseline;
|
||||
healthy renewals return metadata only. Recreated sockets use a new nonce and
|
||||
cannot accept the previous registration's updates.
|
||||
|
||||
Live updates, including edits, expirations, and refreshes, are sent immediately
|
||||
while the baseline is in flight. The receiver buffers them until every declared
|
||||
initial batch has been fully reassembled and validated. It then applies the
|
||||
baseline followed by contiguous live changes in sequence order. Duplicates are
|
||||
acknowledged without being applied again. Known sequence gaps prevent the cache
|
||||
from being considered synchronized; retransmission can fill them. Initialization
|
||||
or an unresolved live gap fails after 30 seconds and allows a fresh registration.
|
||||
|
||||
Batches contain at most 32 values and target the existing 56 KiB serialized-value
|
||||
budget; individual larger values retain the existing fragment transport. The
|
||||
receiver bounds buffered batches to 4096 and payload bytes to 64 MiB, and rejects
|
||||
baselines exceeding its 4096-value cache capacity. Invalid version-2 values fail
|
||||
the batch instead of being silently skipped. This design adds neither pacing
|
||||
nor send windows. Per-peer baselines are coherent with their subsequent updates,
|
||||
but do not provide an atomic snapshot of the entire distributed DHT.
|
||||
|
||||
Peers that do not explicitly acknowledge version 2 remain legacy listeners.
|
||||
Their values are delivered normally and successful LISTEN acknowledgment
|
||||
completes their best-effort initialization. No GET fallback or polling is added.
|
||||
Legacy LISTEN acknowledgments may omit the token; clients retain the token
|
||||
previously obtained during discovery and keep the listener socket open. A
|
||||
version-2 acknowledgment must include its token and synchronization metadata.
|
||||
The source records whether it uses legacy completion or a verified version-2
|
||||
stream. An established version-2 stream cannot downgrade on a metadata-free
|
||||
acknowledgment; invalid initialization and registration failures still report
|
||||
failure.
|
||||
|
||||
## HTTP Transport Note
|
||||
|
||||
HTTP requests now report status code zero for transport errors and cancellation,
|
||||
even if a 200 status was received before the failure. Cancellation also sets
|
||||
`aborted`. EOF succeeds only when the HTTP parser confirms complete framing;
|
||||
truncated content-length and chunked responses fail. Callers must not treat an
|
||||
earlier status callback as proof that a response body completed successfully.
|
||||
@@ -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 {};
|
||||
@@ -278,8 +272,7 @@ public:
|
||||
* 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)
|
||||
{
|
||||
void sign(const Identity& id, duration validity_period = {}, uint64_t number = 0) {
|
||||
sign(*id.first, *id.second, validity_period, number);
|
||||
}
|
||||
|
||||
|
||||
+4
-21
@@ -236,7 +236,6 @@ public:
|
||||
* @return a token to cancel the listener later.
|
||||
*/
|
||||
size_t listen(const InfoHash&, ValueCallback, Value::Filter = {}, Where = {}) override;
|
||||
size_t listen(const InfoHash&, ValueCallback, Value::Filter, Where, DoneCallbackSimple ready) override;
|
||||
|
||||
size_t listen(const InfoHash& key, GetCallback cb, Value::Filter f = {}, Where w = {}) override
|
||||
{
|
||||
@@ -461,12 +460,7 @@ private:
|
||||
void reportedAddr(const SockAddr&);
|
||||
|
||||
// Storage
|
||||
std::optional<net::ListenSync> storageAddListener(const InfoHash& id,
|
||||
const Sp<Node>& node,
|
||||
size_t tid,
|
||||
Query&& = {},
|
||||
int version = 0,
|
||||
const std::optional<net::ListenSync>& sync = {});
|
||||
void storageAddListener(const InfoHash& id, const Sp<Node>& node, size_t tid, Query&& = {}, int version = 0);
|
||||
bool storageStore(const InfoHash& id,
|
||||
const Sp<Value>& value,
|
||||
time_point created,
|
||||
@@ -541,12 +535,7 @@ private:
|
||||
DoneCallback callback,
|
||||
time_point created = time_point::max(),
|
||||
bool permanent = false);
|
||||
size_t listenTo(const InfoHash& id,
|
||||
sa_family_t af,
|
||||
ValueCallback cb,
|
||||
Value::Filter f = {},
|
||||
const Sp<Query>& q = {},
|
||||
DoneCallbackSimple ready = {});
|
||||
size_t listenTo(const InfoHash& id, sa_family_t af, ValueCallback cb, Value::Filter f = {}, const Sp<Query>& q = {});
|
||||
|
||||
/**
|
||||
* Refill the search with good nodes if possible.
|
||||
@@ -620,7 +609,6 @@ private:
|
||||
void searchStep(std::weak_ptr<Search> ws);
|
||||
|
||||
void searchSynchedNodeListen(const Sp<Search>&, SearchNode&);
|
||||
void searchNodeListenDeadline(const Sp<Search>&, SearchNode&, const Sp<Query>&);
|
||||
|
||||
void dumpSearch(const Search& sr, std::ostream& out) const;
|
||||
|
||||
@@ -638,13 +626,8 @@ private:
|
||||
net::RequestAnswer onGetValues(Sp<Node> node, const InfoHash& hash, want_t want, const Query& q);
|
||||
void onGetValuesDone(const Sp<Node>& status, net::RequestAnswer& a, Sp<Search>& sr, const Sp<Query>& orig_query);
|
||||
/* when we receive a listen request */
|
||||
net::RequestAnswer onListen(Sp<Node> node,
|
||||
const InfoHash& hash,
|
||||
const Blob& token,
|
||||
size_t socket_id,
|
||||
const Query& query,
|
||||
int version = 0,
|
||||
const std::optional<net::ListenSync>& sync = {});
|
||||
net::RequestAnswer onListen(
|
||||
Sp<Node> node, const InfoHash& hash, const Blob& token, size_t socket_id, const Query& query, int version = 0);
|
||||
void onListenDone(const Sp<Node>& node, net::RequestAnswer& a, Sp<Search>& sr);
|
||||
/* when we receive an announce request */
|
||||
net::RequestAnswer onAnnounce(Sp<Node> node,
|
||||
|
||||
@@ -79,7 +79,8 @@ public:
|
||||
|
||||
virtual time_point periodic(const uint8_t* buf, size_t buflen, SockAddr, const time_point& now) = 0;
|
||||
virtual time_point periodic(
|
||||
const uint8_t* buf, size_t buflen, const sockaddr* from, socklen_t fromlen, const time_point& now) = 0;
|
||||
const uint8_t* buf, size_t buflen, const sockaddr* from, socklen_t fromlen, const time_point& now)
|
||||
= 0;
|
||||
|
||||
/**
|
||||
* Get a value by searching on all available protocols (IPv4, IPv6),
|
||||
@@ -92,22 +93,17 @@ public:
|
||||
* @param f a filter function used to prefilter values.
|
||||
*/
|
||||
virtual void get(
|
||||
const InfoHash& key, GetCallback cb, DoneCallback donecb = {}, Value::Filter&& f = {}, Where&& w = {}) = 0;
|
||||
virtual void get(const InfoHash& key,
|
||||
GetCallback cb,
|
||||
DoneCallbackSimple donecb = {},
|
||||
Value::Filter&& f = {},
|
||||
Where&& w = {}) = 0;
|
||||
virtual void get(const InfoHash& key,
|
||||
GetCallbackSimple cb,
|
||||
DoneCallback donecb = {},
|
||||
Value::Filter&& f = {},
|
||||
Where&& w = {}) = 0;
|
||||
virtual void get(const InfoHash& key,
|
||||
GetCallbackSimple cb,
|
||||
DoneCallbackSimple donecb,
|
||||
Value::Filter&& f = {},
|
||||
Where&& w = {}) = 0;
|
||||
const InfoHash& key, GetCallback cb, DoneCallback donecb = {}, Value::Filter&& f = {}, Where&& w = {})
|
||||
= 0;
|
||||
virtual void get(
|
||||
const InfoHash& key, GetCallback cb, DoneCallbackSimple donecb = {}, Value::Filter&& f = {}, Where&& w = {})
|
||||
= 0;
|
||||
virtual void get(
|
||||
const InfoHash& key, GetCallbackSimple cb, DoneCallback donecb = {}, Value::Filter&& f = {}, Where&& w = {})
|
||||
= 0;
|
||||
virtual void get(
|
||||
const InfoHash& key, GetCallbackSimple cb, DoneCallbackSimple donecb, Value::Filter&& f = {}, Where&& w = {})
|
||||
= 0;
|
||||
|
||||
/**
|
||||
* Similar to Dht::get, but sends a Query to filter data remotely.
|
||||
@@ -142,22 +138,26 @@ public:
|
||||
Sp<Value>,
|
||||
DoneCallback cb = nullptr,
|
||||
time_point created = time_point::max(),
|
||||
bool permanent = false) = 0;
|
||||
bool permanent = false)
|
||||
= 0;
|
||||
virtual void put(const InfoHash& key,
|
||||
const Sp<Value>& v,
|
||||
DoneCallbackSimple cb,
|
||||
time_point created = time_point::max(),
|
||||
bool permanent = false) = 0;
|
||||
bool permanent = false)
|
||||
= 0;
|
||||
virtual void put(const InfoHash& key,
|
||||
Value&& v,
|
||||
DoneCallback cb = nullptr,
|
||||
time_point created = time_point::max(),
|
||||
bool permanent = false) = 0;
|
||||
bool permanent = false)
|
||||
= 0;
|
||||
virtual void put(const InfoHash& key,
|
||||
Value&& v,
|
||||
DoneCallbackSimple cb,
|
||||
time_point created = time_point::max(),
|
||||
bool permanent = false) = 0;
|
||||
bool permanent = false)
|
||||
= 0;
|
||||
|
||||
/**
|
||||
* Get data currently being put at the given hash.
|
||||
@@ -261,25 +261,6 @@ public:
|
||||
*/
|
||||
virtual PushNotificationResult pushNotificationReceived(const std::map<std::string, std::string>& data) = 0;
|
||||
|
||||
/**
|
||||
* Listen with a one-shot notification after initial synchronization completes
|
||||
* on all enabled address families. Version-2 native peers complete after
|
||||
* initial delivery; legacy peers complete on LISTEN acknowledgment and may
|
||||
* deliver initial values after the notification.
|
||||
* @param ready called with false on initialization failure or shutdown.
|
||||
* Canceling the listener suppresses a pending readiness notification.
|
||||
* The callback may run before listen returns. Implementations that do
|
||||
* not support readiness report false while preserving the listener.
|
||||
*/
|
||||
virtual size_t listen(
|
||||
const InfoHash& key, ValueCallback cb, Value::Filter filter, Where where, DoneCallbackSimple ready)
|
||||
{
|
||||
auto token = listen(key, std::move(cb), std::move(filter), std::move(where));
|
||||
if (ready)
|
||||
ready(false);
|
||||
return token;
|
||||
}
|
||||
|
||||
protected:
|
||||
std::shared_ptr<Logger> logger_ {};
|
||||
std::queue<std::function<void()>> onConnectCallbacks_ {};
|
||||
|
||||
@@ -189,7 +189,6 @@ public:
|
||||
* @return a token to cancel the listener later.
|
||||
*/
|
||||
virtual size_t listen(const InfoHash&, ValueCallback, Value::Filter = {}, Where = {}) override;
|
||||
size_t listen(const InfoHash&, ValueCallback, Value::Filter, Where, DoneCallbackSimple ready) override;
|
||||
|
||||
virtual size_t listen(const InfoHash& key, GetCallback cb, Value::Filter f = {}, Where w = {}) override
|
||||
{
|
||||
@@ -292,10 +291,9 @@ public:
|
||||
virtual size_t getStorageLimit() const override { return 0; }
|
||||
void setLocalStorageLimit(size_t) override {}
|
||||
virtual size_t getLocalStorageLimit() const override { return STORAGE_LIMIT_UNLIMITED; }
|
||||
void connectivityChanged(sa_family_t) override { connectivityChanged(); }
|
||||
void connectivityChanged(sa_family_t) override { getProxyInfos(); }
|
||||
void connectivityChanged() override
|
||||
{
|
||||
launchConnectedCbs_ = true;
|
||||
getProxyInfos();
|
||||
loopSignal_();
|
||||
}
|
||||
@@ -331,6 +329,7 @@ private:
|
||||
enum class ListenMethod {
|
||||
LISTEN,
|
||||
SUBSCRIBE,
|
||||
RESUBSCRIBE,
|
||||
};
|
||||
using CacheValueCallback
|
||||
= std::function<bool(const std::vector<std::shared_ptr<Value>>& values, bool expired, system_clock::time_point)>;
|
||||
@@ -342,16 +341,11 @@ private:
|
||||
const CacheValueCallback& cb,
|
||||
const Sp<OperationState>& opstate,
|
||||
Listener& listener,
|
||||
ListenMethod method,
|
||||
const InfoHash& key,
|
||||
size_t token);
|
||||
ListenMethod method = ListenMethod::LISTEN);
|
||||
void handleResubscribe(const asio::error_code& ec,
|
||||
const InfoHash& key,
|
||||
const size_t token,
|
||||
std::shared_ptr<OperationState> opstate);
|
||||
void scheduleListenRetry(const InfoHash& key, size_t token, Listener& listener);
|
||||
bool reconcileListenerSnapshot(Listener& listener, const std::vector<Sp<Value>>& values, const Json::Value& marker);
|
||||
std::chrono::seconds subscriptionRequestTimeout_ {30};
|
||||
|
||||
void doPut(const InfoHash&, Sp<Value>, DoneCallbackSimple, time_point created, bool permanent);
|
||||
void handleRefreshPut(const asio::error_code& ec, InfoHash key, Value::Id id);
|
||||
@@ -425,7 +419,6 @@ private:
|
||||
*/
|
||||
std::mutex lockCallbacks_;
|
||||
std::vector<std::function<void()>> callbacks_;
|
||||
std::vector<std::function<void()>> readinessCallbacks_;
|
||||
|
||||
Sp<InfoState> infoState_;
|
||||
|
||||
@@ -444,8 +437,6 @@ private:
|
||||
/**
|
||||
* Refresh a listen via a token
|
||||
* @param token
|
||||
* @param fetchValues ask the server to send the values it stores, for a listener
|
||||
* that may have missed some (its previous subscription failed).
|
||||
*/
|
||||
void resubscribe(const InfoHash& key, const size_t token, Listener& listener);
|
||||
|
||||
@@ -477,6 +468,7 @@ private:
|
||||
const std::function<void()> loopSignal_;
|
||||
|
||||
#ifdef OPENDHT_PUSH_NOTIFICATIONS
|
||||
std::string fillBody(bool resubscribe);
|
||||
void getPushRequest(Json::Value&) const;
|
||||
#endif // OPENDHT_PUSH_NOTIFICATIONS
|
||||
|
||||
|
||||
@@ -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
|
||||
{
|
||||
@@ -272,19 +266,12 @@ private:
|
||||
{
|
||||
std::mutex lock;
|
||||
std::string sessionId;
|
||||
const std::string epoch {InfoHash::getRandom().toString()};
|
||||
uint64_t revision {0};
|
||||
std::map<Value::Id, Json::Value> values;
|
||||
bool initialized {false};
|
||||
bool initialOk {false};
|
||||
std::vector<std::function<void()>> pendingSnapshots;
|
||||
PushSessionContext(const std::string& id)
|
||||
: sessionId(id)
|
||||
{}
|
||||
};
|
||||
|
||||
#ifdef OPENDHT_PUSH_NOTIFICATIONS
|
||||
void onPushListenReady(const std::shared_ptr<PushSessionContext>& sessionCtx, bool ok);
|
||||
PushType getTypeFromString(const std::string& type);
|
||||
std::string getDefaultTopic(PushType type);
|
||||
|
||||
@@ -414,12 +401,12 @@ private:
|
||||
std::shared_ptr<ConnectionListener> connListener_;
|
||||
struct PermanentPut
|
||||
{
|
||||
sys_time_point expiration;
|
||||
time_point expiration;
|
||||
std::string pushToken;
|
||||
std::string clientId;
|
||||
std::shared_ptr<PushSessionContext> sessionCtx;
|
||||
std::unique_ptr<asio::system_timer> expireTimer;
|
||||
std::unique_ptr<asio::system_timer> expireNotifyTimer;
|
||||
std::unique_ptr<asio::steady_timer> expireTimer;
|
||||
std::unique_ptr<asio::steady_timer> expireNotifyTimer;
|
||||
Sp<Value> value;
|
||||
PushType type;
|
||||
std::string topic;
|
||||
@@ -432,7 +419,7 @@ private:
|
||||
p.pack("value");
|
||||
p.pack(value);
|
||||
p.pack("exp");
|
||||
p.pack(sys_clock::to_time_t(expiration));
|
||||
p.pack(to_time_t(expiration));
|
||||
if (not clientId.empty()) {
|
||||
p.pack("cid");
|
||||
p.pack(clientId);
|
||||
@@ -473,12 +460,12 @@ private:
|
||||
#ifdef OPENDHT_PUSH_NOTIFICATIONS
|
||||
struct Listener
|
||||
{
|
||||
sys_time_point expiration;
|
||||
time_point expiration;
|
||||
std::string clientId;
|
||||
std::shared_ptr<PushSessionContext> sessionCtx;
|
||||
std::future<size_t> internalToken;
|
||||
std::unique_ptr<asio::system_timer> expireTimer;
|
||||
std::unique_ptr<asio::system_timer> expireNotifyTimer;
|
||||
std::unique_ptr<asio::steady_timer> expireTimer;
|
||||
std::unique_ptr<asio::steady_timer> expireNotifyTimer;
|
||||
PushType type;
|
||||
std::string topic;
|
||||
|
||||
@@ -489,7 +476,7 @@ private:
|
||||
p.pack("cid");
|
||||
p.pack(clientId);
|
||||
p.pack("exp");
|
||||
p.pack(sys_clock::to_time_t(expiration));
|
||||
p.pack(to_time_t(expiration));
|
||||
if (sessionCtx) {
|
||||
std::lock_guard l(sessionCtx->lock);
|
||||
p.pack("sid");
|
||||
|
||||
@@ -156,7 +156,6 @@ public:
|
||||
}
|
||||
|
||||
std::future<size_t> listen(InfoHash key, ValueCallback vcb, Value::Filter f = {}, Where w = {});
|
||||
std::future<size_t> listen(InfoHash key, ValueCallback vcb, Value::Filter f, Where w, DoneCallbackSimple ready);
|
||||
|
||||
std::future<size_t> listen(InfoHash key, GetCallback cb, Value::Filter f = {}, Where w = {})
|
||||
{
|
||||
@@ -533,8 +532,6 @@ public:
|
||||
|
||||
/**
|
||||
* Insert a push notification to process for OpenDHT
|
||||
* Returns an immediately ready future with IgnoredStopped if the runner is idle,
|
||||
* or IgnoredDisabled if push notification support is disabled at build time.
|
||||
*/
|
||||
std::future<PushNotificationResult> pushNotificationReceived(const std::map<std::string, std::string>& data);
|
||||
|
||||
|
||||
@@ -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
|
||||
{
|
||||
|
||||
@@ -19,7 +19,6 @@
|
||||
#include <algorithm>
|
||||
#include <memory>
|
||||
#include <queue>
|
||||
#include <optional>
|
||||
|
||||
namespace dht {
|
||||
namespace net {
|
||||
@@ -79,15 +78,6 @@ private:
|
||||
|
||||
struct ParsedMessage;
|
||||
|
||||
struct ListenSync
|
||||
{
|
||||
uint64_t registration {0};
|
||||
uint64_t epoch {0};
|
||||
uint64_t sequence {0};
|
||||
uint64_t snapshotEnd {0};
|
||||
MSGPACK_DEFINE_ARRAY(registration, epoch, sequence, snapshotEnd)
|
||||
};
|
||||
|
||||
/**
|
||||
* Answer for a request.
|
||||
*/
|
||||
@@ -101,7 +91,6 @@ struct RequestAnswer
|
||||
std::vector<Sp<FieldValueIndex>> fields {};
|
||||
std::vector<Sp<Node>> nodes4 {};
|
||||
std::vector<Sp<Node>> nodes6 {};
|
||||
std::optional<ListenSync> listenSync {};
|
||||
RequestAnswer() {}
|
||||
RequestAnswer(ParsedMessage&& msg);
|
||||
};
|
||||
@@ -178,9 +167,7 @@ private:
|
||||
* @param token (type: Blob) security token.
|
||||
* @param rid (type: uint16_t) request id.
|
||||
*/
|
||||
std::function<
|
||||
RequestAnswer(Sp<Node>, const InfoHash&, const Blob&, Tid, const Query&, int, const std::optional<ListenSync>&)>
|
||||
onListen {};
|
||||
std::function<RequestAnswer(Sp<Node>, const InfoHash&, const Blob&, Tid, const Query&, int)> onListen {};
|
||||
/**
|
||||
* Called on announce request.
|
||||
*
|
||||
@@ -244,32 +231,29 @@ public:
|
||||
* @param values The values to send.
|
||||
* @param version If version = 1, a request will be used to answer to the listener
|
||||
*/
|
||||
std::vector<Sp<Request>> tellListener(const Sp<Node>& n,
|
||||
Tid socket_id,
|
||||
const InfoHash& hash,
|
||||
want_t want,
|
||||
const Blob& ntoken,
|
||||
std::vector<Sp<Node>>&& nodes,
|
||||
std::vector<Sp<Node>>&& nodes6,
|
||||
std::vector<Sp<Value>>&& values,
|
||||
const Query& q,
|
||||
int version,
|
||||
ListenSync* sync = nullptr);
|
||||
void tellListener(const Sp<Node>& n,
|
||||
Tid socket_id,
|
||||
const InfoHash& hash,
|
||||
want_t want,
|
||||
const Blob& ntoken,
|
||||
std::vector<Sp<Node>>&& nodes,
|
||||
std::vector<Sp<Node>>&& nodes6,
|
||||
std::vector<Sp<Value>>&& values,
|
||||
const Query& q,
|
||||
int version);
|
||||
|
||||
void tellListenerRefreshed(const Sp<Node>& n,
|
||||
Tid socket_id,
|
||||
const InfoHash& hash,
|
||||
const Blob& ntoken,
|
||||
const std::vector<Value::Id>& values,
|
||||
int version,
|
||||
ListenSync* sync = nullptr);
|
||||
int version);
|
||||
void tellListenerExpired(const Sp<Node>& n,
|
||||
Tid socket_id,
|
||||
const InfoHash& hash,
|
||||
const Blob& ntoken,
|
||||
const std::vector<Value::Id>& values,
|
||||
int version,
|
||||
ListenSync* sync = nullptr);
|
||||
int version);
|
||||
|
||||
bool isRunning(sa_family_t af) const;
|
||||
inline want_t want() const { return dht_socket->hasIPv4() and dht_socket->hasIPv6() ? (WANT4 | WANT6) : -1; }
|
||||
@@ -374,8 +358,7 @@ public:
|
||||
const Blob& token,
|
||||
Tid socketId,
|
||||
RequestCb&& on_done,
|
||||
RequestExpiredCb&& on_expired,
|
||||
const std::optional<ListenSync>& sync = {});
|
||||
RequestExpiredCb&& on_expired);
|
||||
/**
|
||||
* Send a "announce" request to a given node.
|
||||
*
|
||||
@@ -428,22 +411,19 @@ public:
|
||||
*
|
||||
* @return the request with information concerning its success.
|
||||
*/
|
||||
static size_t getUpdateBatchCount(const std::vector<Sp<Value>>& values);
|
||||
std::vector<Sp<Request>> sendUpdateValues(const Sp<Node>& n,
|
||||
const InfoHash& infohash,
|
||||
std::vector<Sp<Value>>&& values,
|
||||
time_point created,
|
||||
const Blob& token,
|
||||
size_t sid,
|
||||
ListenSync* sync = nullptr);
|
||||
void sendUpdateValues(const Sp<Node>& n,
|
||||
const InfoHash& infohash,
|
||||
std::vector<Sp<Value>>&& values,
|
||||
time_point created,
|
||||
const Blob& token,
|
||||
size_t sid);
|
||||
Sp<Request> sendUpdateValues(const Sp<Node>& n,
|
||||
const InfoHash& infohash,
|
||||
std::vector<Sp<Value>>::iterator begin,
|
||||
std::vector<Sp<Value>>::iterator end,
|
||||
time_point created,
|
||||
const Blob& token,
|
||||
size_t sid,
|
||||
ListenSync* sync = nullptr);
|
||||
size_t sid);
|
||||
|
||||
/**
|
||||
* Parses a message and calls appropriate callbacks.
|
||||
@@ -572,10 +552,7 @@ private:
|
||||
std::pair<Blob, Blob> bufferNodes(
|
||||
sa_family_t af, const InfoHash& id, want_t want, std::vector<Sp<Node>>& nodes, std::vector<Sp<Node>>& nodes6);
|
||||
/* answer to a listen request */
|
||||
void sendListenConfirmation(const SockAddr& addr,
|
||||
Tid tid,
|
||||
const Blob& token = {},
|
||||
const std::optional<ListenSync>& sync = {});
|
||||
void sendListenConfirmation(const SockAddr& addr, Tid tid, const Blob& token = {});
|
||||
/* answer to put request */
|
||||
void sendValueAnnounced(const SockAddr& addr, Tid, Value::Id);
|
||||
/* answer in case of error */
|
||||
|
||||
@@ -3,12 +3,13 @@
|
||||
#pragma once
|
||||
|
||||
#include "def.h"
|
||||
#include "sockaddr.h"
|
||||
#include "infohash.h"
|
||||
#include "logger.h"
|
||||
#include "sockaddr.h"
|
||||
#include "utils.h"
|
||||
|
||||
#include <asio/steady_timer.hpp>
|
||||
#include <functional>
|
||||
#include <memory>
|
||||
#include <thread>
|
||||
|
||||
namespace asio {
|
||||
@@ -20,66 +21,26 @@ namespace dht {
|
||||
class OPENDHT_PUBLIC PeerDiscovery
|
||||
{
|
||||
public:
|
||||
static constexpr in_port_t DEFAULT_PORT = 8888;
|
||||
using ServiceDiscoveredCallback = std::function<void(msgpack::object&&, SockAddr&&)>;
|
||||
using PeerDiscoveredCallback = std::function<void(const InfoHash&, SockAddr&&)>;
|
||||
|
||||
PeerDiscovery(in_port_t port = DEFAULT_PORT,
|
||||
std::shared_ptr<asio::io_context> ioContext = {},
|
||||
std::shared_ptr<Logger> logger = {});
|
||||
PeerDiscovery(std::shared_ptr<asio::io_context> ioContext = {}, std::shared_ptr<Logger> logger = {});
|
||||
~PeerDiscovery();
|
||||
|
||||
/**
|
||||
* startDiscovery - Keep Listening data from the sender until node is joinned
|
||||
* or stop is called
|
||||
*/
|
||||
void startDiscovery(std::string_view type, ServiceDiscoveredCallback callback);
|
||||
void startDiscovery(NetId network, PeerDiscoveredCallback callback);
|
||||
void startPublish(const InfoHash& nodeId, NetId network, in_port_t port);
|
||||
|
||||
template<typename T>
|
||||
void startDiscovery(std::string_view type, std::function<void(T&&, SockAddr&&)> cb)
|
||||
{
|
||||
startDiscovery(type, [cb](msgpack::object&& ob, SockAddr&& addr) { cb(ob.as<T>(), std::move(addr)); });
|
||||
}
|
||||
|
||||
/**
|
||||
* startPublish - Keeping sending data until node is joinned or stop is called
|
||||
*/
|
||||
void startPublish(std::string_view type, const msgpack::sbuffer& pack_buf);
|
||||
void startPublish(sa_family_t domain, std::string_view type, const msgpack::sbuffer& pack_buf);
|
||||
|
||||
template<typename T>
|
||||
void startPublish(std::string_view type, const T& object)
|
||||
{
|
||||
msgpack::sbuffer buf;
|
||||
msgpack::pack(buf, object);
|
||||
startPublish(type, buf);
|
||||
}
|
||||
|
||||
/**
|
||||
* Thread Stopper
|
||||
*/
|
||||
bool stopDiscovery();
|
||||
bool stopPublish();
|
||||
void stop();
|
||||
|
||||
/**
|
||||
* Remove possible callBack to discovery
|
||||
*/
|
||||
bool stopDiscovery(std::string_view type);
|
||||
|
||||
/**
|
||||
* Remove different serivce message to send
|
||||
*/
|
||||
bool stopPublish(std::string_view type);
|
||||
bool stopPublish(sa_family_t domain, std::string_view type);
|
||||
|
||||
void connectivityChanged();
|
||||
|
||||
void stopConnectivityChanged();
|
||||
|
||||
private:
|
||||
class DomainPeerDiscovery;
|
||||
std::unique_ptr<DomainPeerDiscovery> peerDiscovery4_;
|
||||
std::unique_ptr<DomainPeerDiscovery> peerDiscovery6_;
|
||||
class Impl;
|
||||
std::shared_ptr<Impl> impl_;
|
||||
std::shared_ptr<asio::io_context> ioContext_;
|
||||
std::thread ioRunnner_;
|
||||
std::thread ioRunner_;
|
||||
};
|
||||
|
||||
} // namespace dht
|
||||
} // namespace dht
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -272,7 +272,6 @@ public:
|
||||
bool cancelPut(const InfoHash& h, const Value::Id& vid) override { return dht_->cancelPut(h, vid); }
|
||||
|
||||
size_t listen(const InfoHash& key, ValueCallback, Value::Filter = {}, Where = {}) override;
|
||||
size_t listen(const InfoHash& key, ValueCallback, Value::Filter, Where, DoneCallbackSimple ready) override;
|
||||
size_t listen(const InfoHash& key, GetCallback cb, Value::Filter = {}, Where w = {}) override;
|
||||
size_t listen(const InfoHash& key, GetCallbackSimple cb, Value::Filter f = {}, Where w = {}) override
|
||||
{
|
||||
|
||||
+20
-37
@@ -5,7 +5,7 @@ project(
|
||||
'cython',
|
||||
license: 'MIT',
|
||||
license_files: 'LICENSE',
|
||||
version: '5.0.0',
|
||||
version: '4.3.0',
|
||||
meson_version: '>=1.1.0',
|
||||
default_options: ['cpp_std=c++20', 'warning_level=3'],
|
||||
)
|
||||
@@ -123,15 +123,21 @@ if get_option('push_notifications').enabled()
|
||||
add_project_arguments('-DOPENDHT_PUSH_NOTIFICATIONS', language: 'cpp')
|
||||
endif
|
||||
if get_option('peer_discovery').enabled()
|
||||
opendht_src += 'src/peer_discovery.cpp'
|
||||
opendht_src += [
|
||||
'src/peer_discovery/mdns.cpp',
|
||||
'src/peer_discovery/dns_sd.cpp',
|
||||
'src/peer_discovery/mdns_transport.cpp',
|
||||
'src/peer_discovery/peer_discovery.cpp',
|
||||
]
|
||||
add_project_arguments('-DOPENDHT_PEER_DISCOVERY', language: 'cpp')
|
||||
if host_machine.system() == 'windows'
|
||||
deps += meson.get_compiler('cpp').find_library('iphlpapi')
|
||||
endif
|
||||
endif
|
||||
opendht = library(
|
||||
'opendht',
|
||||
opendht_src,
|
||||
version: meson.project_version(),
|
||||
soversion: '5',
|
||||
include_directories: opendht_inc,
|
||||
include_directories: [opendht_inc, opendht_interface_inc],
|
||||
dependencies: deps,
|
||||
cpp_args: ['-DOPENDHT_BUILD', '-Dopendht_EXPORTS'],
|
||||
install: true,
|
||||
@@ -180,8 +186,6 @@ if get_option('c').enabled()
|
||||
opendht_c = library(
|
||||
'opendht-c',
|
||||
['c/opendht.cpp', 'c/opendht_c.h'],
|
||||
version: meson.project_version(),
|
||||
soversion: '5',
|
||||
dependencies: [opendht_dep, msgpack, jsoncpp, fmt],
|
||||
cpp_args: ['-DOPENDHT_C_BUILD', '-Dopendht_c_EXPORTS'],
|
||||
install: true,
|
||||
@@ -296,24 +300,6 @@ if get_option('tests').enabled()
|
||||
)
|
||||
test('NetworkEngine', test_networkengine)
|
||||
|
||||
test_opcache = executable(
|
||||
'test_opcache',
|
||||
'tests/test_opcache.cpp',
|
||||
'tests/tests_runner.cpp',
|
||||
include_directories: opendht_inc,
|
||||
dependencies: [opendht_dep, cppunit, jsoncpp, fmt, openssl, msgpack],
|
||||
)
|
||||
test('OpCache', test_opcache)
|
||||
|
||||
test_valuecache = executable(
|
||||
'test_valuecache',
|
||||
'tests/test_valuecache.cpp',
|
||||
'tests/tests_runner.cpp',
|
||||
include_directories: opendht_inc,
|
||||
dependencies: [opendht_dep, cppunit, jsoncpp, fmt, openssl, msgpack],
|
||||
)
|
||||
test('ValueCache', test_valuecache)
|
||||
|
||||
test_crypto = executable(
|
||||
'test_crypto',
|
||||
'tests/test_crypto.cpp',
|
||||
@@ -348,18 +334,15 @@ if get_option('tests').enabled()
|
||||
test('Http', test_http)
|
||||
endif
|
||||
|
||||
if get_option('proxy_client').enabled()
|
||||
test_proxy_client = executable(
|
||||
'test_dhtproxy_client',
|
||||
'tests/test_dhtproxy_client.cpp',
|
||||
'tests/tests_runner.cpp',
|
||||
include_directories: opendht_inc,
|
||||
dependencies: [opendht_dep, cppunit, jsoncpp, fmt, openssl, msgpack, llhttp],
|
||||
)
|
||||
test('DhtProxyClient', test_proxy_client)
|
||||
endif
|
||||
|
||||
if get_option('peer_discovery').enabled()
|
||||
test_mdns = executable(
|
||||
'test_mdns',
|
||||
'tests/test_mdns.cpp',
|
||||
'tests/tests_runner.cpp',
|
||||
dependencies: [opendht_dep, cppunit, jsoncpp, fmt, openssl, msgpack],
|
||||
)
|
||||
test('Mdns', test_mdns)
|
||||
|
||||
test_peerdiscovery = executable(
|
||||
'test_peerdiscovery',
|
||||
'tests/test_peerdiscovery.cpp',
|
||||
@@ -376,7 +359,7 @@ if get_option('tests').enabled()
|
||||
'tests/tests_runner.cpp',
|
||||
dependencies: [opendht_dep, cppunit, jsoncpp, fmt, openssl, msgpack],
|
||||
)
|
||||
test('DhtProxy', test_proxy, timeout: 120)
|
||||
test('DhtProxy', test_proxy)
|
||||
|
||||
if get_option('long_tests').enabled()
|
||||
test_proxystress = executable(
|
||||
|
||||
@@ -1,118 +0,0 @@
|
||||
diff --git a/SMP/libgnutls.vcxproj.filters b/SMP/libgnutls.vcxproj.filters
|
||||
index b182189..91ee160 100644
|
||||
--- a/SMP/libgnutls.vcxproj.filters
|
||||
+++ b/SMP/libgnutls.vcxproj.filters
|
||||
@@ -103,9 +103,6 @@
|
||||
<Filter Include="Header Files\lib\inih">
|
||||
<UniqueIdentifier>{ae0c3eeb-53df-4c72-a85a-6b46de35e7ba}</UniqueIdentifier>
|
||||
</Filter>
|
||||
- <Filter Include="Source Files\libtasn1">
|
||||
- <UniqueIdentifier>{32be60b7-8c65-486e-9df5-7e529443cf07}</UniqueIdentifier>
|
||||
- </Filter>
|
||||
<Filter Include="Source Files\lib\nettle\gost">
|
||||
<UniqueIdentifier>{1f3549a8-3c3d-475f-8fd6-20451d336464}</UniqueIdentifier>
|
||||
</Filter>
|
||||
@@ -1553,30 +1550,6 @@
|
||||
<ClCompile Include="..\lib\nettle\sysrng-bcrypt.c">
|
||||
<Filter>Source Files\lib\nettle</Filter>
|
||||
</ClCompile>
|
||||
- <ClCompile Include="..\devel\libtasn1\lib\coding.c">
|
||||
- <Filter>Source Files\libtasn1</Filter>
|
||||
- </ClCompile>
|
||||
- <ClCompile Include="..\devel\libtasn1\lib\decoding.c">
|
||||
- <Filter>Source Files\libtasn1</Filter>
|
||||
- </ClCompile>
|
||||
- <ClCompile Include="..\devel\libtasn1\lib\element.c">
|
||||
- <Filter>Source Files\libtasn1</Filter>
|
||||
- </ClCompile>
|
||||
- <ClCompile Include="..\devel\libtasn1\lib\errors.c">
|
||||
- <Filter>Source Files\libtasn1</Filter>
|
||||
- </ClCompile>
|
||||
- <ClCompile Include="..\devel\libtasn1\lib\gstr.c">
|
||||
- <Filter>Source Files\libtasn1</Filter>
|
||||
- </ClCompile>
|
||||
- <ClCompile Include="..\devel\libtasn1\lib\parser_aux.c">
|
||||
- <Filter>Source Files\libtasn1</Filter>
|
||||
- </ClCompile>
|
||||
- <ClCompile Include="..\devel\libtasn1\lib\structure.c">
|
||||
- <Filter>Source Files\libtasn1</Filter>
|
||||
- </ClCompile>
|
||||
- <ClCompile Include="..\devel\libtasn1\lib\version.c">
|
||||
- <Filter>Source Files\libtasn1</Filter>
|
||||
- </ClCompile>
|
||||
<ClCompile Include="..\lib\nettle\gost\acpkm.c">
|
||||
<Filter>Source Files\lib\nettle\gost</Filter>
|
||||
</ClCompile>
|
||||
diff --git a/SMP/libgnutls_files.props b/SMP/libgnutls_files.props
|
||||
index adcb074..2317c2a 100644
|
||||
--- a/SMP/libgnutls_files.props
|
||||
+++ b/SMP/libgnutls_files.props
|
||||
@@ -179,13 +179,6 @@
|
||||
<ItemGroup>
|
||||
<ClCompile Include="lib\gnutls_asn1_tab.c" />
|
||||
<ClCompile Include="lib\pkix_asn1_tab.c" />
|
||||
- <ClCompile Include="..\devel\libtasn1\lib\coding.c" />
|
||||
- <ClCompile Include="..\devel\libtasn1\lib\decoding.c" />
|
||||
- <ClCompile Include="..\devel\libtasn1\lib\element.c" />
|
||||
- <ClCompile Include="..\devel\libtasn1\lib\gstr.c" />
|
||||
- <ClCompile Include="..\devel\libtasn1\lib\parser_aux.c" />
|
||||
- <ClCompile Include="..\devel\libtasn1\lib\structure.c" />
|
||||
- <ClCompile Include="..\devel\libtasn1\lib\version.c" />
|
||||
<ClCompile Include="..\gnulib\lib\c-strcasecmp.c" />
|
||||
<ClCompile Include="..\gnulib\lib\c-strncasecmp.c" />
|
||||
<ClCompile Include="..\gnulib\lib\cloexec.c" />
|
||||
@@ -572,9 +565,6 @@
|
||||
<ClCompile Include="..\gnulib\lib\readdir.c">
|
||||
<ForcedIncludeFiles>dirent-private.h;%(ForcedIncludeFiles)</ForcedIncludeFiles>
|
||||
</ClCompile>
|
||||
- <ClCompile Include="..\devel\libtasn1\lib\errors.c">
|
||||
- <ObjectFileName>$(IntDir)\tasn1_$(filename).obj</ObjectFileName>
|
||||
- </ClCompile>
|
||||
<ClCompile Include="..\lib\algorithms\ecc.c">
|
||||
<ObjectFileName>$(IntDir)\alg_%(Filename).obj</ObjectFileName>
|
||||
</ClCompile>
|
||||
diff --git a/SMP/libgnutls_winrt.vcxproj.filters b/SMP/libgnutls_winrt.vcxproj.filters
|
||||
index 014a408..ae5ecff 100644
|
||||
--- a/SMP/libgnutls_winrt.vcxproj.filters
|
||||
+++ b/SMP/libgnutls_winrt.vcxproj.filters
|
||||
@@ -103,9 +103,6 @@
|
||||
<Filter Include="Header Files\lib\inih">
|
||||
<UniqueIdentifier>{ae0c3eeb-53df-4c72-a85a-6b46de35e7ba}</UniqueIdentifier>
|
||||
</Filter>
|
||||
- <Filter Include="Source Files\libtasn1">
|
||||
- <UniqueIdentifier>{32be60b7-8c65-486e-9df5-7e529443cf07}</UniqueIdentifier>
|
||||
- </Filter>
|
||||
<Filter Include="Header Files\lib\nettle\gost">
|
||||
<UniqueIdentifier>{4fada990-3138-4089-a6c7-ae722a0e7fe9}</UniqueIdentifier>
|
||||
</Filter>
|
||||
@@ -1553,30 +1550,6 @@
|
||||
<ClCompile Include="..\lib\nettle\sysrng-bcrypt.c">
|
||||
<Filter>Source Files\lib\nettle</Filter>
|
||||
</ClCompile>
|
||||
- <ClCompile Include="..\devel\libtasn1\lib\coding.c">
|
||||
- <Filter>Source Files\libtasn1</Filter>
|
||||
- </ClCompile>
|
||||
- <ClCompile Include="..\devel\libtasn1\lib\decoding.c">
|
||||
- <Filter>Source Files\libtasn1</Filter>
|
||||
- </ClCompile>
|
||||
- <ClCompile Include="..\devel\libtasn1\lib\element.c">
|
||||
- <Filter>Source Files\libtasn1</Filter>
|
||||
- </ClCompile>
|
||||
- <ClCompile Include="..\devel\libtasn1\lib\errors.c">
|
||||
- <Filter>Source Files\libtasn1</Filter>
|
||||
- </ClCompile>
|
||||
- <ClCompile Include="..\devel\libtasn1\lib\gstr.c">
|
||||
- <Filter>Source Files\libtasn1</Filter>
|
||||
- </ClCompile>
|
||||
- <ClCompile Include="..\devel\libtasn1\lib\parser_aux.c">
|
||||
- <Filter>Source Files\libtasn1</Filter>
|
||||
- </ClCompile>
|
||||
- <ClCompile Include="..\devel\libtasn1\lib\structure.c">
|
||||
- <Filter>Source Files\libtasn1</Filter>
|
||||
- </ClCompile>
|
||||
- <ClCompile Include="..\devel\libtasn1\lib\version.c">
|
||||
- <Filter>Source Files\libtasn1</Filter>
|
||||
- </ClCompile>
|
||||
<ClCompile Include="..\lib\accelerated\afalg.c">
|
||||
<Filter>Source Files\lib\accelerated</Filter>
|
||||
</ClCompile>
|
||||
@@ -1,12 +0,0 @@
|
||||
--- a/lib/gnutls.pc.in
|
||||
+++ b/lib/gnutls.pc.in
|
||||
@@ -18,7 +18,7 @@
|
||||
Name: GnuTLS
|
||||
Description: Transport Security Layer implementation for the GNU system
|
||||
URL: https://www.gnutls.org/
|
||||
Version: @VERSION@
|
||||
-Libs: -L${libdir} -lgnutls
|
||||
+Libs: -L${libdir} @GNUTLS_LIBS@
|
||||
Libs.private: @LIBINTL@ @LIBSOCKET@ @INET_PTON_LIB@ @LIBPTHREAD@ @LIB_SELECT@ @TSS_LIBS@ @GMP_LIBS@ @LIBUNISTRING@ @LIBATOMIC_LIBS@ @GNUTLS_LIBS_PRIVATE@
|
||||
@GNUTLS_REQUIRES_PRIVATE@
|
||||
Cflags: -I${includedir}
|
||||
@@ -1,187 +0,0 @@
|
||||
set(GNULIB_REF "1cc0125a28cba4e8503d853bf99b854fe4dd454c")
|
||||
|
||||
vcpkg_from_github(
|
||||
OUT_SOURCE_PATH SOURCE_PATH
|
||||
REPO savoirfairelinux/gnutls
|
||||
REF 300b0c721fc1809f3d191b7f9e289c0267eabd01
|
||||
SHA512 8a74fbf0343e658f6321da0f28bcc05c580fb7b950f6edfb6a8fdaad6d814456ed2dbf94e8dd330c7740c836f01e04552e649d529ee29424c3641861c41344bf
|
||||
HEAD_REF fix/ci-msvc
|
||||
PATCHES
|
||||
external-libtasn1.patch
|
||||
pkgconfig.patch
|
||||
)
|
||||
|
||||
file(REMOVE_RECURSE "${SOURCE_PATH}/devel/perlasm")
|
||||
|
||||
vcpkg_download_distfile(
|
||||
GNULIB_SNAPSHOT
|
||||
URLS "https://gitlab.com/libidn/gnulib-mirror/-/archive/${GNULIB_REF}/gnulib-mirror-${GNULIB_REF}.tar.gz"
|
||||
FILENAME "gnulib-mirror-${GNULIB_REF}.tar.gz"
|
||||
SHA512 340539bd7b8e30b1e0f81a15fb207960e9ae3c4ef515f39f1d1bdb43ed52ad94a29c6a6ce8bbca3484ba5392e1dae7447c2f6b58124209614e884345e13914f5
|
||||
)
|
||||
|
||||
vcpkg_extract_source_archive(
|
||||
GNULIB_SOURCE_PATH
|
||||
ARCHIVE "${GNULIB_SNAPSHOT}"
|
||||
SOURCE_BASE ${GNULIB_REF}
|
||||
)
|
||||
|
||||
file(REMOVE_RECURSE "${SOURCE_PATH}/gnulib")
|
||||
file(RENAME "${GNULIB_SOURCE_PATH}" "${SOURCE_PATH}/gnulib")
|
||||
|
||||
include("${CURRENT_HOST_INSTALLED_DIR}/share/yasm-tool-helper/yasm-tool-helper.cmake")
|
||||
yasm_tool_helper(OUT_VAR YASM)
|
||||
file(TO_NATIVE_PATH "${YASM}" YASM)
|
||||
|
||||
if(VCPKG_LIBRARY_LINKAGE STREQUAL "dynamic")
|
||||
set(CONFIGURATION_RELEASE ReleaseDLL)
|
||||
set(CONFIGURATION_DEBUG DebugDLL)
|
||||
else()
|
||||
set(CONFIGURATION_RELEASE Release)
|
||||
set(CONFIGURATION_DEBUG Debug)
|
||||
endif()
|
||||
|
||||
if(VCPKG_TARGET_IS_UWP)
|
||||
string(APPEND CONFIGURATION_RELEASE WinRT)
|
||||
string(APPEND CONFIGURATION_DEBUG WinRT)
|
||||
endif()
|
||||
|
||||
set(_gnutlsproject "${SOURCE_PATH}/SMP/libgnutls.vcxproj")
|
||||
file(READ "${_gnutlsproject}" _contents)
|
||||
string(REPLACE [[<Import Project="$(VCTargetsPath)\BuildCustomizations\yasm.props" />]]
|
||||
"<Import Project=\"${CURRENT_HOST_INSTALLED_DIR}/share/vs-yasm/yasm.props\" />"
|
||||
_contents "${_contents}")
|
||||
string(REPLACE [[<Import Project="$(VCTargetsPath)\BuildCustomizations\yasm.targets" />]]
|
||||
"<Import Project=\"${CURRENT_HOST_INSTALLED_DIR}/share/vs-yasm/yasm.targets\" />"
|
||||
_contents "${_contents}")
|
||||
string(REGEX REPLACE "${VCPKG_ROOT_DIR}/installed/[^/]+/share" "${CURRENT_HOST_INSTALLED_DIR}/share" _contents "${_contents}") # Above already
|
||||
file(WRITE "${_gnutlsproject}" "${_contents}")
|
||||
|
||||
if(VCPKG_CRT_LINKAGE STREQUAL "static")
|
||||
set(RuntimeLibraryExt "")
|
||||
else()
|
||||
set(RuntimeLibraryExt "DLL")
|
||||
endif()
|
||||
|
||||
# patch output library file path and name
|
||||
foreach(PROPS IN ITEMS
|
||||
"${SOURCE_PATH}/SMP/smp_deps.props"
|
||||
"${SOURCE_PATH}/SMP/smp_winrt_deps.props")
|
||||
vcpkg_replace_string(
|
||||
"${PROPS}"
|
||||
[=[_winrt</TargetName>]=]
|
||||
[=[</TargetName>]=]
|
||||
IGNORE_UNCHANGED
|
||||
)
|
||||
vcpkg_replace_string(
|
||||
"${PROPS}"
|
||||
[=[<TargetName>lib$(RootNamespace)]=]
|
||||
[=[<TargetName>$(RootNamespace)]=]
|
||||
)
|
||||
endforeach()
|
||||
|
||||
# patch hogweed, gmp, nettle, zlib libraries file names; inject RuntimeLibrary property to control CRT linkage
|
||||
foreach(VCXPROJ IN ITEMS
|
||||
"${SOURCE_PATH}/SMP/libgnutls.vcxproj"
|
||||
"${SOURCE_PATH}/SMP/libgnutls_winrt.vcxproj")
|
||||
vcpkg_replace_string(
|
||||
"${VCXPROJ}"
|
||||
"_winrt.lib"
|
||||
".lib"
|
||||
IGNORE_UNCHANGED
|
||||
)
|
||||
vcpkg_replace_string(
|
||||
"${VCXPROJ}"
|
||||
"libhogweed"
|
||||
"hogweed"
|
||||
)
|
||||
vcpkg_replace_string(
|
||||
"${VCXPROJ}"
|
||||
"hogweedd"
|
||||
"hogweed"
|
||||
)
|
||||
vcpkg_replace_string(
|
||||
"${VCXPROJ}"
|
||||
"libgmp"
|
||||
"gmp"
|
||||
)
|
||||
vcpkg_replace_string(
|
||||
"${VCXPROJ}"
|
||||
"gmpd"
|
||||
"gmp"
|
||||
)
|
||||
vcpkg_replace_string(
|
||||
"${VCXPROJ}"
|
||||
"libnettle"
|
||||
"nettle"
|
||||
)
|
||||
vcpkg_replace_string(
|
||||
"${VCXPROJ}"
|
||||
"nettled"
|
||||
"nettle"
|
||||
)
|
||||
set(zlib_basename "zlib")
|
||||
if(EXISTS "${CURRENT_INSTALLED_DIR}/lib/zs.lib")
|
||||
set(zlib_basename "zs")
|
||||
elseif(EXISTS "${CURRENT_INSTALLED_DIR}/lib/z.lib")
|
||||
set(zlib_basename "z")
|
||||
endif()
|
||||
vcpkg_replace_string(
|
||||
"${VCXPROJ}"
|
||||
"libzlib"
|
||||
"${zlib_basename}"
|
||||
)
|
||||
vcpkg_replace_string(
|
||||
"${VCXPROJ}"
|
||||
"zlib"
|
||||
"${zlib_basename}"
|
||||
)
|
||||
vcpkg_replace_string(
|
||||
"${VCXPROJ}"
|
||||
[=[</DisableSpecificWarnings>]=]
|
||||
[=[</DisableSpecificWarnings><ForcedIncludeFiles>gnutls_msvc_fixes.h;%(ForcedIncludeFiles)</ForcedIncludeFiles><RuntimeLibrary>$(RuntimeLibrary)</RuntimeLibrary>]=]
|
||||
)
|
||||
endforeach()
|
||||
|
||||
vcpkg_install_msbuild(
|
||||
USE_VCPKG_INTEGRATION
|
||||
SOURCE_PATH "${SOURCE_PATH}"
|
||||
PROJECT_SUBPATH SMP/libgnutls.sln
|
||||
PLATFORM ${TRIPLET_SYSTEM_ARCH}
|
||||
LICENSE_SUBPATH LICENSE
|
||||
RELEASE_CONFIGURATION ${CONFIGURATION_RELEASE}
|
||||
DEBUG_CONFIGURATION ${CONFIGURATION_DEBUG}
|
||||
SKIP_CLEAN
|
||||
OPTIONS /p:YasmPath="${YASM}" /p:OutDir=..\\msvc
|
||||
OPTIONS_DEBUG /p:RuntimeLibrary=MultiThreadedDebug${RuntimeLibraryExt}
|
||||
OPTIONS_RELEASE /p:RuntimeLibrary=MultiThreaded${RuntimeLibraryExt}
|
||||
)
|
||||
|
||||
get_filename_component(SOURCE_PATH_SUFFIX "${SOURCE_PATH}" NAME)
|
||||
if(VCPKG_TARGET_IS_UWP)
|
||||
set(WINRT_SUBFOLDER libgnutls_winrt)
|
||||
endif()
|
||||
file(INSTALL "${CURRENT_BUILDTREES_DIR}/${TARGET_TRIPLET}-rel/${SOURCE_PATH_SUFFIX}/msvc/${WINRT_SUBFOLDER}/include" DESTINATION "${CURRENT_PACKAGES_DIR}")
|
||||
|
||||
set(GNUTLS_REQUIRES_PRIVATE "Requires.private: gmp, nettle, hogweed, libtasn1, zlib")
|
||||
set(GNUTLS_LIBS_PRIVATE "-lcrypt32 -lws2_32 -lkernel32 -lncrypt")
|
||||
|
||||
set(prefix "${CURRENT_INSTALLED_DIR}")
|
||||
set(exec_prefix "\${prefix}")
|
||||
set(libdir "\${prefix}/lib")
|
||||
set(includedir "\${prefix}/include")
|
||||
set(GNUTLS_LIBS "-lgnutls")
|
||||
configure_file("${SOURCE_PATH}/lib/gnutls.pc.in" "${CURRENT_PACKAGES_DIR}/lib/pkgconfig/gnutls.pc" @ONLY)
|
||||
|
||||
if(NOT VCPKG_BUILD_TYPE)
|
||||
set(prefix "${CURRENT_INSTALLED_DIR}/debug")
|
||||
set(exec_prefix "\${prefix}")
|
||||
set(libdir "\${prefix}/lib")
|
||||
set(includedir "\${prefix}/../include")
|
||||
set(GNUTLS_LIBS "-lgnutlsd")
|
||||
configure_file("${SOURCE_PATH}/lib/gnutls.pc.in" "${CURRENT_PACKAGES_DIR}/debug/lib/pkgconfig/gnutls.pc" @ONLY)
|
||||
endif()
|
||||
|
||||
vcpkg_fixup_pkgconfig()
|
||||
vcpkg_copy_pdbs()
|
||||
file(COPY "${CURRENT_PORT_DIR}/vcpkg-cmake-wrapper.cmake" DESTINATION "${CURRENT_PACKAGES_DIR}/share/gnutls")
|
||||
@@ -1,2 +0,0 @@
|
||||
find_library(GNUTLS_LIBRARY NAMES gnutls gnutlsd NAMES_PER_DIR)
|
||||
_find_package(${ARGS})
|
||||
@@ -1,23 +0,0 @@
|
||||
{
|
||||
"name": "shiftmedia-libgnutls",
|
||||
"version": "3.8.13",
|
||||
"port-version": 0,
|
||||
"description": "GnuTLS fork maintained by Savoir-faire Linux with native Visual Studio project build tools.",
|
||||
"homepage": "https://github.com/savoirfairelinux/gnutls",
|
||||
"license": "LGPL-2.1-only",
|
||||
"supports": "windows & !arm & !mingw & !xbox",
|
||||
"dependencies": [
|
||||
"gmp",
|
||||
"libtasn1",
|
||||
"nettle",
|
||||
{
|
||||
"name": "vs-yasm",
|
||||
"host": true
|
||||
},
|
||||
{
|
||||
"name": "yasm-tool-helper",
|
||||
"host": true
|
||||
},
|
||||
"zlib"
|
||||
]
|
||||
}
|
||||
+34
-5
@@ -1294,16 +1294,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);
|
||||
@@ -1659,8 +1657,24 @@ OcspResponse::toString(const bool compact) const
|
||||
gnutls_ocsp_cert_status_t
|
||||
OcspResponse::getCertificateStatus() const
|
||||
{
|
||||
int ret;
|
||||
unsigned int status;
|
||||
int ret = gnutls_ocsp_resp_get_single(response, 0, NULL, NULL, NULL, NULL, &status, NULL, NULL, NULL, NULL);
|
||||
// MSVC/vcpkg GnuTLS cast workaround
|
||||
#if _WIN32 && (GNUTLS_VERSION_NUMBER == 0x030807)
|
||||
ret = gnutls_ocsp_resp_get_single(response,
|
||||
0,
|
||||
NULL,
|
||||
NULL,
|
||||
NULL,
|
||||
NULL,
|
||||
reinterpret_cast<gnutls_ocsp_cert_status_t*>(&status),
|
||||
NULL,
|
||||
NULL,
|
||||
NULL,
|
||||
NULL);
|
||||
#else
|
||||
ret = gnutls_ocsp_resp_get_single(response, 0, NULL, NULL, NULL, NULL, &status, NULL, NULL, NULL, NULL);
|
||||
#endif
|
||||
if (ret < 0)
|
||||
throw CryptoException(gnutls_strerror(ret));
|
||||
return (gnutls_ocsp_cert_status_t) status;
|
||||
@@ -1719,7 +1733,22 @@ OcspResponse::verifyDirect(const Certificate& crt, const Blob& nonce)
|
||||
|
||||
// Check certificate revocation status
|
||||
unsigned int status_ocsp;
|
||||
// MSVC/vcpkg GnuTLS cast workaround
|
||||
#if _WIN32 && (GNUTLS_VERSION_NUMBER == 0x030807)
|
||||
ret = gnutls_ocsp_resp_get_single(response,
|
||||
0,
|
||||
NULL,
|
||||
NULL,
|
||||
NULL,
|
||||
NULL,
|
||||
reinterpret_cast<gnutls_ocsp_cert_status_t*>(&status_ocsp),
|
||||
NULL,
|
||||
NULL,
|
||||
NULL,
|
||||
NULL);
|
||||
#else
|
||||
ret = gnutls_ocsp_resp_get_single(response, 0, NULL, NULL, NULL, NULL, &status_ocsp, NULL, NULL, NULL, NULL);
|
||||
#endif
|
||||
if (ret < 0)
|
||||
throw CryptoException(gnutls_strerror(ret));
|
||||
return (gnutls_ocsp_cert_status_t) status_ocsp;
|
||||
|
||||
+45
-203
@@ -83,7 +83,6 @@ Dht::shutdown(ShutdownCallback cb, bool stop)
|
||||
cb(false, {});
|
||||
}
|
||||
sr.second->announce.clear();
|
||||
sr.second->cache.onReady(false);
|
||||
sr.second->listeners.clear();
|
||||
}
|
||||
}
|
||||
@@ -649,30 +648,6 @@ Dht::searchSendAnnounceValue(const Sp<Search>& sr, unsigned syncLevel)
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
Dht::searchNodeListenDeadline(const Sp<Search>& search, SearchNode& source, const Sp<Query>& query)
|
||||
{
|
||||
auto found = source.listenStatus.find(query);
|
||||
if (found == source.listenStatus.end() || found->second.initialized || found->second.syncDeadline)
|
||||
return;
|
||||
std::weak_ptr<Search> pending = search;
|
||||
found->second.syncDeadline
|
||||
= scheduler.add(scheduler.time() + std::chrono::seconds(30),
|
||||
[this, pending, query, node = source.node, registration = found->second.registration] {
|
||||
if (auto search = pending.lock()) {
|
||||
if (auto source = search->getNode(node)) {
|
||||
auto status = source->listenStatus.find(query);
|
||||
if (status != source->listenStatus.end()
|
||||
&& status->second.registration == registration && !status->second.initialized) {
|
||||
search->cache.onReady(query, false);
|
||||
source->listenStatus.erase(query);
|
||||
scheduler.edit(search->nextSearchStep, scheduler.time());
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void
|
||||
Dht::searchSynchedNodeListen(const Sp<Search>& sr, SearchNode& n)
|
||||
{
|
||||
@@ -689,11 +664,7 @@ Dht::searchSynchedNodeListen(const Sp<Search>& sr, SearchNode& n)
|
||||
if (auto sn = sr->getNode(node)) {
|
||||
if (not answer.ntoken.empty())
|
||||
sn->refreshToken(answer.ntoken);
|
||||
if (!sn->onValues(query, std::move(answer), types, scheduler)) {
|
||||
sr->cache.onReady(query, false);
|
||||
sn->listenStatus.erase(query);
|
||||
} else
|
||||
searchNodeListenDeadline(sr, *sn, query);
|
||||
sn->onValues(query, std::move(answer), types, scheduler);
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -706,10 +677,6 @@ Dht::searchSynchedNodeListen(const Sp<Search>& sr, SearchNode& n)
|
||||
// logger_->d(sr->id, n.node->id, "[search %s] [node %s] Sending 'listen'",
|
||||
// sr->id.toString().c_str(), n.node->toString().c_str());
|
||||
|
||||
if (r != n.listenStatus.end() && !n.node->getSocket(r->second.socketId)) {
|
||||
n.listenStatus.erase(r);
|
||||
r = n.listenStatus.end();
|
||||
}
|
||||
if (r == n.listenStatus.end()) {
|
||||
r = n.listenStatus
|
||||
.emplace(std::piecewise_construct,
|
||||
@@ -723,57 +690,36 @@ Dht::searchSynchedNodeListen(const Sp<Search>& sr, SearchNode& n)
|
||||
}
|
||||
}
|
||||
});
|
||||
} else if (not n.node->getSocket(r->second.socketId)) {
|
||||
r->second.socketId = openListenSocket();
|
||||
}
|
||||
if (!r->second.registration)
|
||||
r->second.registration = std::uniform_int_distribution<uint64_t>(1)(rd);
|
||||
searchNodeListenDeadline(sr, n, query);
|
||||
auto new_req = network_engine.sendListen(
|
||||
n.node,
|
||||
sr->id,
|
||||
*query,
|
||||
n.token,
|
||||
r->second.socketId,
|
||||
[this, ws, query, socketId = r->second.socketId](const net::Request& req,
|
||||
net::RequestAnswer&& answer) mutable {
|
||||
[this, ws, query](const net::Request& req, net::RequestAnswer&& answer) mutable {
|
||||
/* on done */
|
||||
if (auto sr = ws.lock()) {
|
||||
scheduler.edit(sr->nextSearchStep, scheduler.time());
|
||||
if (auto sn = sr->getNode(req.node)) {
|
||||
auto status = sn->listenStatus.find(query);
|
||||
if (status == sn->listenStatus.end() || status->second.socketId != socketId)
|
||||
return;
|
||||
scheduler.edit(sr->nextSearchStep, scheduler.time());
|
||||
onListenDone(req.node, answer, sr);
|
||||
auto job = scheduler.add(sn->getListenTime(query, getListenExpiration()),
|
||||
std::bind(&Dht::searchStep, this, ws));
|
||||
sn->onListenSynced(query, status->second.initialized, std::move(job));
|
||||
if ((answer.listenSync && answer.ntoken.empty()) || !answer.fields.empty()
|
||||
|| !answer.values.empty()
|
||||
|| !sn->confirmListen(query, answer.listenSync, types, scheduler)) {
|
||||
sr->cache.onReady(query, false);
|
||||
status = sn->listenStatus.find(query);
|
||||
if (status != sn->listenStatus.end())
|
||||
sn->listenStatus.erase(status);
|
||||
} else
|
||||
searchNodeListenDeadline(sr, *sn, query);
|
||||
sn->onListenSynced(query, true, std::move(job));
|
||||
}
|
||||
onListenDone(req.node, answer, sr);
|
||||
}
|
||||
},
|
||||
[this, ws, query, socketId = r->second.socketId](const net::Request& req, bool over) mutable {
|
||||
[this, ws, query](const net::Request& req, bool over) mutable {
|
||||
/* on request expired */
|
||||
if (auto sr = ws.lock()) {
|
||||
if (auto sn = sr->getNode(req.node)) {
|
||||
auto status = sn->listenStatus.find(query);
|
||||
if (status == sn->listenStatus.end() || status->second.socketId != socketId)
|
||||
return;
|
||||
scheduler.edit(sr->nextSearchStep, scheduler.time());
|
||||
if (over) {
|
||||
sr->cache.onReady(query, false);
|
||||
scheduler.edit(sr->nextSearchStep, scheduler.time());
|
||||
if (over)
|
||||
if (auto sn = sr->getNode(req.node))
|
||||
sn->listenStatus.erase(query);
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
net::ListenSync {r->second.registration, 0, 0, 0});
|
||||
});
|
||||
// Here the request may have failed and the CachedListenStatus removed
|
||||
r = n.listenStatus.find(query);
|
||||
if (r != n.listenStatus.end()) {
|
||||
@@ -805,9 +751,6 @@ Dht::searchStep(std::weak_ptr<Search> ws)
|
||||
if (sr->refill_time + Node::NODE_EXPIRE_TIME < now and sr->nodes.size() - sr->getNumberOfBadNodes() < SEARCH_NODES)
|
||||
refill(*sr);
|
||||
|
||||
if (sr->nodes.empty())
|
||||
sr->cache.onReady();
|
||||
|
||||
/* Check if the first TARGET_NODES (8) live nodes have replied. */
|
||||
if (sr->isSynced(now)) {
|
||||
if (not(sr->callbacks.empty() and sr->announce.empty())) {
|
||||
@@ -855,7 +798,6 @@ Dht::searchStep(std::weak_ptr<Search> ws)
|
||||
|
||||
// Announce requests
|
||||
searchSendAnnounceValue(sr);
|
||||
sr->cache.onSearchReady();
|
||||
|
||||
if (sr->callbacks.empty() && sr->announce.empty() && sr->listeners.empty())
|
||||
sr->setDone();
|
||||
@@ -1012,47 +954,30 @@ Dht::announce(
|
||||
}
|
||||
|
||||
size_t
|
||||
Dht::listenTo(
|
||||
const InfoHash& id, sa_family_t af, ValueCallback cb, Value::Filter f, const Sp<Query>& q, DoneCallbackSimple ready)
|
||||
Dht::listenTo(const InfoHash& id, sa_family_t af, ValueCallback cb, Value::Filter f, const Sp<Query>& q)
|
||||
{
|
||||
if (!isRunning(af)) {
|
||||
if (ready)
|
||||
ready(true);
|
||||
if (!isRunning(af))
|
||||
return 0;
|
||||
}
|
||||
// logger__ERR("[search %s IPv%c] Search_time is now in %lfs", sr->id.toString().c_str(), (sr->af == AF_INET) ? '4'
|
||||
// : '6', print_duration(tm-clock::now()));
|
||||
|
||||
// logger__WARN("listenTo %s", id.toString().c_str());
|
||||
auto& srs = searches(af);
|
||||
auto srp = srs.find(id);
|
||||
Sp<Search> sr = (srp == srs.end() || srp->second->expired) ? search(id, af) : srp->second;
|
||||
if (!sr) {
|
||||
if (ready)
|
||||
ready(false);
|
||||
return 0;
|
||||
}
|
||||
Sp<Search> sr = (srp == srs.end()) ? search(id, af) : srp->second;
|
||||
if (!sr)
|
||||
throw DhtException("Unable to create search");
|
||||
if (logger_)
|
||||
logger_->warn("[search {} IPv{}] Listen", id.to_view(), (af == AF_INET) ? '4' : '6');
|
||||
sr->done = false;
|
||||
scheduler.edit(sr->nextSearchStep, scheduler.time());
|
||||
return sr->listen(cb, std::move(f), q, scheduler, std::move(ready));
|
||||
return sr->listen(cb, std::move(f), q, scheduler);
|
||||
}
|
||||
|
||||
size_t
|
||||
Dht::listen(const InfoHash& id, ValueCallback cb, Value::Filter f, Where where)
|
||||
{
|
||||
return listen(id, std::move(cb), std::move(f), std::move(where), {});
|
||||
}
|
||||
|
||||
size_t
|
||||
Dht::listen(const InfoHash& id, ValueCallback cb, Value::Filter f, Where where, DoneCallbackSimple ready)
|
||||
{
|
||||
if (not id) {
|
||||
if (logger_)
|
||||
logger_->warn("Listen called with invalid key");
|
||||
if (ready)
|
||||
ready(false);
|
||||
return 0;
|
||||
}
|
||||
scheduler.syncTime();
|
||||
@@ -1069,60 +994,18 @@ Dht::listen(const InfoHash& id, ValueCallback cb, Value::Filter f, Where where,
|
||||
size_t tokenlocal = 0;
|
||||
if (st != store.end()) {
|
||||
tokenlocal = st->second.listen(gcb, filter, query);
|
||||
if (tokenlocal == 0) {
|
||||
if (ready)
|
||||
ready(false);
|
||||
if (tokenlocal == 0)
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
struct InitialReadiness
|
||||
{
|
||||
DoneCallbackSimple callback;
|
||||
unsigned remaining {2};
|
||||
bool registered {false};
|
||||
bool failed {false};
|
||||
void notify(bool ok)
|
||||
{
|
||||
failed = failed || !ok;
|
||||
if (remaining)
|
||||
--remaining;
|
||||
finish();
|
||||
}
|
||||
void finish()
|
||||
{
|
||||
if (registered && (failed || remaining == 0) && callback) {
|
||||
auto ready = std::move(callback);
|
||||
ready(!failed);
|
||||
}
|
||||
}
|
||||
};
|
||||
auto initial = std::make_shared<InitialReadiness>();
|
||||
initial->callback = std::move(ready);
|
||||
auto onReady = [initial](bool ok) {
|
||||
initial->notify(ok);
|
||||
};
|
||||
auto token4 = Dht::listenTo(id, AF_INET, gcb, filter, query, onReady);
|
||||
auto token6 = token4 == 0 && isRunning(AF_INET) ? 0 : Dht::listenTo(id, AF_INET6, gcb, filter, query, onReady);
|
||||
initial->registered = true;
|
||||
if ((isRunning(AF_INET) && token4 == 0) || (isRunning(AF_INET6) && token6 == 0)) {
|
||||
if (st != store.end())
|
||||
st->second.cancelListen(tokenlocal);
|
||||
for (auto family : {AF_INET, AF_INET6}) {
|
||||
auto listenerToken = family == AF_INET ? token4 : token6;
|
||||
if (!listenerToken)
|
||||
continue;
|
||||
auto& familySearches = searches(family);
|
||||
auto search = familySearches.find(id);
|
||||
if (search != familySearches.end())
|
||||
search->second->cancelListen(listenerToken, scheduler);
|
||||
}
|
||||
initial->notify(false);
|
||||
auto token4 = Dht::listenTo(id, AF_INET, gcb, filter, query);
|
||||
auto token6 = token4 == 0 ? 0 : Dht::listenTo(id, AF_INET6, gcb, filter, query);
|
||||
if (token6 == 0 && st != store.end()) {
|
||||
st->second.cancelListen(tokenlocal);
|
||||
return 0;
|
||||
}
|
||||
|
||||
listeners.emplace(token, std::make_tuple(tokenlocal, token4, token6));
|
||||
initial->finish();
|
||||
return token;
|
||||
}
|
||||
|
||||
@@ -1531,8 +1414,7 @@ Dht::storageChanged(const InfoHash& id, Storage& st, const Sp<Value>& v, bool ne
|
||||
{},
|
||||
std::move(vals),
|
||||
l.second.query,
|
||||
l.second.version,
|
||||
l.second.stream ? &l.second.stream->sync : nullptr);
|
||||
l.second.version);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1605,56 +1487,35 @@ Dht::storageStore(const InfoHash& id,
|
||||
return std::get<0>(store);
|
||||
}
|
||||
|
||||
std::optional<net::ListenSync>
|
||||
Dht::storageAddListener(const InfoHash& id,
|
||||
const Sp<Node>& node,
|
||||
size_t socket_id,
|
||||
Query&& query,
|
||||
int version,
|
||||
const std::optional<net::ListenSync>& sync)
|
||||
void
|
||||
Dht::storageAddListener(const InfoHash& id, const Sp<Node>& node, size_t socket_id, Query&& query, int version)
|
||||
{
|
||||
const auto& now = scheduler.time();
|
||||
auto st = store.find(id);
|
||||
if (st == store.end()) {
|
||||
if (store.size() >= max_store_keys)
|
||||
throw net::DhtProtocolException(net::DhtProtocolException::NON_AUTHORITATIVE_INFORMATION);
|
||||
return;
|
||||
st = store.emplace(id, now).first;
|
||||
}
|
||||
auto& node_listeners = st->second.listeners[node];
|
||||
auto l = node_listeners.find(socket_id);
|
||||
if (l != node_listeners.end() && sync
|
||||
&& (!l->second.stream || l->second.stream->sync.registration != sync->registration)) {
|
||||
node_listeners.erase(l);
|
||||
l = node_listeners.end();
|
||||
}
|
||||
if (l == node_listeners.end()) {
|
||||
l = node_listeners.emplace(socket_id, Listener {now, std::move(query), version}).first;
|
||||
auto vals = st->second.get(l->second.query.where.getFilter());
|
||||
if (version == 2 && sync) {
|
||||
l->second.stream = std::make_shared<Listener::Stream>();
|
||||
l->second.stream->sync = {sync->registration,
|
||||
std::uniform_int_distribution<uint64_t>(1)(rd),
|
||||
0,
|
||||
network_engine.getUpdateBatchCount(vals)};
|
||||
}
|
||||
auto vals = st->second.get(query.where.getFilter());
|
||||
if (not vals.empty()) {
|
||||
auto requests = network_engine.tellListener(node,
|
||||
socket_id,
|
||||
id,
|
||||
WANT4 | WANT6,
|
||||
makeToken(node->getAddr(), false),
|
||||
dht4.buckets.findClosestNodes(id, now, TARGET_NODES),
|
||||
dht6.buckets.findClosestNodes(id, now, TARGET_NODES),
|
||||
std::move(vals),
|
||||
l->second.query,
|
||||
version,
|
||||
l->second.stream ? &l->second.stream->sync : nullptr);
|
||||
if (l->second.stream)
|
||||
l->second.stream->initialRequests = std::move(requests);
|
||||
network_engine.tellListener(node,
|
||||
socket_id,
|
||||
id,
|
||||
WANT4 | WANT6,
|
||||
makeToken(node->getAddr(), false),
|
||||
dht4.buckets.findClosestNodes(id, now, TARGET_NODES),
|
||||
dht6.buckets.findClosestNodes(id, now, TARGET_NODES),
|
||||
std::move(vals),
|
||||
query,
|
||||
version);
|
||||
}
|
||||
node_listeners.emplace(socket_id, Listener {now, std::forward<Query>(query), version});
|
||||
} else
|
||||
l->second.refresh(now, std::forward<Query>(query));
|
||||
return l->second.stream ? std::optional<net::ListenSync>(l->second.stream->sync) : std::nullopt;
|
||||
}
|
||||
|
||||
void
|
||||
@@ -1701,13 +1562,7 @@ Dht::storageRemoved(const InfoHash& id, Storage& st, const std::vector<Sp<Value>
|
||||
id.to_view(),
|
||||
node_listeners.first->toString());
|
||||
Blob ntoken = makeToken(node_listeners.first->getAddr(), false);
|
||||
network_engine.tellListenerExpired(node_listeners.first,
|
||||
l.first,
|
||||
id,
|
||||
ntoken,
|
||||
ids,
|
||||
l.second.version,
|
||||
l.second.stream ? &l.second.stream->sync : nullptr);
|
||||
network_engine.tellListenerExpired(node_listeners.first, l.first, id, ntoken, ids, l.second.version);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2160,7 +2015,7 @@ Dht::Dht(std::unique_ptr<net::DatagramSocket>&& sock,
|
||||
std::bind(&Dht::onPing, this, _1),
|
||||
std::bind(&Dht::onFindNode, this, _1, _2, _3),
|
||||
std::bind(&Dht::onGetValues, this, _1, _2, _3, _4),
|
||||
std::bind(&Dht::onListen, this, _1, _2, _3, _4, _5, _6, _7),
|
||||
std::bind(&Dht::onListen, this, _1, _2, _3, _4, _5, _6),
|
||||
std::bind(&Dht::onAnnounce, this, _1, _2, _3, _4, _5),
|
||||
std::bind(&Dht::onRefresh, this, _1, _2, _3, _4))
|
||||
, persistPath(config.persist_path)
|
||||
@@ -2859,13 +2714,7 @@ Dht::onGetValuesDone(const Sp<Node>& node, net::RequestAnswer& a, Sp<Search>& sr
|
||||
}
|
||||
|
||||
net::RequestAnswer
|
||||
Dht::onListen(Sp<Node> node,
|
||||
const InfoHash& hash,
|
||||
const Blob& token,
|
||||
size_t socket_id,
|
||||
const Query& query,
|
||||
int version,
|
||||
const std::optional<net::ListenSync>& sync)
|
||||
Dht::onListen(Sp<Node> node, const InfoHash& hash, const Blob& token, size_t socket_id, const Query& query, int version)
|
||||
{
|
||||
if (not hash) {
|
||||
if (logger_)
|
||||
@@ -2879,11 +2728,9 @@ Dht::onListen(Sp<Node> node,
|
||||
throw net::DhtProtocolException {net::DhtProtocolException::UNAUTHORIZED,
|
||||
net::DhtProtocolException::LISTEN_WRONG_TOKEN};
|
||||
}
|
||||
if (version == 2 && (!sync || sync->epoch || sync->sequence || sync->snapshotEnd))
|
||||
throw net::DhtProtocolException(net::DhtProtocolException::NON_AUTHORITATIVE_INFORMATION);
|
||||
Query q = query;
|
||||
storageAddListener(hash, node, socket_id, std::move(q), version);
|
||||
net::RequestAnswer answer {};
|
||||
answer.listenSync = storageAddListener(hash, node, socket_id, std::move(q), version, sync);
|
||||
answer.ntoken = makeToken(node->getAddr(), false);
|
||||
return answer;
|
||||
}
|
||||
@@ -3025,13 +2872,8 @@ Dht::storageRefresh(const InfoHash& id, Value::Id vid)
|
||||
id.toString(),
|
||||
node_listeners.first->toString());
|
||||
Blob ntoken = makeToken(node_listeners.first->getAddr(), false);
|
||||
network_engine.tellListenerRefreshed(node_listeners.first,
|
||||
l.first,
|
||||
id,
|
||||
ntoken,
|
||||
ids,
|
||||
l.second.version,
|
||||
l.second.stream ? &l.second.stream->sync : nullptr);
|
||||
network_engine
|
||||
.tellListenerRefreshed(node_listeners.first, l.first, id, ntoken, ids, l.second.version);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+153
-513
@@ -2,7 +2,6 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
#include "dht_proxy_client.h"
|
||||
#include <charconv>
|
||||
#include "dhtrunner.h"
|
||||
#include "op_cache.h"
|
||||
#include "utils.h"
|
||||
@@ -22,64 +21,41 @@ struct DhtProxyClient::OperationState
|
||||
{
|
||||
std::atomic_bool ok {true};
|
||||
std::atomic_bool stop {false};
|
||||
std::atomic_uint64_t generation {0};
|
||||
std::atomic_bool inFlight {false};
|
||||
};
|
||||
|
||||
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;
|
||||
CacheValueCallback cb;
|
||||
Sp<OperationState> opstate;
|
||||
std::shared_ptr<http::Request> request;
|
||||
std::shared_ptr<asio::steady_timer> requestDeadline;
|
||||
bool needsRecovery {true};
|
||||
std::string syncEpoch;
|
||||
uint64_t syncRevision {0};
|
||||
std::string observedEpoch;
|
||||
uint64_t observedRevision {0};
|
||||
std::set<std::string> retiredEpochs;
|
||||
std::string requestToken;
|
||||
uint64_t pushGeneration {0};
|
||||
std::chrono::seconds retryDelay {1};
|
||||
DoneCallbackSimple ready;
|
||||
#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
|
||||
{
|
||||
struct Readiness
|
||||
{
|
||||
std::atomic_bool pending {true};
|
||||
DoneCallbackSimple callback;
|
||||
size_t token {0};
|
||||
};
|
||||
|
||||
SearchCache ops {false};
|
||||
SearchCache ops {};
|
||||
std::unique_ptr<asio::steady_timer> opExpirationTimer;
|
||||
std::map<size_t, Listener> listeners {};
|
||||
std::map<size_t, std::shared_ptr<Readiness>> readiness;
|
||||
std::map<Value::Id, PermanentPut> puts {};
|
||||
std::set<Sp<Value>> pendingPuts {};
|
||||
};
|
||||
@@ -101,7 +77,6 @@ struct LineSplit
|
||||
return false;
|
||||
}
|
||||
const std::string& line() const { return line_; }
|
||||
bool empty() const { return buf_.empty(); }
|
||||
|
||||
private:
|
||||
std::deque<char> buf_ {};
|
||||
@@ -225,17 +200,18 @@ DhtProxyClient::stop()
|
||||
}
|
||||
if (resolver)
|
||||
resolver->cancel();
|
||||
cancelAllListeners();
|
||||
if (infoState_)
|
||||
infoState_->cancel = true;
|
||||
{
|
||||
std::lock_guard lock(requestLock_);
|
||||
for (auto& request : requests_)
|
||||
request.second->cancel();
|
||||
}
|
||||
if (not httpContext_.stopped())
|
||||
httpContext_.stop();
|
||||
if (httpClientThread_.joinable())
|
||||
httpClientThread_.join();
|
||||
cancelAllListeners();
|
||||
for (auto& request : requests_) {
|
||||
request.second->cancel();
|
||||
request.second->terminate(asio::error::operation_aborted);
|
||||
}
|
||||
requests_.clear();
|
||||
}
|
||||
}
|
||||
@@ -263,24 +239,19 @@ DhtProxyClient::getLocalById(const InfoHash& k, Value::Id id) const
|
||||
void
|
||||
DhtProxyClient::cancelAllListeners()
|
||||
{
|
||||
std::map<InfoHash, ProxySearch> canceled;
|
||||
{
|
||||
std::lock_guard lock(searchLock_);
|
||||
canceled.swap(searches_);
|
||||
}
|
||||
std::lock_guard lock(searchLock_);
|
||||
if (logger_)
|
||||
logger_->debug("[proxy:client] [listeners] [{} searches] cancel all", canceled.size());
|
||||
for (auto& s : canceled) {
|
||||
for (auto& listener : s.second.listeners) {
|
||||
listener.second.opstate->stop.store(true);
|
||||
if (listener.second.request)
|
||||
listener.second.request->cancel();
|
||||
}
|
||||
s.second.ops.onReady(false);
|
||||
s.second.ops.cancelAll([&](size_t token) { s.second.listeners.erase(token); });
|
||||
for (const auto& readiness : s.second.readiness)
|
||||
if (readiness.second->pending.exchange(false))
|
||||
readiness.second->callback(false);
|
||||
logger_->debug("[proxy:client] [listeners] [{} searches] cancel all", searches_.size());
|
||||
for (auto& s : searches_) {
|
||||
s.second.ops.cancelAll([&](size_t token) {
|
||||
auto l = s.second.listeners.find(token);
|
||||
if (l == s.second.listeners.end())
|
||||
return;
|
||||
l->second.opstate->stop.store(true);
|
||||
l->second.request->cancel();
|
||||
// implicit request.reset()
|
||||
s.second.listeners.erase(token);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -332,12 +303,6 @@ DhtProxyClient::periodic(const uint8_t*, size_t, SockAddr, const time_point& /*n
|
||||
for (auto& callback : callbacks)
|
||||
callback();
|
||||
callbacks.clear();
|
||||
{
|
||||
std::lock_guard lock(lockCallbacks_);
|
||||
callbacks = std::move(readinessCallbacks_);
|
||||
}
|
||||
for (auto& callback : callbacks)
|
||||
callback();
|
||||
return time_point::max();
|
||||
}
|
||||
|
||||
@@ -459,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));
|
||||
@@ -575,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(
|
||||
@@ -866,52 +831,17 @@ DhtProxyClient::getPublicAddress(sa_family_t family)
|
||||
|
||||
size_t
|
||||
DhtProxyClient::listen(const InfoHash& key, ValueCallback cb, Value::Filter filter, Where where)
|
||||
{
|
||||
return listen(key, std::move(cb), std::move(filter), std::move(where), {});
|
||||
}
|
||||
|
||||
size_t
|
||||
DhtProxyClient::listen(const InfoHash& key, ValueCallback cb, Value::Filter filter, Where where, DoneCallbackSimple ready)
|
||||
{
|
||||
if (logger_)
|
||||
logger_->debug("[proxy:client] [listen] [search {}]", key.to_view());
|
||||
if (isDestroying_) {
|
||||
if (ready)
|
||||
ready(false);
|
||||
if (isDestroying_)
|
||||
return 0;
|
||||
}
|
||||
|
||||
std::lock_guard lock(searchLock_);
|
||||
auto& search = searches_[key];
|
||||
auto query = std::make_shared<Query>(Select {}, std::move(where));
|
||||
std::shared_ptr<ProxySearch::Readiness> readiness;
|
||||
DoneCallbackSimple notify;
|
||||
if (ready) {
|
||||
readiness = std::make_shared<ProxySearch::Readiness>();
|
||||
readiness->callback = std::move(ready);
|
||||
notify = [this, key, readiness](bool ok) {
|
||||
{
|
||||
std::lock_guard callbackLock(lockCallbacks_);
|
||||
readinessCallbacks_.emplace_back([this, key, readiness, ok] {
|
||||
{
|
||||
std::lock_guard searchLock(searchLock_);
|
||||
if (!readiness->pending.exchange(false))
|
||||
return;
|
||||
auto search = searches_.find(key);
|
||||
if (search != searches_.end())
|
||||
search->second.readiness.erase(readiness->token);
|
||||
}
|
||||
readiness->callback(ok);
|
||||
});
|
||||
}
|
||||
loopSignal_();
|
||||
};
|
||||
}
|
||||
auto token = search.ops.listen(
|
||||
cb,
|
||||
query,
|
||||
std::move(filter),
|
||||
[this, key](Sp<Query> query, ValueCallback cb, SyncCallback synced) -> size_t {
|
||||
return search.ops
|
||||
.listen(cb, query, std::move(filter), [this, key](Sp<Query>, ValueCallback cb, SyncCallback) -> size_t {
|
||||
// Find search
|
||||
auto search = searches_.find(key);
|
||||
if (search == searches_.end()) {
|
||||
@@ -939,16 +869,6 @@ DhtProxyClient::listen(const InfoHash& key, ValueCallback cb, Value::Filter filt
|
||||
// Add cache callback
|
||||
auto opstate = std::make_shared<OperationState>();
|
||||
l->second.opstate = opstate;
|
||||
synced(ListenSyncStatus::ADDED);
|
||||
l->second.ready = [this, key, query, synced, synchronized = std::make_shared<bool>(false)](bool ok) {
|
||||
if (*synchronized != ok) {
|
||||
*synchronized = ok;
|
||||
synced(ok ? ListenSyncStatus::SYNCED : ListenSyncStatus::UNSYNCED);
|
||||
}
|
||||
auto search = searches_.find(key);
|
||||
if (search != searches_.end())
|
||||
search->second.ops.onReady(query, ok);
|
||||
};
|
||||
l->second.cb = [this, key, token, opstate](const std::vector<Sp<Value>>& values,
|
||||
bool expired,
|
||||
system_clock::time_point t) {
|
||||
@@ -971,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));
|
||||
@@ -989,17 +909,9 @@ DhtProxyClient::listen(const InfoHash& key, ValueCallback cb, Value::Filter filt
|
||||
header.method(restinio::http_method_subscribe());
|
||||
header.request_target(fmt::format("/key/{}", key.to_view()));
|
||||
}
|
||||
sendListen(header, l->second.cb, opstate, l->second, method, key, token);
|
||||
sendListen(header, l->second.cb, opstate, l->second, method);
|
||||
return token;
|
||||
},
|
||||
notify);
|
||||
if (readiness) {
|
||||
readiness->token = token;
|
||||
search.readiness.emplace(token, readiness);
|
||||
if (!token)
|
||||
notify(false);
|
||||
}
|
||||
return token;
|
||||
});
|
||||
}
|
||||
|
||||
void
|
||||
@@ -1043,11 +955,6 @@ DhtProxyClient::cancelListen(const InfoHash& key, size_t gtoken)
|
||||
return false;
|
||||
auto& ops = it->second.ops;
|
||||
bool canceled = ops.cancelListen(gtoken, std::chrono::steady_clock::now());
|
||||
auto readiness = it->second.readiness.find(gtoken);
|
||||
if (readiness != it->second.readiness.end()) {
|
||||
readiness->second->pending = false;
|
||||
it->second.readiness.erase(readiness);
|
||||
}
|
||||
|
||||
// define real cancel listen only once
|
||||
if (not it->second.opExpirationTimer)
|
||||
@@ -1144,313 +1051,77 @@ DhtProxyClient::handleExpireListener(const asio::error_code& ec, const InfoHash&
|
||||
}
|
||||
}
|
||||
|
||||
bool
|
||||
DhtProxyClient::reconcileListenerSnapshot(Listener& listener,
|
||||
const std::vector<Sp<Value>>& values,
|
||||
const Json::Value& marker)
|
||||
{
|
||||
if (!marker.isObject() || !marker["epoch"].isString() || marker["epoch"].asString().empty()
|
||||
|| !marker["revision"].isUInt64() || !marker["snapshot"].isBool() || !marker["complete"].isBool()
|
||||
|| !marker["complete"].asBool())
|
||||
return false;
|
||||
auto epoch = marker["epoch"].asString();
|
||||
auto revision = marker["revision"].asUInt64();
|
||||
if (listener.retiredEpochs.count(epoch) || (listener.syncEpoch == epoch && listener.syncRevision > revision)
|
||||
|| (listener.observedEpoch == epoch && listener.observedRevision > revision))
|
||||
return false;
|
||||
bool full = marker["snapshot"].asBool();
|
||||
if (!full
|
||||
&& (listener.needsRecovery || epoch != listener.syncEpoch || revision != listener.syncRevision
|
||||
|| !values.empty()))
|
||||
return false;
|
||||
if (full) {
|
||||
std::set<Value::Id> ids;
|
||||
for (const auto& value : values)
|
||||
ids.insert(value->id);
|
||||
std::vector<Sp<Value>> expired;
|
||||
for (const auto& value : listener.cache.getValues())
|
||||
if (!ids.count(value->id))
|
||||
expired.emplace_back(value);
|
||||
if (!listener.cache.onValue(values, false, system_clock::time_point::min())
|
||||
|| (!expired.empty() && !listener.cache.onValue(expired, true, system_clock::time_point::max()))) {
|
||||
listener.opstate->stop = true;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
if (!listener.syncEpoch.empty() && listener.syncEpoch != epoch)
|
||||
listener.retiredEpochs.insert(listener.syncEpoch);
|
||||
while (listener.retiredEpochs.size() > 8)
|
||||
listener.retiredEpochs.erase(listener.retiredEpochs.begin());
|
||||
listener.syncEpoch = epoch;
|
||||
listener.syncRevision = revision;
|
||||
listener.observedEpoch = epoch;
|
||||
listener.observedRevision = revision;
|
||||
listener.needsRecovery = false;
|
||||
listener.retryDelay = std::chrono::seconds(1);
|
||||
return true;
|
||||
}
|
||||
|
||||
void
|
||||
DhtProxyClient::scheduleListenRetry([[maybe_unused]] const InfoHash& key,
|
||||
[[maybe_unused]] size_t token,
|
||||
[[maybe_unused]] Listener& listener)
|
||||
{
|
||||
#ifdef OPENDHT_PUSH_NOTIFICATIONS
|
||||
if (!listener.refreshSubscriberTimer)
|
||||
listener.refreshSubscriberTimer = std::make_unique<asio::system_timer>(httpContext_);
|
||||
listener.refreshSubscriberTimer->expires_after(listener.retryDelay);
|
||||
listener.retryDelay = std::min(listener.retryDelay * 2, std::chrono::seconds(60));
|
||||
listener.refreshSubscriberTimer->async_wait(
|
||||
std::bind(&DhtProxyClient::handleResubscribe, this, std::placeholders::_1, key, token, listener.opstate));
|
||||
#endif
|
||||
}
|
||||
|
||||
void
|
||||
DhtProxyClient::sendListen(const restinio::http_request_header_t& header,
|
||||
const CacheValueCallback& cb,
|
||||
const Sp<OperationState>& opstate,
|
||||
Listener& listener,
|
||||
ListenMethod method,
|
||||
const InfoHash& key,
|
||||
size_t token)
|
||||
ListenMethod method)
|
||||
{
|
||||
if (logger_)
|
||||
logger_->error("[proxy:client] [listen] sendListen: {}", (int) method);
|
||||
try {
|
||||
auto generation = ++opstate->generation;
|
||||
bool syncRequest = method != ListenMethod::LISTEN;
|
||||
opstate->inFlight = syncRequest;
|
||||
if (syncRequest)
|
||||
opstate->ok = false;
|
||||
auto request = buildRequest();
|
||||
listener.request = request;
|
||||
auto deadline = std::make_shared<asio::steady_timer>(httpContext_);
|
||||
listener.requestDeadline = deadline;
|
||||
if (syncRequest) {
|
||||
deadline->expires_after(subscriptionRequestTimeout_);
|
||||
deadline->async_wait([weak = std::weak_ptr<http::Request>(request)](const asio::error_code& ec) {
|
||||
if (!ec)
|
||||
if (auto request = weak.lock()) {
|
||||
request->cancel();
|
||||
request->terminate(asio::error::timed_out);
|
||||
}
|
||||
});
|
||||
}
|
||||
auto reqid = request->id();
|
||||
auto requestHeader = header;
|
||||
if (method == ListenMethod::LISTEN)
|
||||
requestHeader.request_target(std::string(header.request_target()) + "?sync=1");
|
||||
request->set_header(requestHeader);
|
||||
request->set_header(header);
|
||||
setHeaderFields(*request);
|
||||
if (method == ListenMethod::LISTEN) {
|
||||
if (method == ListenMethod::LISTEN)
|
||||
request->set_connection_type(restinio::http_connection_header_t::keep_alive);
|
||||
}
|
||||
#ifdef OPENDHT_PUSH_NOTIFICATIONS
|
||||
std::string body;
|
||||
std::string requestToken;
|
||||
if (syncRequest) {
|
||||
Json::Value root;
|
||||
getPushRequest(root);
|
||||
requestToken = root["key"].asString();
|
||||
if (listener.requestToken != requestToken) {
|
||||
listener.requestToken = requestToken;
|
||||
listener.observedEpoch.clear();
|
||||
listener.observedRevision = 0;
|
||||
listener.retiredEpochs.clear();
|
||||
}
|
||||
root["sync"] = Json::Value(Json::objectValue);
|
||||
if (!listener.needsRecovery && !listener.syncEpoch.empty()) {
|
||||
root["sync"]["epoch"] = listener.syncEpoch;
|
||||
root["sync"]["revision"] = Json::UInt64(listener.syncRevision);
|
||||
}
|
||||
body = Json::writeString(jsonBuilder_, root);
|
||||
}
|
||||
if (method != ListenMethod::LISTEN)
|
||||
body = fillBody(method == ListenMethod::RESUBSCRIBE);
|
||||
request->set_body(body);
|
||||
#else
|
||||
std::string requestToken;
|
||||
#endif
|
||||
auto pushGeneration = listener.pushGeneration;
|
||||
auto rxBuf = std::make_shared<LineSplit>();
|
||||
auto snapshot = std::make_shared<std::vector<Sp<Value>>>();
|
||||
auto marker = std::make_shared<Json::Value>();
|
||||
auto valid = std::make_shared<bool>(true);
|
||||
request->add_on_body_callback(
|
||||
[this, reqid, key, token, opstate, generation, rxBuf, cb, syncRequest, snapshot, marker, valid](
|
||||
const char* at, size_t length) {
|
||||
if (opstate->generation != generation || opstate->stop)
|
||||
return;
|
||||
try {
|
||||
auto& b = *rxBuf;
|
||||
b.append(at, length);
|
||||
request->add_on_body_callback([this, reqid, opstate, rxBuf, cb](const char* at, size_t length) {
|
||||
try {
|
||||
auto& b = *rxBuf;
|
||||
b.append(at, length);
|
||||
|
||||
// one value per body line
|
||||
while (b.getLine('\n') and !opstate->stop) {
|
||||
std::string err;
|
||||
Json::Value json;
|
||||
const auto& line = b.line();
|
||||
if (!jsonReader_->parse(line.data(), line.data() + line.size(), &json, &err)) {
|
||||
*valid = false;
|
||||
return;
|
||||
}
|
||||
if (syncRequest) {
|
||||
if (!marker->isNull()) {
|
||||
*valid = false;
|
||||
continue;
|
||||
}
|
||||
if (json.isMember("sync")) {
|
||||
*marker = json["sync"];
|
||||
} else if (json.size() != 0) {
|
||||
snapshot->emplace_back(std::make_shared<Value>(json));
|
||||
} else {
|
||||
*valid = false;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (json.isMember("sync")) {
|
||||
if (!json["sync"].isObject() || !json["sync"]["complete"].isBool()) {
|
||||
*valid = false;
|
||||
return;
|
||||
}
|
||||
std::lock_guard callbackLock(lockCallbacks_);
|
||||
callbacks_.emplace_back(
|
||||
[this, key, token, opstate, generation, ok = json["sync"]["complete"].asBool()] {
|
||||
std::lock_guard searchLock(searchLock_);
|
||||
if (opstate->generation != generation || opstate->stop)
|
||||
return;
|
||||
auto search = searches_.find(key);
|
||||
if (search == searches_.end())
|
||||
return;
|
||||
auto found = search->second.listeners.find(token);
|
||||
if (found != search->second.listeners.end() && found->second.ready) {
|
||||
auto ready = found->second.ready;
|
||||
ready(ok);
|
||||
}
|
||||
});
|
||||
loopSignal_();
|
||||
continue;
|
||||
}
|
||||
if (json.size() == 0) { // it's the end
|
||||
break;
|
||||
}
|
||||
// one value per body line
|
||||
while (b.getLine('\n') and !opstate->stop) {
|
||||
std::string err;
|
||||
Json::Value json;
|
||||
const auto& line = b.line();
|
||||
if (!jsonReader_->parse(line.data(), line.data() + line.size(), &json, &err)) {
|
||||
opstate->ok.store(false);
|
||||
return;
|
||||
}
|
||||
if (json.size() == 0) { // it's the end
|
||||
break;
|
||||
}
|
||||
|
||||
auto value = std::make_shared<Value>(json);
|
||||
if (cb) {
|
||||
auto expired = json.get("expired", Json::Value(false)).asBool();
|
||||
{
|
||||
std::lock_guard lock(lockCallbacks_);
|
||||
callbacks_.emplace_back([cb, value, opstate, generation, expired]() {
|
||||
if (opstate->generation == generation && not opstate->stop.load()
|
||||
and not cb({value}, expired, system_clock::time_point::min()))
|
||||
opstate->stop.store(true);
|
||||
});
|
||||
}
|
||||
loopSignal_();
|
||||
auto value = std::make_shared<Value>(json);
|
||||
if (cb) {
|
||||
auto expired = json.get("expired", Json::Value(false)).asBool();
|
||||
{
|
||||
std::lock_guard lock(lockCallbacks_);
|
||||
callbacks_.emplace_back([cb, value, opstate, expired]() {
|
||||
if (not opstate->stop.load()
|
||||
and not cb({value}, expired, system_clock::time_point::min()))
|
||||
opstate->stop.store(true);
|
||||
});
|
||||
}
|
||||
loopSignal_();
|
||||
}
|
||||
} catch (const std::exception& e) {
|
||||
if (logger_)
|
||||
logger_->error("[proxy:client] [listen] request #{} error in parsing: {}", reqid, e.what());
|
||||
*valid = false;
|
||||
}
|
||||
});
|
||||
request->add_on_done_callback([this,
|
||||
opstate,
|
||||
generation,
|
||||
reqid,
|
||||
key,
|
||||
token,
|
||||
syncRequest,
|
||||
rxBuf,
|
||||
snapshot,
|
||||
marker,
|
||||
valid,
|
||||
deadline,
|
||||
requestToken,
|
||||
pushGeneration](const http::Response& response) {
|
||||
deadline->cancel();
|
||||
bool current = opstate->generation == generation && !opstate->stop;
|
||||
bool successful = response.status_code == 200 && !response.aborted && *valid
|
||||
&& (!syncRequest || rxBuf->empty());
|
||||
if (current && !isDestroying_) {
|
||||
{
|
||||
std::lock_guard callbackLock(lockCallbacks_);
|
||||
callbacks_.emplace_back([this,
|
||||
key,
|
||||
token,
|
||||
opstate,
|
||||
generation,
|
||||
successful,
|
||||
syncRequest,
|
||||
transportFailed = !response.aborted && response.status_code == 0,
|
||||
snapshot,
|
||||
marker,
|
||||
requestToken,
|
||||
pushGeneration]() {
|
||||
std::lock_guard searchLock(searchLock_);
|
||||
if (opstate->generation != generation || opstate->stop)
|
||||
return;
|
||||
auto search = searches_.find(key);
|
||||
if (search == searches_.end())
|
||||
return;
|
||||
auto found = search->second.listeners.find(token);
|
||||
if (found == search->second.listeners.end() || found->second.opstate != opstate)
|
||||
return;
|
||||
auto& listener = found->second;
|
||||
if (!successful) {
|
||||
opstate->ok = false;
|
||||
if (transportFailed)
|
||||
opFailed();
|
||||
}
|
||||
opstate->inFlight = false;
|
||||
if (!syncRequest) {
|
||||
if (!successful && listener.ready) {
|
||||
auto ready = listener.ready;
|
||||
ready(false);
|
||||
}
|
||||
return;
|
||||
}
|
||||
auto observedEpoch = listener.observedEpoch;
|
||||
if (successful && reconcileListenerSnapshot(listener, *snapshot, *marker)) {
|
||||
bool validateEpoch = listener.pushGeneration != pushGeneration && !observedEpoch.empty()
|
||||
&& observedEpoch != listener.syncEpoch;
|
||||
opstate->ok = true;
|
||||
#ifdef OPENDHT_PUSH_NOTIFICATIONS
|
||||
if (listener.refreshSubscriberTimer) {
|
||||
listener.refreshSubscriberTimer->expires_at(system_clock::now() + proxy::OP_TIMEOUT
|
||||
- proxy::OP_MARGIN);
|
||||
listener.refreshSubscriberTimer->async_wait(std::bind(&DhtProxyClient::handleResubscribe,
|
||||
this,
|
||||
std::placeholders::_1,
|
||||
key,
|
||||
token,
|
||||
opstate));
|
||||
}
|
||||
#endif
|
||||
if (listener.ready) {
|
||||
auto ready = listener.ready;
|
||||
ready(true);
|
||||
}
|
||||
bool changedToken;
|
||||
{
|
||||
std::lock_guard infoLock(lockCurrentProxyInfos_);
|
||||
changedToken = requestToken != deviceKey_;
|
||||
}
|
||||
if (changedToken || validateEpoch)
|
||||
resubscribe(key, token, listener);
|
||||
return;
|
||||
}
|
||||
if (opstate->stop)
|
||||
return;
|
||||
listener.needsRecovery = true;
|
||||
opstate->ok = false;
|
||||
if (listener.ready) {
|
||||
auto ready = listener.ready;
|
||||
ready(false);
|
||||
}
|
||||
scheduleListenRetry(key, token, listener);
|
||||
});
|
||||
}
|
||||
loopSignal_();
|
||||
} catch (const std::exception& e) {
|
||||
if (logger_)
|
||||
logger_->error("[proxy:client] [listen] request #{} error in parsing: {}", reqid, e.what());
|
||||
opstate->ok.store(false);
|
||||
}
|
||||
});
|
||||
request->add_on_done_callback([this, opstate, reqid](const http::Response& response) {
|
||||
if (response.status_code != 200) {
|
||||
if (logger_)
|
||||
logger_->error("[proxy:client] [listen] send request #{} failed with code={}",
|
||||
reqid,
|
||||
response.status_code);
|
||||
opstate->ok.store(false);
|
||||
if (not response.aborted and response.status_code == 0)
|
||||
opFailed();
|
||||
}
|
||||
if (not isDestroying_) {
|
||||
std::lock_guard l(requestLock_);
|
||||
@@ -1461,21 +1132,14 @@ DhtProxyClient::sendListen(const restinio::http_request_header_t& header,
|
||||
std::lock_guard l(requestLock_);
|
||||
requests_[reqid] = request;
|
||||
}
|
||||
request->add_on_status_callback(
|
||||
[weak = std::weak_ptr<http::Request>(request), seconds = this->listenKeepIdle()](unsigned status_code) {
|
||||
if (status_code == 200) {
|
||||
// increase TCP_KEEPIDLE to save power
|
||||
if (auto request = weak.lock())
|
||||
if (auto connection = request->get_connection())
|
||||
connection->set_keepalive(seconds);
|
||||
}
|
||||
});
|
||||
request->add_on_status_callback([request, seconds = this->listenKeepIdle()](unsigned status_code) {
|
||||
if (status_code == 200) {
|
||||
// increase TCP_KEEPIDLE to save power
|
||||
request->get_connection()->set_keepalive(seconds);
|
||||
}
|
||||
});
|
||||
request->send();
|
||||
} catch (const std::exception& e) {
|
||||
opstate->ok = false;
|
||||
opstate->inFlight = false;
|
||||
listener.needsRecovery = true;
|
||||
scheduleListenRetry(key, token, listener);
|
||||
if (logger_)
|
||||
logger_->error("[proxy:client] [listen] request failed: {}", e.what());
|
||||
}
|
||||
@@ -1528,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));
|
||||
@@ -1546,7 +1210,8 @@ DhtProxyClient::restartListeners(const asio::error_code& ec)
|
||||
// Connectivity changed, refresh all subscribe
|
||||
for (auto& search : searches_)
|
||||
for (auto& listener : search.second.listeners)
|
||||
resubscribe(search.first, listener.first, listener.second);
|
||||
if (!listener.second.opstate->ok)
|
||||
resubscribe(search.first, listener.first, listener.second);
|
||||
return;
|
||||
}
|
||||
if (logger_)
|
||||
@@ -1556,8 +1221,7 @@ DhtProxyClient::restartListeners(const asio::error_code& ec)
|
||||
auto& listener = l.second;
|
||||
if (auto opstate = listener.opstate)
|
||||
opstate->stop = true;
|
||||
if (listener.request)
|
||||
listener.request->cancel();
|
||||
listener.request->cancel();
|
||||
listener.request.reset();
|
||||
}
|
||||
}
|
||||
@@ -1573,34 +1237,16 @@ DhtProxyClient::restartListeners(const asio::error_code& ec)
|
||||
restinio::http_request_header_t header;
|
||||
header.method(restinio::http_method_get());
|
||||
header.request_target("/key/" + search.first.toString() + "/listen");
|
||||
sendListen(header, cb, opstate, listener, ListenMethod::LISTEN, search.first, l.first);
|
||||
sendListen(header, cb, opstate, listener, ListenMethod::LISTEN);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#ifdef OPENDHT_PUSH_NOTIFICATIONS
|
||||
static uint64_t
|
||||
parsePushNumber(const std::string& text)
|
||||
{
|
||||
uint64_t number = 0;
|
||||
auto result = std::from_chars(text.data(), text.data() + text.size(), number);
|
||||
if (text.empty() || result.ec != std::errc {} || result.ptr != text.data() + text.size())
|
||||
throw std::invalid_argument("Invalid push number");
|
||||
return number;
|
||||
}
|
||||
#endif
|
||||
|
||||
PushNotificationResult
|
||||
DhtProxyClient::pushNotificationReceived([[maybe_unused]] const std::map<std::string, std::string>& notification)
|
||||
{
|
||||
auto ret = PushNotificationResult::IgnoredNoOp;
|
||||
#ifdef OPENDHT_PUSH_NOTIFICATIONS
|
||||
auto sessionId = notification.find("s");
|
||||
if (sessionId != notification.end() and sessionId->second != pushSessionId_) {
|
||||
if (logger_)
|
||||
logger_->debug("[proxy:client] [push] ignoring push for other session");
|
||||
return PushNotificationResult::IgnoredWrongSession;
|
||||
}
|
||||
{
|
||||
// If a push notification is received, the proxy is up and running
|
||||
std::lock_guard l(lockCurrentProxyInfos_);
|
||||
@@ -1612,6 +1258,12 @@ DhtProxyClient::pushNotificationReceived([[maybe_unused]] const std::map<std::st
|
||||
}
|
||||
auto launchLoop = false;
|
||||
try {
|
||||
auto sessionId = notification.find("s");
|
||||
if (sessionId != notification.end() and sessionId->second != pushSessionId_) {
|
||||
if (logger_)
|
||||
logger_->debug("[proxy:client] [push] ignoring push for other session");
|
||||
return PushNotificationResult::IgnoredWrongSession;
|
||||
}
|
||||
std::lock_guard lock(searchLock_);
|
||||
auto timeout = notification.find("timeout");
|
||||
if (timeout != notification.cend()) {
|
||||
@@ -1623,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;
|
||||
@@ -1639,24 +1291,13 @@ DhtProxyClient::pushNotificationReceived([[maybe_unused]] const std::map<std::st
|
||||
}
|
||||
} else {
|
||||
auto key = InfoHash(notification.at("key"));
|
||||
system_clock::time_point sendTime = system_clock::time_point::min();
|
||||
try {
|
||||
sendTime = system_clock::time_point(std::chrono::milliseconds(std::stoull(notification.at("t"))));
|
||||
} catch (...) {
|
||||
}
|
||||
auto& search = searches_.at(key);
|
||||
auto expired = notification.find("exp");
|
||||
auto expirationFlag = notification.find("expired");
|
||||
bool isExpired = expired != notification.end()
|
||||
|| (expirationFlag != notification.end() && expirationFlag->second == "true");
|
||||
auto epoch = notification.find("epoch");
|
||||
auto revision = notification.find("revision");
|
||||
if (epoch == notification.end() || revision == notification.end() || epoch->second.empty())
|
||||
throw std::invalid_argument("Incomplete push revision");
|
||||
auto number = parsePushNumber(revision->second);
|
||||
std::vector<Value::Id> ids;
|
||||
if (isExpired) {
|
||||
std::stringstream stream(expired != notification.end() ? expired->second : notification.at("ids"));
|
||||
for (std::string id; std::getline(stream, id, ',');)
|
||||
ids.emplace_back(parsePushNumber(id));
|
||||
if (ids.empty())
|
||||
throw std::invalid_argument("Empty expiration list");
|
||||
}
|
||||
for (auto& list : search.listeners) {
|
||||
if (list.second.opstate->stop)
|
||||
continue;
|
||||
@@ -1664,19 +1305,29 @@ DhtProxyClient::pushNotificationReceived([[maybe_unused]] const std::map<std::st
|
||||
logger_->debug("[proxy:client] [push] [search {}] received", key);
|
||||
auto token = list.first;
|
||||
auto opstate = list.second.opstate;
|
||||
auto& listener = list.second;
|
||||
if (listener.retiredEpochs.count(epoch->second)
|
||||
|| (epoch->second == listener.syncEpoch && number <= listener.syncRevision)
|
||||
|| (epoch->second == listener.observedEpoch && number <= listener.observedRevision))
|
||||
continue;
|
||||
++listener.pushGeneration;
|
||||
listener.observedEpoch = epoch->second;
|
||||
listener.observedRevision = number;
|
||||
if (isExpired && !listener.needsRecovery && epoch->second == listener.syncEpoch
|
||||
&& number > listener.syncRevision && number - listener.syncRevision == 1) {
|
||||
if (expired == notification.end()) {
|
||||
auto cb = list.second.cb;
|
||||
auto oldValues = list.second.cache.getValues();
|
||||
get(
|
||||
key,
|
||||
[cb, sendTime](const std::vector<Sp<Value>>& vals) { return cb(vals, false, sendTime); },
|
||||
[cb, oldValues, sendTime](bool /*ok*/) {
|
||||
// Decrement old values refcount to expire values not
|
||||
// present in the new list
|
||||
cb(oldValues, true, sendTime);
|
||||
});
|
||||
ret = PushNotificationResult::Values;
|
||||
} else {
|
||||
std::stringstream ss(expired->second);
|
||||
std::vector<Value::Id> ids;
|
||||
while (ss.good()) {
|
||||
std::string substr;
|
||||
getline(ss, substr, ',');
|
||||
ids.emplace_back(std::stoull(substr));
|
||||
}
|
||||
{
|
||||
std::lock_guard lockCb(lockCallbacks_);
|
||||
callbacks_.emplace_back([this, key, token, opstate, ids, epoch = epoch->second, number]() {
|
||||
callbacks_.emplace_back([this, key, token, opstate, ids, sendTime]() {
|
||||
if (opstate->stop)
|
||||
return;
|
||||
std::lock_guard lock(searchLock_);
|
||||
@@ -1684,27 +1335,15 @@ DhtProxyClient::pushNotificationReceived([[maybe_unused]] const std::map<std::st
|
||||
if (s == searches_.end())
|
||||
return;
|
||||
auto l = s->second.listeners.find(token);
|
||||
if (l == s->second.listeners.end() || l->second.opstate != opstate)
|
||||
if (l == s->second.listeners.end())
|
||||
return;
|
||||
auto& listener = l->second;
|
||||
if (!listener.needsRecovery && listener.syncEpoch == epoch && number > listener.syncRevision
|
||||
&& number - listener.syncRevision == 1) {
|
||||
if (!listener.cache.onValuesExpired(ids, system_clock::time_point::max()))
|
||||
opstate->stop = true;
|
||||
listener.syncRevision = number;
|
||||
} else if (listener.syncEpoch != epoch || number > listener.syncRevision) {
|
||||
listener.needsRecovery = true;
|
||||
resubscribe(key, token, listener);
|
||||
}
|
||||
if (not opstate->stop and not l->second.cache.onValuesExpired(ids, sendTime))
|
||||
opstate->stop = true;
|
||||
});
|
||||
}
|
||||
launchLoop = true;
|
||||
} else {
|
||||
if (epoch->second == listener.syncEpoch)
|
||||
listener.needsRecovery = true;
|
||||
resubscribe(key, token, listener);
|
||||
ret = PushNotificationResult::ValuesExpired;
|
||||
}
|
||||
ret = isExpired ? PushNotificationResult::ValuesExpired : PushNotificationResult::Values;
|
||||
}
|
||||
}
|
||||
} catch (const std::out_of_range& e) {
|
||||
@@ -1732,31 +1371,20 @@ DhtProxyClient::resubscribe([[maybe_unused]] const InfoHash& key,
|
||||
logger_->debug("[proxy:client] [resubscribe] [search {}]", key);
|
||||
|
||||
auto opstate = listener.opstate;
|
||||
if (opstate->stop)
|
||||
return;
|
||||
listener.needsRecovery = listener.needsRecovery || (!opstate->ok && !opstate->inFlight);
|
||||
if (opstate->inFlight && listener.request) {
|
||||
if (listener.refreshSubscriberTimer && listener.refreshSubscriberTimer->expiry() <= system_clock::now())
|
||||
scheduleListenRetry(key, token, listener);
|
||||
return;
|
||||
}
|
||||
if (listener.request) {
|
||||
++opstate->generation;
|
||||
listener.request->cancel();
|
||||
listener.request.reset();
|
||||
}
|
||||
listener.request.reset(); // This will update ok to false
|
||||
opstate->ok = true;
|
||||
|
||||
restinio::http_request_header_t header;
|
||||
header.method(restinio::http_method_subscribe());
|
||||
header.request_target("/key/" + 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;
|
||||
sendListen(header, vcb, opstate, listener, ListenMethod::SUBSCRIBE, key, token);
|
||||
sendListen(header, vcb, opstate, listener, ListenMethod::RESUBSCRIBE);
|
||||
#endif
|
||||
}
|
||||
|
||||
@@ -1764,7 +1392,6 @@ DhtProxyClient::resubscribe([[maybe_unused]] const InfoHash& key,
|
||||
void
|
||||
DhtProxyClient::getPushRequest(Json::Value& body) const
|
||||
{
|
||||
std::lock_guard infoLock(lockCurrentProxyInfos_);
|
||||
body["key"] = deviceKey_;
|
||||
body["client_id"] = pushClientId_;
|
||||
body["session_id"] = pushSessionId_;
|
||||
@@ -1772,6 +1399,23 @@ DhtProxyClient::getPushRequest(Json::Value& body) const
|
||||
body["platform"] = platform_;
|
||||
}
|
||||
|
||||
std::string
|
||||
DhtProxyClient::fillBody(bool resubscribe)
|
||||
{
|
||||
// Fill body with
|
||||
// {
|
||||
// "key":"device_key",
|
||||
// }
|
||||
Json::Value body;
|
||||
getPushRequest(body);
|
||||
if (resubscribe) {
|
||||
// This is the first listen, we want to retrieve previous values.
|
||||
body["refresh"] = true;
|
||||
}
|
||||
auto content = Json::writeString(jsonBuilder_, body) + "\n";
|
||||
std::replace(content.begin(), content.end(), '\n', ' ');
|
||||
return content;
|
||||
}
|
||||
#endif // OPENDHT_PUSH_NOTIFICATIONS
|
||||
|
||||
void
|
||||
@@ -1788,10 +1432,6 @@ DhtProxyClient::setPushNotificationToken([[maybe_unused]] const std::string& tok
|
||||
}
|
||||
if (logger_)
|
||||
logger_->debug("[proxy:client] [push] token changed, resubscribing");
|
||||
if (token.empty()) {
|
||||
restartListeners({});
|
||||
return;
|
||||
}
|
||||
std::lock_guard lock(searchLock_);
|
||||
for (auto& search : searches_) {
|
||||
for (auto& listener : search.second.listeners) {
|
||||
|
||||
+92
-195
@@ -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 {
|
||||
@@ -467,31 +467,27 @@ DhtProxyServer::loadState(Is& is, size_t size)
|
||||
for (auto& listeners : pushListener.second.listeners) {
|
||||
for (auto& listener : listeners.second) {
|
||||
// start listening
|
||||
listener.internalToken = dht_->listen(
|
||||
listeners.first,
|
||||
[this,
|
||||
infoHash = listeners.first,
|
||||
pushToken = pushListener.first,
|
||||
type = listener.type,
|
||||
clientId = listener.clientId,
|
||||
sessionCtx = listener.sessionCtx,
|
||||
topic = listener.topic](const std::vector<Sp<Value>>& values, bool expired) {
|
||||
return this->handlePushListen(infoHash,
|
||||
pushToken,
|
||||
type,
|
||||
clientId,
|
||||
sessionCtx,
|
||||
topic,
|
||||
values,
|
||||
expired);
|
||||
},
|
||||
{},
|
||||
{},
|
||||
[this, sessionCtx = listener.sessionCtx](bool ok) {
|
||||
onPushListenReady(sessionCtx, ok);
|
||||
});
|
||||
listener.internalToken
|
||||
= dht_->listen(listeners.first,
|
||||
[this,
|
||||
infoHash = listeners.first,
|
||||
pushToken = pushListener.first,
|
||||
type = listener.type,
|
||||
clientId = listener.clientId,
|
||||
sessionCtx = listener.sessionCtx,
|
||||
topic = listener.topic](const std::vector<Sp<Value>>& values,
|
||||
bool expired) {
|
||||
return this->handlePushListen(infoHash,
|
||||
pushToken,
|
||||
type,
|
||||
clientId,
|
||||
sessionCtx,
|
||||
topic,
|
||||
values,
|
||||
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(),
|
||||
@@ -513,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,
|
||||
@@ -564,7 +560,6 @@ DhtProxyServer::~DhtProxyServer()
|
||||
l.expireNotifyTimer->cancel();
|
||||
if (l.expireTimer)
|
||||
l.expireTimer->cancel();
|
||||
onPushListenReady(l.sessionCtx, false);
|
||||
dht_->cancelListen(ls.first, std::move(l.internalToken));
|
||||
}
|
||||
}
|
||||
@@ -576,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");
|
||||
}
|
||||
@@ -839,29 +822,16 @@ DhtProxyServer::listen(restinio::request_handle_t request, restinio::router::rou
|
||||
auto& session = listeners_[request->connection_id()];
|
||||
session.hash = infoHash;
|
||||
session.response = response;
|
||||
bool notifyReady = request->header().query() == "sync=1";
|
||||
session.token = dht_->listen(
|
||||
infoHash,
|
||||
[this, response](const std::vector<Sp<Value>>& values, bool expired) {
|
||||
for (const auto& value : values) {
|
||||
auto jsonVal = value->toJson();
|
||||
if (expired)
|
||||
jsonVal["expired"] = true;
|
||||
response->append_chunk(Json::writeString(jsonBuilder_, jsonVal) + "\n");
|
||||
}
|
||||
response->flush();
|
||||
return true;
|
||||
},
|
||||
{},
|
||||
{},
|
||||
[this, response, notifyReady](bool ok) {
|
||||
if (notifyReady) {
|
||||
Json::Value marker;
|
||||
marker["sync"]["complete"] = ok;
|
||||
response->append_chunk(Json::writeString(jsonBuilder_, marker) + "\n");
|
||||
response->flush();
|
||||
}
|
||||
});
|
||||
session.token = dht_->listen(infoHash, [this, response](const std::vector<Sp<Value>>& values, bool expired) {
|
||||
for (const auto& value : values) {
|
||||
auto jsonVal = value->toJson();
|
||||
if (expired)
|
||||
jsonVal["expired"] = true;
|
||||
response->append_chunk(Json::writeString(jsonBuilder_, jsonVal) + "\n");
|
||||
}
|
||||
response->flush();
|
||||
return true;
|
||||
});
|
||||
return restinio::request_handling_status_t::accepted;
|
||||
} catch (const std::exception& e) {
|
||||
return serverError(*request);
|
||||
@@ -937,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);
|
||||
@@ -976,30 +945,20 @@ DhtProxyServer::subscribe(restinio::request_handle_t request, restinio::router::
|
||||
listIt->clientId = clientId;
|
||||
listIt->sessionCtx = std::make_shared<PushSessionContext>(sessionId);
|
||||
} else {
|
||||
bool failed;
|
||||
{
|
||||
std::lock_guard l(listIt->sessionCtx->lock);
|
||||
listIt->sessionCtx->sessionId = sessionId;
|
||||
failed = listIt->sessionCtx->initialized && !listIt->sessionCtx->initialOk;
|
||||
}
|
||||
if (failed) {
|
||||
onPushListenReady(listIt->sessionCtx, false);
|
||||
dht_->cancelListen(infoHash, std::move(listIt->internalToken));
|
||||
listIt->sessionCtx = std::make_shared<PushSessionContext>(sessionId);
|
||||
newListener = true;
|
||||
}
|
||||
std::lock_guard l(listIt->sessionCtx->lock);
|
||||
listIt->sessionCtx->sessionId = sessionId;
|
||||
}
|
||||
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;
|
||||
@@ -1016,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,
|
||||
@@ -1026,84 +985,56 @@ DhtProxyServer::subscribe(restinio::request_handle_t request, restinio::router::
|
||||
infoHash,
|
||||
clientId));
|
||||
|
||||
if (newListener) {
|
||||
// Send response
|
||||
if (not newListener) {
|
||||
if (logger_)
|
||||
logger_->debug("[proxy:server] [subscribe {}] new", infoHash);
|
||||
listener.internalToken = dht_->listen(
|
||||
infoHash,
|
||||
[this, infoHash, pushToken, type, clientId, sessionCtx = listener.sessionCtx, topic](
|
||||
const std::vector<Sp<Value>>& values, bool expired) {
|
||||
return this
|
||||
->handlePushListen(infoHash, pushToken, type, clientId, sessionCtx, topic, values, expired);
|
||||
},
|
||||
{},
|
||||
{},
|
||||
[this, sessionCtx = listener.sessionCtx](bool ok) { onPushListenReady(sessionCtx, ok); });
|
||||
}
|
||||
if (root["sync"].isObject()) {
|
||||
auto sessionCtx = listener.sessionCtx;
|
||||
auto finish = [this,
|
||||
request,
|
||||
context = std::weak_ptr<PushSessionContext>(sessionCtx),
|
||||
sync = root["sync"]]() {
|
||||
auto sessionCtx = context.lock();
|
||||
if (!sessionCtx) {
|
||||
auto response = initHttpResponse(request->create_response(restinio::status_service_unavailable()));
|
||||
response.done();
|
||||
return;
|
||||
}
|
||||
std::vector<Json::Value> snapshotValues;
|
||||
Json::Value marker;
|
||||
bool ok;
|
||||
{
|
||||
std::lock_guard sessionLock(sessionCtx->lock);
|
||||
ok = sessionCtx->initialOk;
|
||||
bool snapshot = !sync["epoch"].isString() || sync["epoch"].asString() != sessionCtx->epoch
|
||||
|| !sync["revision"].isUInt64()
|
||||
|| sync["revision"].asUInt64() != sessionCtx->revision;
|
||||
marker["sync"]["epoch"] = sessionCtx->epoch;
|
||||
marker["sync"]["revision"] = Json::UInt64(sessionCtx->revision);
|
||||
marker["sync"]["snapshot"] = snapshot;
|
||||
marker["sync"]["complete"] = true;
|
||||
if (snapshot)
|
||||
for (const auto& value : sessionCtx->values)
|
||||
snapshotValues.emplace_back(value.second);
|
||||
}
|
||||
if (!ok) {
|
||||
auto response = initHttpResponse(request->create_response(restinio::status_service_unavailable()));
|
||||
response.done();
|
||||
return;
|
||||
}
|
||||
auto response = std::make_shared<ResponseByPartsBuilder>(
|
||||
initHttpResponse(request->create_response<ResponseByParts>()));
|
||||
for (const auto& value : snapshotValues)
|
||||
response->append_chunk(Json::writeString(jsonBuilder_, value) + "\n");
|
||||
response->append_chunk(Json::writeString(jsonBuilder_, marker) + "\n");
|
||||
response->done();
|
||||
};
|
||||
{
|
||||
std::lock_guard sessionLock(sessionCtx->lock);
|
||||
if (sessionCtx->initialized)
|
||||
asio::post(io_context(), std::move(finish));
|
||||
else
|
||||
sessionCtx->pendingSnapshots.emplace_back(std::move(finish));
|
||||
}
|
||||
} else if (newListener || root["refresh"].asBool()) {
|
||||
auto response = initHttpResponse(request->create_response());
|
||||
response.set_body("{}\n");
|
||||
return response.done();
|
||||
} else {
|
||||
logger_->debug("[proxy:server] [subscribe {}] found [client {}]", infoHash, listener.clientId);
|
||||
// Send response header
|
||||
auto response = std::make_shared<ResponseByPartsBuilder>(
|
||||
initHttpResponse(request->create_response<ResponseByParts>()));
|
||||
response->flush();
|
||||
dht_->get(
|
||||
infoHash,
|
||||
[this, response](const Sp<Value>& value) {
|
||||
response->append_chunk(Json::writeString(jsonBuilder_, value->toJson()) + "\n");
|
||||
response->flush();
|
||||
return true;
|
||||
},
|
||||
[response](bool) { response->done(); });
|
||||
if (!root["refresh"].asBool()) {
|
||||
// No Refresh
|
||||
dht_->get(
|
||||
infoHash,
|
||||
[this, response](const Sp<Value>& value) {
|
||||
auto output = Json::writeString(jsonBuilder_, value->toJson()) + "\n";
|
||||
response->append_chunk(output);
|
||||
response->flush();
|
||||
return true;
|
||||
},
|
||||
[response](bool) { response->done(); });
|
||||
} else {
|
||||
// Refresh
|
||||
response->append_chunk("{}\n");
|
||||
return response->done();
|
||||
}
|
||||
} else {
|
||||
// =========== No existing listener for an infoHash ============
|
||||
// Add listen on dht
|
||||
if (logger_)
|
||||
logger_->debug("[proxy:server] [subscribe {}] new", infoHash);
|
||||
listener.internalToken = dht_->listen(infoHash,
|
||||
[this,
|
||||
infoHash,
|
||||
pushToken,
|
||||
type,
|
||||
clientId,
|
||||
sessionCtx = listener.sessionCtx,
|
||||
topic](const std::vector<Sp<Value>>& values, bool expired) {
|
||||
return this->handlePushListen(infoHash,
|
||||
pushToken,
|
||||
type,
|
||||
clientId,
|
||||
sessionCtx,
|
||||
topic,
|
||||
values,
|
||||
expired);
|
||||
});
|
||||
// Send response header
|
||||
auto response = initHttpResponse(request->create_response());
|
||||
response.set_body("{}\n");
|
||||
return response.done();
|
||||
}
|
||||
} catch (...) {
|
||||
return serverError(*request);
|
||||
@@ -1190,7 +1121,6 @@ DhtProxyServer::handleCancelPushListen(const asio::error_code& ec,
|
||||
|
||||
for (auto listener = listeners->second.begin(); listener != listeners->second.end();) {
|
||||
if (listener->clientId == clientId) {
|
||||
onPushListenReady(listener->sessionCtx, false);
|
||||
if (dht_)
|
||||
dht_->cancelListen(key, std::move(listener->internalToken));
|
||||
listener = listeners->second.erase(listener);
|
||||
@@ -1204,29 +1134,6 @@ DhtProxyServer::handleCancelPushListen(const asio::error_code& ec,
|
||||
pushListeners_.erase(pushListener);
|
||||
}
|
||||
|
||||
void
|
||||
DhtProxyServer::onPushListenReady(const std::shared_ptr<PushSessionContext>& sessionCtx, bool ok)
|
||||
{
|
||||
if (!sessionCtx)
|
||||
return;
|
||||
std::vector<std::function<void()>> pending;
|
||||
{
|
||||
std::lock_guard sessionLock(sessionCtx->lock);
|
||||
if (sessionCtx->initialized)
|
||||
return;
|
||||
sessionCtx->initialized = true;
|
||||
sessionCtx->initialOk = ok;
|
||||
pending = std::move(sessionCtx->pendingSnapshots);
|
||||
sessionCtx->pendingSnapshots.clear();
|
||||
}
|
||||
for (auto& response : pending) {
|
||||
if (ok)
|
||||
asio::post(io_context(), std::move(response));
|
||||
else
|
||||
response();
|
||||
}
|
||||
}
|
||||
|
||||
bool
|
||||
DhtProxyServer::handlePushListen(const InfoHash& infoHash,
|
||||
const std::string& pushToken,
|
||||
@@ -1245,15 +1152,7 @@ DhtProxyServer::handlePushListen(const InfoHash& infoHash,
|
||||
|
||||
{
|
||||
std::lock_guard l(sessionCtx->lock);
|
||||
for (const auto& value : values) {
|
||||
if (expired)
|
||||
sessionCtx->values.erase(value->id);
|
||||
else
|
||||
sessionCtx->values[value->id] = value->toJson();
|
||||
}
|
||||
json["s"] = sessionCtx->sessionId;
|
||||
json["epoch"] = sessionCtx->epoch;
|
||||
json["revision"] = std::to_string(++sessionCtx->revision);
|
||||
}
|
||||
|
||||
// Build a comma-separated list of ids from the values. This is intended to be used when
|
||||
@@ -1276,8 +1175,6 @@ DhtProxyServer::handlePushListen(const InfoHash& infoHash,
|
||||
if (expired && values.size() < 2) {
|
||||
json["exp"] = json["ids"];
|
||||
}
|
||||
if (expired)
|
||||
json["expired"] = "true";
|
||||
|
||||
auto minPriority = 1000u;
|
||||
for (const auto& v : values)
|
||||
@@ -1508,7 +1405,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) {
|
||||
@@ -1542,7 +1439,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;
|
||||
@@ -1581,7 +1478,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);
|
||||
@@ -1610,7 +1507,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 {
|
||||
|
||||
+15
-47
@@ -21,16 +21,6 @@ namespace dht {
|
||||
|
||||
using namespace std::literals;
|
||||
|
||||
constexpr auto PEER_DISCOVERY_DHT_SERVICE = "dht"sv;
|
||||
|
||||
struct NodeInsertionPack
|
||||
{
|
||||
dht::InfoHash nodeId;
|
||||
in_port_t port;
|
||||
dht::NetId net;
|
||||
MSGPACK_DEFINE(nodeId, port, net)
|
||||
};
|
||||
|
||||
DhtRunner::DhtRunner()
|
||||
: dht_()
|
||||
{
|
||||
@@ -247,7 +237,7 @@ DhtRunner::run(const Config& config, Context&& context)
|
||||
if (config.peer_discovery or config.peer_publish) {
|
||||
#ifdef OPENDHT_PEER_DISCOVERY
|
||||
peerDiscovery_ = context.peerDiscovery ? std::move(context.peerDiscovery)
|
||||
: std::make_shared<PeerDiscovery>();
|
||||
: std::make_shared<PeerDiscovery>(nullptr, context.logger);
|
||||
#else
|
||||
std::cerr << "Peer discovery requested but OpenDHT built without peer discovery support." << std::endl;
|
||||
#endif
|
||||
@@ -256,32 +246,22 @@ DhtRunner::run(const Config& config, Context&& context)
|
||||
#ifdef OPENDHT_PEER_DISCOVERY
|
||||
auto netId = config.dht_config.node_config.network;
|
||||
if (config.peer_discovery) {
|
||||
peerDiscovery_->startDiscovery<NodeInsertionPack>(PEER_DISCOVERY_DHT_SERVICE,
|
||||
[this, netId](NodeInsertionPack&& v, SockAddr&& addr) {
|
||||
addr.setPort(v.port);
|
||||
if (v.nodeId != dht_->getNodeId() && netId == v.net) {
|
||||
bootstrap(v.nodeId, addr);
|
||||
}
|
||||
});
|
||||
peerDiscovery_->startDiscovery(netId, [this](const InfoHash& nodeId, SockAddr&& address) {
|
||||
if (nodeId != dht_->getNodeId())
|
||||
bootstrap(nodeId, address);
|
||||
});
|
||||
}
|
||||
if (config.peer_publish) {
|
||||
msgpack::sbuffer sbuf_node;
|
||||
NodeInsertionPack adc;
|
||||
adc.net = netId;
|
||||
adc.nodeId = dht_->getNodeId();
|
||||
if (auto socket = dht_->getSocket()) {
|
||||
// IPv4
|
||||
if (const auto& bound4 = socket->getBoundRef(AF_INET)) {
|
||||
adc.port = bound4.getPort();
|
||||
msgpack::pack(sbuf_node, adc);
|
||||
peerDiscovery_->startPublish(AF_INET, PEER_DISCOVERY_DHT_SERVICE, sbuf_node);
|
||||
}
|
||||
// IPv6
|
||||
if (const auto& bound6 = socket->getBoundRef(AF_INET6)) {
|
||||
adc.port = bound6.getPort();
|
||||
sbuf_node.clear();
|
||||
msgpack::pack(sbuf_node, adc);
|
||||
peerDiscovery_->startPublish(AF_INET6, PEER_DISCOVERY_DHT_SERVICE, sbuf_node);
|
||||
const auto& bound4 = socket->getBoundRef(AF_INET);
|
||||
const auto& bound6 = socket->getBoundRef(AF_INET6);
|
||||
const auto port4 = bound4 ? bound4.getPort() : 0;
|
||||
const auto port6 = bound6 ? bound6.getPort() : 0;
|
||||
if (port4 and port6 and port4 != port6) {
|
||||
if (logger_)
|
||||
logger_->error("Unable to publish peer discovery with different IPv4 and IPv6 ports");
|
||||
} else if (const auto port = port4 ? port4 : port6) {
|
||||
peerDiscovery_->startPublish(dht_->getNodeId(), netId, port);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -819,24 +799,16 @@ DhtRunner::query(const InfoHash& hash, QueryCallback cb, DoneCallback done_cb, Q
|
||||
|
||||
std::future<size_t>
|
||||
DhtRunner::listen(InfoHash hash, ValueCallback vcb, Value::Filter f, Where w)
|
||||
{
|
||||
return listen(hash, std::move(vcb), std::move(f), std::move(w), {});
|
||||
}
|
||||
|
||||
std::future<size_t>
|
||||
DhtRunner::listen(InfoHash hash, ValueCallback vcb, Value::Filter f, Where w, DoneCallbackSimple ready)
|
||||
{
|
||||
auto ret_token = std::make_shared<std::promise<size_t>>();
|
||||
std::unique_lock lck(storage_mtx);
|
||||
if (running != State::Running) {
|
||||
lck.unlock();
|
||||
ret_token->set_value(0);
|
||||
if (ready)
|
||||
ready(false);
|
||||
return ret_token->get_future();
|
||||
}
|
||||
pending_ops.emplace([=](SecureDht& dht) mutable {
|
||||
ret_token->set_value(dht.listen(hash, std::move(vcb), std::move(f), std::move(w), std::move(ready)));
|
||||
ret_token->set_value(dht.listen(hash, std::move(vcb), std::move(f), std::move(w)));
|
||||
});
|
||||
cv.notify_all();
|
||||
return ret_token->get_future();
|
||||
@@ -1247,10 +1219,6 @@ DhtRunner::pushNotificationReceived([[maybe_unused]] const std::map<std::string,
|
||||
auto ret_token = std::make_shared<std::promise<PushNotificationResult>>();
|
||||
auto future = ret_token->get_future();
|
||||
std::lock_guard lck(storage_mtx);
|
||||
if (running == State::Idle) {
|
||||
ret_token->set_value(PushNotificationResult::IgnoredStopped);
|
||||
return future;
|
||||
}
|
||||
pending_ops_prio.emplace([ret_token, this, data](SecureDht&) {
|
||||
if (dht_)
|
||||
ret_token->set_value(dht_->pushNotificationReceived(data));
|
||||
|
||||
+8
-34
@@ -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;
|
||||
@@ -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_) {
|
||||
@@ -1527,8 +1505,9 @@ Request::terminate(const asio::error_code& ec)
|
||||
return;
|
||||
|
||||
response_.aborted = ec == asio::error::operation_aborted;
|
||||
if (ec and ec != asio::error::eof)
|
||||
response_.status_code = 0U;
|
||||
if (ec == asio::error::basic_errors::broken_pipe)
|
||||
response_.status_code
|
||||
= 0U; // Avoid to give a successful answer (happen with a broken pipe, takes the last status)
|
||||
|
||||
if (logger_) {
|
||||
if (ec and ec != asio::error::eof and !response_.aborted)
|
||||
@@ -1575,12 +1554,7 @@ Request::handle_response(const asio::error_code& ec, size_t /* n_bytes */)
|
||||
terminate(ec);
|
||||
return;
|
||||
}
|
||||
if (ec == asio::error::eof) {
|
||||
if (!parser_ || llhttp_finish(parser_.get()) != HPE_OK || state_ != State::DONE)
|
||||
terminate(asio::error::broken_pipe);
|
||||
return;
|
||||
}
|
||||
auto request = conn_->read_bytes();
|
||||
auto request = (ec == asio::error::eof) ? std::string {} : conn_->read_bytes();
|
||||
enum llhttp_errno ret = llhttp_execute(parser_.get(), request.c_str(), request.size());
|
||||
if (ret != HPE_OK && ret != HPE_PAUSED) {
|
||||
if (logger_)
|
||||
|
||||
@@ -5,8 +5,6 @@
|
||||
#include "value.h"
|
||||
#include "utils.h"
|
||||
#include "callbacks.h"
|
||||
#include "network_engine.h"
|
||||
#include "request.h"
|
||||
|
||||
namespace dht {
|
||||
|
||||
@@ -15,21 +13,9 @@ namespace dht {
|
||||
*/
|
||||
struct Listener
|
||||
{
|
||||
struct Stream
|
||||
{
|
||||
net::ListenSync sync;
|
||||
std::vector<Sp<net::Request>> initialRequests;
|
||||
~Stream()
|
||||
{
|
||||
for (const auto& request : initialRequests)
|
||||
request->cancel();
|
||||
}
|
||||
};
|
||||
|
||||
time_point time;
|
||||
Query query;
|
||||
int version;
|
||||
std::shared_ptr<Stream> stream;
|
||||
|
||||
Listener(time_point t, Query&& q, int version = 0)
|
||||
: time(t)
|
||||
|
||||
+34
-101
@@ -103,7 +103,6 @@ RequestAnswer::RequestAnswer(ParsedMessage&& msg)
|
||||
, fields(std::move(msg.fields))
|
||||
, nodes4(std::move(msg.nodes4))
|
||||
, nodes6(std::move(msg.nodes6))
|
||||
, listenSync(std::move(msg.listenSync))
|
||||
{}
|
||||
|
||||
NetworkEngine::NetworkEngine(InfoHash& myid,
|
||||
@@ -145,7 +144,7 @@ NetworkEngine::~NetworkEngine()
|
||||
clear();
|
||||
}
|
||||
|
||||
std::vector<Sp<Request>>
|
||||
void
|
||||
NetworkEngine::tellListener(const Sp<Node>& node,
|
||||
Tid socket_id,
|
||||
const InfoHash& hash,
|
||||
@@ -155,13 +154,12 @@ NetworkEngine::tellListener(const Sp<Node>& node,
|
||||
std::vector<Sp<Node>>&& nodes6,
|
||||
std::vector<Sp<Value>>&& values,
|
||||
const Query& query,
|
||||
int version,
|
||||
ListenSync* sync)
|
||||
int version)
|
||||
{
|
||||
auto nnodes = bufferNodes(node->getFamily(), hash, want, nodes, nodes6);
|
||||
try {
|
||||
if (version >= 1) {
|
||||
return sendUpdateValues(node, hash, std::move(values), scheduler.time(), ntoken, socket_id, sync);
|
||||
sendUpdateValues(node, hash, std::move(values), scheduler.time(), ntoken, socket_id);
|
||||
} else {
|
||||
sendNodesValues(node->getAddr(), socket_id, nnodes.first, nnodes.second, std::move(values), query, ntoken);
|
||||
}
|
||||
@@ -169,7 +167,6 @@ NetworkEngine::tellListener(const Sp<Node>& node,
|
||||
if (logger_)
|
||||
logger_->error("Unable to send value: buffer not large enough.");
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
void
|
||||
@@ -178,8 +175,7 @@ NetworkEngine::tellListenerRefreshed(const Sp<Node>& n,
|
||||
const InfoHash&,
|
||||
const Blob& token,
|
||||
const std::vector<Value::Id>& values,
|
||||
int version,
|
||||
ListenSync* sync)
|
||||
int version)
|
||||
{
|
||||
msgpack::sbuffer buffer;
|
||||
msgpack::packer<msgpack::sbuffer> pk(&buffer);
|
||||
@@ -187,15 +183,7 @@ NetworkEngine::tellListenerRefreshed(const Sp<Node>& n,
|
||||
pk.pack_map(4 + (version >= 1 ? 1 : 0) + (config.network ? 1 : 0) + (config.is_client ? 1 : 0));
|
||||
|
||||
pk.pack(version >= 1 ? KEY_A : KEY_U);
|
||||
pk.pack_map(1 + (version >= 1 ? 1 : 0) + (not values.empty() ? 1 : 0) + (not token.empty() ? 1 : 0)
|
||||
+ (sync ? 2 : 0));
|
||||
if (sync) {
|
||||
++sync->sequence;
|
||||
pk.pack(KEY_VERSION);
|
||||
pk.pack(2);
|
||||
pk.pack(KEY_LISTEN_SYNC);
|
||||
pk.pack(*sync);
|
||||
}
|
||||
pk.pack_map(1 + (version >= 1 ? 1 : 0) + (not values.empty() ? 1 : 0) + (not token.empty() ? 1 : 0));
|
||||
pk.pack(KEY_REQ_ID);
|
||||
pk.pack(myid);
|
||||
if (version >= 1) {
|
||||
@@ -258,8 +246,7 @@ NetworkEngine::tellListenerExpired(const Sp<Node>& n,
|
||||
const InfoHash&,
|
||||
const Blob& token,
|
||||
const std::vector<Value::Id>& values,
|
||||
int version,
|
||||
ListenSync* sync)
|
||||
int version)
|
||||
{
|
||||
msgpack::sbuffer buffer;
|
||||
msgpack::packer<msgpack::sbuffer> pk(&buffer);
|
||||
@@ -267,15 +254,7 @@ NetworkEngine::tellListenerExpired(const Sp<Node>& n,
|
||||
pk.pack_map(4 + (version >= 1 ? 1 : 0) + (config.network ? 1 : 0) + (config.is_client ? 1 : 0));
|
||||
|
||||
pk.pack(version >= 1 ? KEY_A : KEY_U);
|
||||
pk.pack_map(1 + (version >= 1 ? 1 : 0) + (not values.empty() ? 1 : 0) + (not token.empty() ? 1 : 0)
|
||||
+ (sync ? 2 : 0));
|
||||
if (sync) {
|
||||
++sync->sequence;
|
||||
pk.pack(KEY_VERSION);
|
||||
pk.pack(2);
|
||||
pk.pack(KEY_LISTEN_SYNC);
|
||||
pk.pack(*sync);
|
||||
}
|
||||
pk.pack_map(1 + (version >= 1 ? 1 : 0) + (not values.empty() ? 1 : 0) + (not token.empty() ? 1 : 0));
|
||||
pk.pack(KEY_REQ_ID);
|
||||
pk.pack(myid);
|
||||
if (version >= 1) {
|
||||
@@ -805,15 +784,10 @@ NetworkEngine::process(std::unique_ptr<ParsedMessage>&& msg, const SockAddr& fro
|
||||
if (logIncoming_ and logger_)
|
||||
logger_->debug("[node {}] got 'listen' request for {}", node->toString(), msg->info_hash.toString());
|
||||
++in_stats.listen;
|
||||
RequestAnswer answer = onListen(node,
|
||||
msg->info_hash,
|
||||
msg->token,
|
||||
msg->socket_id,
|
||||
std::move(msg->query),
|
||||
msg->version,
|
||||
msg->listenSync);
|
||||
RequestAnswer answer
|
||||
= onListen(node, msg->info_hash, msg->token, msg->socket_id, std::move(msg->query), msg->version);
|
||||
auto nnodes = bufferNodes(from.getFamily(), msg->info_hash, msg->want, answer.nodes4, answer.nodes6);
|
||||
sendListenConfirmation(from, msg->tid, answer.ntoken, answer.listenSync);
|
||||
sendListenConfirmation(from, msg->tid, answer.ntoken);
|
||||
break;
|
||||
}
|
||||
case MessageType::UpdateValue: {
|
||||
@@ -1333,8 +1307,7 @@ NetworkEngine::sendListen(const Sp<Node>& n,
|
||||
const Blob& token,
|
||||
Tid socketId,
|
||||
RequestCb&& on_done,
|
||||
RequestExpiredCb&& on_expired,
|
||||
const std::optional<ListenSync>& sync)
|
||||
RequestExpiredCb&& on_expired)
|
||||
{
|
||||
Tid tid(n->getNewTid());
|
||||
msgpack::sbuffer buffer;
|
||||
@@ -1343,15 +1316,11 @@ NetworkEngine::sendListen(const Sp<Node>& n,
|
||||
|
||||
auto has_query = not query.where.empty() or not query.select.empty();
|
||||
pk.pack(KEY_A);
|
||||
pk.pack_map(5 + has_query + (sync ? 1 : 0));
|
||||
pk.pack_map(5 + has_query);
|
||||
pk.pack(KEY_REQ_ID);
|
||||
pk.pack(myid);
|
||||
pk.pack(KEY_VERSION);
|
||||
pk.pack(sync ? 2 : 1);
|
||||
if (sync) {
|
||||
pk.pack(KEY_LISTEN_SYNC);
|
||||
pk.pack(*sync);
|
||||
}
|
||||
pk.pack(1);
|
||||
pk.pack(KEY_REQ_H);
|
||||
pk.pack(hash);
|
||||
pk.pack(KEY_REQ_TOKEN);
|
||||
@@ -1397,23 +1366,14 @@ NetworkEngine::sendListen(const Sp<Node>& n,
|
||||
}
|
||||
|
||||
void
|
||||
NetworkEngine::sendListenConfirmation(const SockAddr& addr,
|
||||
Tid tid,
|
||||
const Blob& token,
|
||||
const std::optional<ListenSync>& sync)
|
||||
NetworkEngine::sendListenConfirmation(const SockAddr& addr, Tid tid, const Blob& token)
|
||||
{
|
||||
msgpack::sbuffer buffer;
|
||||
msgpack::packer<msgpack::sbuffer> pk(&buffer);
|
||||
pk.pack_map(4 + (config.network ? 1 : 0) + (config.is_client ? 1 : 0));
|
||||
|
||||
pk.pack(KEY_R);
|
||||
pk.pack_map(2 + (not token.empty() ? 1 : 0) + (sync ? 2 : 0));
|
||||
if (sync) {
|
||||
pk.pack(KEY_VERSION);
|
||||
pk.pack(2);
|
||||
pk.pack(KEY_LISTEN_SYNC);
|
||||
pk.pack(*sync);
|
||||
}
|
||||
pk.pack_map(2 + (not token.empty() ? 1 : 0));
|
||||
pk.pack(KEY_REQ_ID);
|
||||
pk.pack(myid);
|
||||
insertAddr(pk, addr);
|
||||
@@ -1511,50 +1471,29 @@ NetworkEngine::sendAnnounceValue(const Sp<Node>& n,
|
||||
return req;
|
||||
}
|
||||
|
||||
static std::vector<size_t>
|
||||
updateBatchEnds(const std::vector<Sp<Value>>& values)
|
||||
{
|
||||
std::vector<size_t> ends;
|
||||
size_t totalSize = 0;
|
||||
size_t count = 0;
|
||||
for (size_t index = 0; index < values.size(); ++index) {
|
||||
auto size = packMsg(*values[index]).size();
|
||||
if (count && (count == 32 || totalSize + size > 56 * 1024)) {
|
||||
ends.emplace_back(index);
|
||||
totalSize = 0;
|
||||
count = 0;
|
||||
}
|
||||
totalSize += size;
|
||||
++count;
|
||||
}
|
||||
if (count)
|
||||
ends.emplace_back(values.size());
|
||||
return ends;
|
||||
}
|
||||
|
||||
size_t
|
||||
NetworkEngine::getUpdateBatchCount(const std::vector<Sp<Value>>& values)
|
||||
{
|
||||
return updateBatchEnds(values).size();
|
||||
}
|
||||
|
||||
std::vector<Sp<Request>>
|
||||
void
|
||||
NetworkEngine::sendUpdateValues(const Sp<Node>& n,
|
||||
const InfoHash& infohash,
|
||||
std::vector<Sp<Value>>&& values,
|
||||
time_point created,
|
||||
const Blob& token,
|
||||
size_t socket_id,
|
||||
ListenSync* sync)
|
||||
size_t socket_id)
|
||||
{
|
||||
std::vector<Sp<Request>> requests;
|
||||
size_t begin = 0;
|
||||
for (auto end : updateBatchEnds(values)) {
|
||||
requests.emplace_back(
|
||||
sendUpdateValues(n, infohash, values.begin() + begin, values.begin() + end, created, token, socket_id, sync));
|
||||
begin = end;
|
||||
size_t total_size = 0;
|
||||
|
||||
auto b = values.begin(), e = values.begin();
|
||||
while (e != values.end()) {
|
||||
if (total_size >= MAX_MESSAGE_VALUE_SIZE) {
|
||||
sendUpdateValues(n, infohash, b, e, created, token, socket_id);
|
||||
b = e;
|
||||
total_size = 0;
|
||||
}
|
||||
total_size += (*e)->size();
|
||||
++e;
|
||||
}
|
||||
if (b != e) {
|
||||
sendUpdateValues(n, infohash, b, e, created, token, socket_id);
|
||||
}
|
||||
return requests;
|
||||
}
|
||||
|
||||
Sp<Request>
|
||||
@@ -1564,8 +1503,7 @@ NetworkEngine::sendUpdateValues(const Sp<Node>& n,
|
||||
std::vector<Sp<Value>>::iterator end,
|
||||
time_point created,
|
||||
const Blob& token,
|
||||
size_t socket_id,
|
||||
ListenSync* sync)
|
||||
size_t socket_id)
|
||||
{
|
||||
Tid tid(n->getNewTid());
|
||||
Tid sid(socket_id);
|
||||
@@ -1575,16 +1513,11 @@ NetworkEngine::sendUpdateValues(const Sp<Node>& n,
|
||||
pk.pack_map(5 + (config.network ? 1 : 0) + (config.is_client ? 1 : 0));
|
||||
|
||||
pk.pack(KEY_A);
|
||||
pk.pack_map((created < scheduler.time() ? 7 : 6) + (sync ? 1 : 0));
|
||||
pk.pack_map((created < scheduler.time() ? 7 : 6));
|
||||
pk.pack(KEY_REQ_ID);
|
||||
pk.pack(myid);
|
||||
pk.pack(KEY_VERSION);
|
||||
pk.pack(sync ? 2 : 1);
|
||||
if (sync) {
|
||||
++sync->sequence;
|
||||
pk.pack(KEY_LISTEN_SYNC);
|
||||
pk.pack(*sync);
|
||||
}
|
||||
pk.pack(1);
|
||||
pk.pack(KEY_REQ_H);
|
||||
pk.pack(infohash);
|
||||
pk.pack(KEY_REQ_SID);
|
||||
|
||||
+20
-32
@@ -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,10 +37,6 @@ OpValueCache::onValuesExpired(const std::vector<Sp<Value>>& vals, const system_c
|
||||
if (vit != values.end()) {
|
||||
if (vit->second.updated > t)
|
||||
continue;
|
||||
// Mirror onValuesAdded: a version that was never counted (different
|
||||
// content, not a newer seq) must not decrement the refcount
|
||||
if (*v != *vit->second.data and v->seq <= vit->second.data->seq)
|
||||
continue;
|
||||
|
||||
vit->second.updated = t;
|
||||
vit->second.refCount--;
|
||||
@@ -174,17 +168,13 @@ SearchCache::getOp(const Sp<Query>& q) const
|
||||
}
|
||||
|
||||
size_t
|
||||
SearchCache::listen(const ValueCallback& get_cb,
|
||||
const Sp<Query>& q,
|
||||
Value::Filter&& filter,
|
||||
const OnListen& onListen,
|
||||
DoneCallbackSimple ready)
|
||||
SearchCache::listen(const ValueCallback& get_cb, const Sp<Query>& q, Value::Filter&& filter, const OnListen& onListen)
|
||||
{
|
||||
// find exact match
|
||||
auto op = getOp(q);
|
||||
if (op == ops.end()) {
|
||||
// New query
|
||||
op = ops.emplace(q, std::make_unique<OpCache>(countSources_, waitForSearch_)).first;
|
||||
op = ops.emplace(q, std::make_unique<OpCache>()).first;
|
||||
auto& cache = *op->second;
|
||||
cache.searchToken = onListen(
|
||||
q,
|
||||
@@ -194,7 +184,7 @@ SearchCache::listen(const ValueCallback& get_cb,
|
||||
auto token = nextToken_++;
|
||||
if (nextToken_ == 0)
|
||||
nextToken_++;
|
||||
return op->second->addListener(token, get_cb, q, std::move(filter), std::move(ready)) ? token : 0;
|
||||
return op->second->addListener(token, get_cb, q, std::move(filter)) ? token : 0;
|
||||
}
|
||||
|
||||
bool
|
||||
@@ -250,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>>
|
||||
|
||||
+16
-108
@@ -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,95 +89,44 @@ private:
|
||||
size_t syncedNodes {0};
|
||||
std::map<Value::Id, OpCacheValueStorage> values {};
|
||||
ValueCallback callback;
|
||||
bool countSources {true};
|
||||
};
|
||||
|
||||
class OpCache
|
||||
{
|
||||
public:
|
||||
explicit OpCache(bool countSources = true, bool waitForSearch = false)
|
||||
: cache(
|
||||
[this](const std::vector<Sp<Value>>& vals, bool expired) {
|
||||
if (expired)
|
||||
onValuesExpired(vals);
|
||||
else
|
||||
onValuesAdded(vals);
|
||||
return true;
|
||||
},
|
||||
countSources)
|
||||
, searchReady(!waitForSearch)
|
||||
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); }
|
||||
void onNodeChanged(ListenSyncStatus status)
|
||||
{
|
||||
cache.onNodeChanged(status);
|
||||
if (searchReady && cache.isSynced())
|
||||
onReady();
|
||||
else
|
||||
initialReady = false;
|
||||
}
|
||||
|
||||
void onReady(bool ok = true)
|
||||
{
|
||||
initialReady = ok;
|
||||
searchReady = ok;
|
||||
std::vector<size_t> pending;
|
||||
pending.reserve(readyCallbacks.size());
|
||||
for (const auto& callback : readyCallbacks)
|
||||
pending.emplace_back(callback.first);
|
||||
for (auto token : pending) {
|
||||
auto found = readyCallbacks.find(token);
|
||||
if (found == readyCallbacks.end())
|
||||
continue;
|
||||
auto callback = std::move(found->second);
|
||||
readyCallbacks.erase(found);
|
||||
callback(ok);
|
||||
}
|
||||
}
|
||||
|
||||
void onSearchReady()
|
||||
{
|
||||
searchReady = true;
|
||||
if (cache.isSynced())
|
||||
onReady();
|
||||
}
|
||||
void onNodeChanged(ListenSyncStatus status) { cache.onNodeChanged(status); }
|
||||
|
||||
void onValuesAdded(const std::vector<Sp<Value>>& vals);
|
||||
void onValuesExpired(const std::vector<Sp<Value>>& vals);
|
||||
|
||||
bool addListener(size_t token,
|
||||
const ValueCallback& cb,
|
||||
const Sp<Query>& q,
|
||||
Value::Filter&& filter,
|
||||
DoneCallbackSimple ready = {})
|
||||
bool addListener(size_t token, const ValueCallback& cb, const Sp<Query>& q, Value::Filter&& filter)
|
||||
{
|
||||
auto cached = cache.get(filter);
|
||||
if (not cached.empty() and not cb(cached, false)) {
|
||||
return false;
|
||||
}
|
||||
listeners.emplace(token, LocalListener {q, std::move(filter), cb});
|
||||
if (ready) {
|
||||
if (initialReady || (searchReady && cache.isSynced()))
|
||||
ready(true);
|
||||
else
|
||||
readyCallbacks.emplace(token, std::move(ready));
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool removeListener(size_t token, const time_point& now)
|
||||
{
|
||||
lastRemoved = now;
|
||||
readyCallbacks.erase(token);
|
||||
return listeners.erase(token) > 0;
|
||||
}
|
||||
|
||||
void removeAll()
|
||||
{
|
||||
listeners.clear();
|
||||
readyCallbacks.clear();
|
||||
}
|
||||
void removeAll() { listeners.clear(); }
|
||||
|
||||
bool isDone() { return listeners.empty(); }
|
||||
|
||||
@@ -211,46 +150,17 @@ private:
|
||||
|
||||
OpValueCache cache;
|
||||
std::map<size_t, LocalListener> listeners;
|
||||
std::map<size_t, DoneCallbackSimple> readyCallbacks;
|
||||
bool initialReady {false};
|
||||
bool searchReady {true};
|
||||
time_point lastRemoved {clock::now()};
|
||||
};
|
||||
|
||||
class SearchCache
|
||||
{
|
||||
public:
|
||||
explicit SearchCache(bool countSources = true, bool waitForSearch = false)
|
||||
: countSources_(countSources)
|
||||
, waitForSearch_(waitForSearch)
|
||||
{}
|
||||
SearchCache() {}
|
||||
SearchCache(SearchCache&&) = default;
|
||||
|
||||
using OnListen = std::function<size_t(Sp<Query>, ValueCallback, SyncCallback)>;
|
||||
size_t listen(const ValueCallback& get_cb,
|
||||
const Sp<Query>& q,
|
||||
Value::Filter&& filter,
|
||||
const OnListen& onListen,
|
||||
DoneCallbackSimple ready = {});
|
||||
|
||||
void onReady(bool ok = true)
|
||||
{
|
||||
for (auto& operation : ops)
|
||||
operation.second->onReady(ok);
|
||||
}
|
||||
|
||||
void onReady(const Sp<Query>& query, bool ok)
|
||||
{
|
||||
auto operation = getOp(query);
|
||||
if (operation != ops.end())
|
||||
operation->second->onReady(ok);
|
||||
}
|
||||
|
||||
void onSearchReady()
|
||||
{
|
||||
for (auto& operation : ops)
|
||||
operation.second->onSearchReady();
|
||||
}
|
||||
size_t listen(const ValueCallback& get_cb, const Sp<Query>& q, Value::Filter&& filter, const OnListen& onListen);
|
||||
|
||||
bool cancelListen(size_t gtoken, const time_point& now);
|
||||
void cancelAll(const std::function<void(size_t)>& onCancel);
|
||||
@@ -282,8 +192,6 @@ private:
|
||||
|
||||
size_t nextToken_ {1};
|
||||
time_point nextExpiration_ {time_point::max()};
|
||||
bool countSources_ {true};
|
||||
bool waitForSearch_ {false};
|
||||
};
|
||||
|
||||
} // namespace dht
|
||||
+3
-33
@@ -5,7 +5,6 @@
|
||||
#include "infohash.h"
|
||||
#include "sockaddr.h"
|
||||
#include "net.h"
|
||||
#include "network_engine.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <map>
|
||||
@@ -45,7 +44,6 @@ static constexpr auto KEY_REQ_REFRESHED = "re"sv;
|
||||
static constexpr auto KEY_REQ_FIELDS = "fileds"sv;
|
||||
static constexpr auto KEY_REQ_WANT = "w"sv;
|
||||
static constexpr auto KEY_VERSION = "ve"sv;
|
||||
static constexpr auto KEY_LISTEN_SYNC = "ls"sv;
|
||||
|
||||
static constexpr auto QUERY_PING = "ping"sv;
|
||||
static constexpr auto QUERY_FIND = "find"sv;
|
||||
@@ -130,7 +128,6 @@ struct ParsedMessage
|
||||
/* reported address by the distant node */
|
||||
std::string ua;
|
||||
int version {0};
|
||||
std::optional<ListenSync> listenSync {};
|
||||
SockAddr addr;
|
||||
void msgpack_unpack(const msgpack::object& o);
|
||||
|
||||
@@ -183,10 +180,7 @@ ParsedMessage::complete()
|
||||
auto assembled = pv.reassemble();
|
||||
msgpack::unpacked msg;
|
||||
msgpack::unpack(msg, (const char*) assembled.data(), assembled.size());
|
||||
auto value = std::make_shared<Value>(msg.get());
|
||||
if (listenSync && value->id == Value::INVALID_ID)
|
||||
throw msgpack::type_error();
|
||||
values.emplace_back(std::move(value));
|
||||
values.emplace_back(std::make_shared<Value>(msg.get()));
|
||||
}
|
||||
return true;
|
||||
}
|
||||
@@ -343,22 +337,8 @@ ParsedMessage::msgpack_unpack(const msgpack::object& msg)
|
||||
parsedReq.want = &o.val;
|
||||
else if (key == KEY_VERSION)
|
||||
version = o.val.as<int>();
|
||||
else if (key == KEY_LISTEN_SYNC) {
|
||||
if (o.val.type != msgpack::type::ARRAY || o.val.via.array.size != 4)
|
||||
throw msgpack::type_error();
|
||||
for (const auto& number :
|
||||
{o.val.via.array.ptr[0], o.val.via.array.ptr[1], o.val.via.array.ptr[2], o.val.via.array.ptr[3]})
|
||||
if (number.type != msgpack::type::POSITIVE_INTEGER)
|
||||
throw msgpack::type_error();
|
||||
listenSync = o.val.as<ListenSync>();
|
||||
if (!listenSync->registration)
|
||||
throw msgpack::type_error();
|
||||
}
|
||||
}
|
||||
|
||||
if (listenSync && version != 2)
|
||||
throw msgpack::type_error();
|
||||
|
||||
if (parsedReq.sa) {
|
||||
if (parsedReq.sa->type != msgpack::type::BIN)
|
||||
throw msgpack::type_error();
|
||||
@@ -380,28 +360,18 @@ ParsedMessage::msgpack_unpack(const msgpack::object& msg)
|
||||
if (parsedReq.values) {
|
||||
if (parsedReq.values->type != msgpack::type::ARRAY)
|
||||
throw msgpack::type_error();
|
||||
if (listenSync && parsedReq.values->via.array.size > 32)
|
||||
throw msgpack::type_error();
|
||||
for (size_t i = 0; i < parsedReq.values->via.array.size; i++) {
|
||||
auto& packed_v = parsedReq.values->via.array.ptr[i];
|
||||
if (packed_v.type == msgpack::type::POSITIVE_INTEGER) {
|
||||
// Skip oversize values with a small margin for header overhead
|
||||
if (packed_v.via.u64 > MAX_VALUE_SIZE + 32 || (listenSync && !packed_v.via.u64)) {
|
||||
if (listenSync)
|
||||
throw msgpack::type_error();
|
||||
if (packed_v.via.u64 > MAX_VALUE_SIZE + 32)
|
||||
continue;
|
||||
}
|
||||
value_parts[i].total_size = packed_v.via.u64;
|
||||
} else {
|
||||
try {
|
||||
auto value = std::make_shared<Value>(parsedReq.values->via.array.ptr[i]);
|
||||
if (listenSync && value->id == Value::INVALID_ID)
|
||||
throw msgpack::type_error();
|
||||
values.emplace_back(std::move(value));
|
||||
values.emplace_back(std::make_shared<Value>(parsedReq.values->via.array.ptr[i]));
|
||||
} catch (const std::exception& e) {
|
||||
// DHT_LOG_WARN("Error reading value: %s", e.what());
|
||||
if (listenSync)
|
||||
throw msgpack::type_error();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,474 +0,0 @@
|
||||
// Copyright (c) 2014-2026 Savoir-faire Linux Inc.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
#include "peer_discovery.h"
|
||||
#include "network_utils.h"
|
||||
#include "utils.h"
|
||||
|
||||
#ifdef __ANDROID__
|
||||
#include "compat/getif_workaround_android.h"
|
||||
#endif
|
||||
|
||||
#include <asio.hpp>
|
||||
|
||||
using namespace std::literals;
|
||||
|
||||
namespace dht {
|
||||
|
||||
// Organization-local Scope multicast
|
||||
constexpr char MULTICAST_ADDRESS_IPV4[] = "239.192.0.1";
|
||||
constexpr char MULTICAST_ADDRESS_IPV6[] = "ff08::101";
|
||||
|
||||
class PeerDiscovery::DomainPeerDiscovery
|
||||
{
|
||||
public:
|
||||
DomainPeerDiscovery(asio::ip::udp domain,
|
||||
in_port_t port,
|
||||
Sp<asio::io_context> ioContext = {},
|
||||
Sp<Logger> logger = {});
|
||||
~DomainPeerDiscovery();
|
||||
|
||||
void startDiscovery(std::string_view type, ServiceDiscoveredCallback callback);
|
||||
void startPublish(std::string_view type, const msgpack::sbuffer& pack_buf);
|
||||
|
||||
void stop();
|
||||
|
||||
bool stopDiscovery(std::string_view type);
|
||||
bool stopPublish(std::string_view type);
|
||||
|
||||
void connectivityChanged();
|
||||
|
||||
void stopConnectivityChanged();
|
||||
|
||||
private:
|
||||
Sp<Logger> logger_;
|
||||
// dmtx_ for callbackmap_ and drunning_ (write)
|
||||
std::mutex dmtx_;
|
||||
// mtx_ for messages_ and lrunning (listen)
|
||||
std::mutex mtx_;
|
||||
std::shared_ptr<asio::io_context> ioContext_;
|
||||
|
||||
static constexpr dht::duration PeerDiscovery_PERIOD_MAX {std::chrono::minutes(1)};
|
||||
static constexpr std::chrono::seconds PeerDiscovery_PERIOD {10};
|
||||
asio::steady_timer peerDiscoveryTimer;
|
||||
std::chrono::steady_clock::duration peerDiscovery_period {PeerDiscovery_PERIOD};
|
||||
|
||||
asio::ip::udp::socket sockFd_;
|
||||
asio::ip::udp::endpoint sockAddrSend_;
|
||||
|
||||
std::array<char, 64 * 1024> receiveBuf_;
|
||||
asio::ip::udp::endpoint receiveFrom_;
|
||||
|
||||
msgpack::sbuffer sbuf_;
|
||||
std::map<std::string, msgpack::sbuffer, std::less<>> messages_;
|
||||
std::map<std::string, ServiceDiscoveredCallback, std::less<>> callbackmap_;
|
||||
bool lrunning_ {false};
|
||||
bool drunning_ {false};
|
||||
|
||||
void loopListener();
|
||||
void query(const asio::ip::udp::endpoint& peer);
|
||||
void reloadMessages();
|
||||
|
||||
void stopDiscovery();
|
||||
void stopPublish();
|
||||
|
||||
void publish(const asio::ip::udp::endpoint& peer);
|
||||
|
||||
void reDiscover();
|
||||
};
|
||||
|
||||
PeerDiscovery::DomainPeerDiscovery::DomainPeerDiscovery(asio::ip::udp domain,
|
||||
in_port_t port,
|
||||
Sp<asio::io_context> ioContext,
|
||||
Sp<Logger> logger)
|
||||
: logger_(logger)
|
||||
, ioContext_(ioContext)
|
||||
, peerDiscoveryTimer(*ioContext_)
|
||||
, sockFd_(*ioContext_, domain)
|
||||
, sockAddrSend_(asio::ip::make_address(domain.family() == AF_INET ? MULTICAST_ADDRESS_IPV4 : MULTICAST_ADDRESS_IPV6),
|
||||
port)
|
||||
{
|
||||
try {
|
||||
sockFd_.set_option(asio::ip::multicast::join_group(sockAddrSend_.address()));
|
||||
sockFd_.set_option(asio::ip::udp::socket::reuse_address(true));
|
||||
sockFd_.bind({domain, port});
|
||||
} catch (const std::exception& e) {
|
||||
#ifdef __ANDROID__
|
||||
if (domain.family() == AF_INET && strcmp(e.what(), "set_option: No such device") == 0) {
|
||||
try {
|
||||
sockFd_.set_option(asio::ip::udp::socket::reuse_address(true));
|
||||
auto mc_interface = workaround::get_interface();
|
||||
sockFd_.set_option(asio::ip::multicast::outbound_interface(mc_interface));
|
||||
sockFd_.set_option(asio::ip::multicast::join_group(sockAddrSend_.address().to_v4(), mc_interface));
|
||||
sockFd_.bind({domain, port});
|
||||
} catch (const std::exception& e) {
|
||||
if (logger_)
|
||||
logger_->error("Unable to start peer discovery using android workaround: {}", e.what());
|
||||
}
|
||||
} else
|
||||
#endif
|
||||
if (logger_)
|
||||
logger_->error("Unable to start peer discovery for {}: {}",
|
||||
domain.family() == AF_INET ? "IPv4" : "IPv6",
|
||||
e.what());
|
||||
}
|
||||
}
|
||||
|
||||
PeerDiscovery::DomainPeerDiscovery::~DomainPeerDiscovery()
|
||||
{
|
||||
stop();
|
||||
sockFd_.close();
|
||||
}
|
||||
|
||||
void
|
||||
PeerDiscovery::DomainPeerDiscovery::startDiscovery(std::string_view type, ServiceDiscoveredCallback callback)
|
||||
{
|
||||
std::lock_guard lck(dmtx_);
|
||||
callbackmap_[std::string(type)] = callback;
|
||||
if (not drunning_) {
|
||||
drunning_ = true;
|
||||
asio::post(*ioContext_, [this]() {
|
||||
loopListener();
|
||||
query(sockAddrSend_);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
PeerDiscovery::DomainPeerDiscovery::loopListener()
|
||||
{
|
||||
std::lock_guard lck(dmtx_);
|
||||
if (not drunning_)
|
||||
return;
|
||||
sockFd_.async_receive_from(asio::buffer(receiveBuf_),
|
||||
receiveFrom_,
|
||||
[this](const asio::error_code& error, size_t bytes) {
|
||||
if (error == asio::error::operation_aborted)
|
||||
return;
|
||||
if (error) {
|
||||
if (logger_)
|
||||
logger_->error("Error receiving message: {}", error.message());
|
||||
}
|
||||
try {
|
||||
auto rcv = msgpack::unpack(receiveBuf_.data(), bytes);
|
||||
msgpack::object obj = rcv.get();
|
||||
|
||||
if (obj.type == msgpack::type::STR) {
|
||||
if (lrunning_ and obj.as<std::string_view>() == "q"sv)
|
||||
publish(receiveFrom_);
|
||||
} else if (obj.type == msgpack::type::MAP) {
|
||||
for (unsigned i = 0; i < obj.via.map.size; i++) {
|
||||
auto& o = obj.via.map.ptr[i];
|
||||
if (o.key.type != msgpack::type::STR)
|
||||
continue;
|
||||
ServiceDiscoveredCallback cb;
|
||||
{
|
||||
std::lock_guard lck(dmtx_);
|
||||
if (drunning_) {
|
||||
auto callback = callbackmap_.find(o.key.as<std::string_view>());
|
||||
if (callback != callbackmap_.end())
|
||||
cb = callback->second;
|
||||
} else
|
||||
return;
|
||||
}
|
||||
if (cb)
|
||||
cb(std::move(o.val),
|
||||
SockAddr {receiveFrom_.data(), (socklen_t) receiveFrom_.size()});
|
||||
}
|
||||
} else {
|
||||
throw msgpack::type_error {};
|
||||
}
|
||||
} catch (const std::exception& e) {
|
||||
if (logger_)
|
||||
logger_->error("Error receiving packet: {}", e.what());
|
||||
}
|
||||
loopListener();
|
||||
});
|
||||
}
|
||||
|
||||
void
|
||||
PeerDiscovery::DomainPeerDiscovery::query(const asio::ip::udp::endpoint& peer)
|
||||
{
|
||||
std::lock_guard lck(dmtx_);
|
||||
if (not drunning_)
|
||||
return;
|
||||
|
||||
msgpack::sbuffer pbuf_request;
|
||||
msgpack::pack(pbuf_request, "q");
|
||||
|
||||
sockFd_.async_send_to(asio::buffer(pbuf_request.data(), pbuf_request.size()),
|
||||
peer,
|
||||
[logger = logger_, peer](const asio::error_code& ec, size_t) {
|
||||
if (ec and (ec != asio::error::operation_aborted) and logger)
|
||||
logger->warn("Error sending packet to: {} with err: {}",
|
||||
peer.address().to_string(),
|
||||
ec.message());
|
||||
});
|
||||
}
|
||||
|
||||
void
|
||||
PeerDiscovery::DomainPeerDiscovery::publish(const asio::ip::udp::endpoint& peer)
|
||||
{
|
||||
std::lock_guard lck(mtx_);
|
||||
if (not lrunning_)
|
||||
return;
|
||||
|
||||
sockFd_.async_send_to(asio::buffer((const void*) sbuf_.data(), sbuf_.size()),
|
||||
peer,
|
||||
[logger = logger_, peer](const asio::error_code& ec, size_t) {
|
||||
if (ec and (ec != asio::error::operation_aborted) and logger)
|
||||
logger->warn("Error sending packet to: {} with err: {}",
|
||||
peer.address().to_string(),
|
||||
ec.message());
|
||||
});
|
||||
}
|
||||
|
||||
void
|
||||
PeerDiscovery::DomainPeerDiscovery::startPublish(std::string_view type, const msgpack::sbuffer& pack_buf)
|
||||
{
|
||||
msgpack::sbuffer pack_buf_c(pack_buf.size());
|
||||
pack_buf_c.write(pack_buf.data(), pack_buf.size());
|
||||
|
||||
std::lock_guard lck(mtx_);
|
||||
messages_[std::string(type)] = std::move(pack_buf_c);
|
||||
reloadMessages();
|
||||
lrunning_ = true;
|
||||
asio::post(*ioContext_, [this]() { publish(sockAddrSend_); });
|
||||
}
|
||||
|
||||
bool
|
||||
PeerDiscovery::DomainPeerDiscovery::stopDiscovery(std::string_view type)
|
||||
{
|
||||
std::lock_guard lck(dmtx_);
|
||||
auto it = callbackmap_.find(type);
|
||||
if (it != callbackmap_.end()) {
|
||||
callbackmap_.erase(it);
|
||||
if (callbackmap_.empty())
|
||||
stopDiscovery();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool
|
||||
PeerDiscovery::DomainPeerDiscovery::stopPublish(std::string_view type)
|
||||
{
|
||||
std::lock_guard lck(mtx_);
|
||||
auto it = messages_.find(type);
|
||||
if (it != messages_.end()) {
|
||||
messages_.erase(it);
|
||||
if (messages_.empty())
|
||||
stopPublish();
|
||||
else
|
||||
reloadMessages();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void
|
||||
PeerDiscovery::DomainPeerDiscovery::stopDiscovery()
|
||||
{
|
||||
drunning_ = false;
|
||||
}
|
||||
|
||||
void
|
||||
PeerDiscovery::DomainPeerDiscovery::stopPublish()
|
||||
{
|
||||
lrunning_ = false;
|
||||
}
|
||||
|
||||
void
|
||||
PeerDiscovery::DomainPeerDiscovery::stop()
|
||||
{
|
||||
{
|
||||
std::lock_guard lck(dmtx_);
|
||||
stopDiscovery();
|
||||
}
|
||||
{
|
||||
std::lock_guard lck(mtx_);
|
||||
stopPublish();
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
PeerDiscovery::DomainPeerDiscovery::reloadMessages()
|
||||
{
|
||||
sbuf_.clear();
|
||||
msgpack::packer<msgpack::sbuffer> pk(&sbuf_);
|
||||
pk.pack_map(messages_.size());
|
||||
for (const auto& m : messages_) {
|
||||
pk.pack(m.first);
|
||||
sbuf_.write(m.second.data(), m.second.size());
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
PeerDiscovery::DomainPeerDiscovery::reDiscover()
|
||||
{
|
||||
asio::error_code ec;
|
||||
|
||||
sockFd_.set_option(asio::ip::multicast::join_group(sockAddrSend_.address()), ec);
|
||||
if (ec and logger_)
|
||||
logger_->warn("Unable to multicast on {}: {}", sockAddrSend_.address().to_string(), ec.message());
|
||||
query(sockAddrSend_);
|
||||
}
|
||||
|
||||
void
|
||||
PeerDiscovery::DomainPeerDiscovery::connectivityChanged()
|
||||
{
|
||||
asio::post(*ioContext_, [this]() {
|
||||
reDiscover();
|
||||
publish(sockAddrSend_);
|
||||
});
|
||||
if (logger_)
|
||||
logger_->debug("PeerDiscovery: connectivity changed");
|
||||
|
||||
if (peerDiscovery_period == PeerDiscovery_PERIOD_MAX) {
|
||||
peerDiscovery_period = PeerDiscovery_PERIOD;
|
||||
} else {
|
||||
peerDiscoveryTimer.expires_after(peerDiscovery_period);
|
||||
peerDiscoveryTimer.async_wait([this](const asio::error_code& ec) {
|
||||
if (ec == asio::error::operation_aborted)
|
||||
return;
|
||||
connectivityChanged();
|
||||
});
|
||||
peerDiscovery_period = std::min(peerDiscovery_period * 2, PeerDiscovery_PERIOD_MAX);
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
PeerDiscovery::DomainPeerDiscovery::stopConnectivityChanged()
|
||||
{
|
||||
peerDiscoveryTimer.cancel();
|
||||
peerDiscovery_period = PeerDiscovery_PERIOD;
|
||||
}
|
||||
|
||||
PeerDiscovery::PeerDiscovery(in_port_t port, Sp<asio::io_context> ioContext, Sp<Logger> logger)
|
||||
{
|
||||
if (not ioContext) {
|
||||
ioContext = std::make_shared<asio::io_context>();
|
||||
ioContext_ = ioContext;
|
||||
ioRunnner_ = std::thread([logger, ioContext] {
|
||||
try {
|
||||
if (logger)
|
||||
logger->d("[peerdiscovery] starting io_context");
|
||||
auto work = asio::make_work_guard(*ioContext);
|
||||
ioContext->run();
|
||||
if (logger)
|
||||
logger->d("[peerdiscovery] io_context stopped");
|
||||
} catch (const std::exception& ex) {
|
||||
if (logger)
|
||||
logger->e("[peerdiscovery] run error: %s", ex.what());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
try {
|
||||
peerDiscovery4_.reset(new DomainPeerDiscovery(asio::ip::udp::v4(), port, ioContext, logger));
|
||||
} catch (const std::exception& e) {
|
||||
if (logger)
|
||||
logger->e("[peerdiscovery] Unable to start IPv4: %s", e.what());
|
||||
}
|
||||
try {
|
||||
peerDiscovery6_.reset(new DomainPeerDiscovery(asio::ip::udp::v6(), port, ioContext, logger));
|
||||
} catch (const std::exception& e) {
|
||||
if (logger)
|
||||
logger->e("[peerdiscovery] Unable to start IPv6: %s", e.what());
|
||||
}
|
||||
}
|
||||
|
||||
PeerDiscovery::~PeerDiscovery()
|
||||
{
|
||||
stop();
|
||||
if (ioContext_)
|
||||
ioContext_->stop();
|
||||
if (ioRunnner_.joinable())
|
||||
ioRunnner_.join();
|
||||
}
|
||||
|
||||
void
|
||||
PeerDiscovery::startDiscovery(std::string_view type, ServiceDiscoveredCallback callback)
|
||||
{
|
||||
if (peerDiscovery4_)
|
||||
peerDiscovery4_->startDiscovery(type, callback);
|
||||
if (peerDiscovery6_)
|
||||
peerDiscovery6_->startDiscovery(type, callback);
|
||||
}
|
||||
|
||||
void
|
||||
PeerDiscovery::startPublish(std::string_view type, const msgpack::sbuffer& pack_buf)
|
||||
{
|
||||
if (peerDiscovery4_)
|
||||
peerDiscovery4_->startPublish(type, pack_buf);
|
||||
if (peerDiscovery6_)
|
||||
peerDiscovery6_->startPublish(type, pack_buf);
|
||||
}
|
||||
|
||||
void
|
||||
PeerDiscovery::startPublish(sa_family_t domain, std::string_view type, const msgpack::sbuffer& pack_buf)
|
||||
{
|
||||
if (domain == AF_INET) {
|
||||
if (peerDiscovery4_)
|
||||
peerDiscovery4_->startPublish(type, pack_buf);
|
||||
} else if (domain == AF_INET6) {
|
||||
if (peerDiscovery6_)
|
||||
peerDiscovery6_->startPublish(type, pack_buf);
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
PeerDiscovery::stop()
|
||||
{
|
||||
if (peerDiscovery4_)
|
||||
peerDiscovery4_->stop();
|
||||
if (peerDiscovery6_)
|
||||
peerDiscovery6_->stop();
|
||||
}
|
||||
|
||||
bool
|
||||
PeerDiscovery::stopDiscovery(std::string_view type)
|
||||
{
|
||||
bool stopped4 = peerDiscovery4_ and peerDiscovery4_->stopDiscovery(type);
|
||||
bool stopped6 = peerDiscovery6_ and peerDiscovery6_->stopDiscovery(type);
|
||||
return stopped4 or stopped6;
|
||||
}
|
||||
|
||||
bool
|
||||
PeerDiscovery::stopPublish(std::string_view type)
|
||||
{
|
||||
bool stopped4 = peerDiscovery4_ and peerDiscovery4_->stopPublish(type);
|
||||
bool stopped6 = peerDiscovery6_ and peerDiscovery6_->stopPublish(type);
|
||||
return stopped4 or stopped6;
|
||||
}
|
||||
|
||||
bool
|
||||
PeerDiscovery::stopPublish(sa_family_t domain, std::string_view type)
|
||||
{
|
||||
if (domain == AF_INET) {
|
||||
return peerDiscovery4_ and peerDiscovery4_->stopPublish(type);
|
||||
} else if (domain == AF_INET6) {
|
||||
return peerDiscovery6_ and peerDiscovery6_->stopPublish(type);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void
|
||||
PeerDiscovery::connectivityChanged()
|
||||
{
|
||||
if (peerDiscovery4_)
|
||||
peerDiscovery4_->connectivityChanged();
|
||||
if (peerDiscovery6_)
|
||||
peerDiscovery6_->connectivityChanged();
|
||||
}
|
||||
|
||||
void
|
||||
PeerDiscovery::stopConnectivityChanged()
|
||||
{
|
||||
if (peerDiscovery4_)
|
||||
peerDiscovery4_->stopConnectivityChanged();
|
||||
if (peerDiscovery6_)
|
||||
peerDiscovery6_->stopConnectivityChanged();
|
||||
}
|
||||
|
||||
} /* namespace dht */
|
||||
@@ -0,0 +1,504 @@
|
||||
// Copyright (c) 2014-2026 Savoir-faire Linux Inc.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
#include "dns_sd.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <charconv>
|
||||
#include <map>
|
||||
|
||||
namespace dht::mdns::dns_sd {
|
||||
namespace {
|
||||
|
||||
constexpr std::string_view SERVICE_NAME = "_opendht._udp.local.";
|
||||
constexpr std::string_view SERVICE_ENUMERATION_NAME = "_services._dns-sd._udp.local.";
|
||||
constexpr std::string_view SERVICE_SUFFIX = "._opendht._udp.local.";
|
||||
constexpr std::string_view HOST_SUFFIX = ".local.";
|
||||
|
||||
std::vector<uint8_t>
|
||||
txtString(std::string value)
|
||||
{
|
||||
return {value.begin(), value.end()};
|
||||
}
|
||||
|
||||
std::string
|
||||
asciiLower(std::string_view value)
|
||||
{
|
||||
std::string lower;
|
||||
lower.reserve(value.size());
|
||||
for (const auto character : value) {
|
||||
if (character >= 'A' and character <= 'Z')
|
||||
lower.push_back(static_cast<char>(character + ('a' - 'A')));
|
||||
else
|
||||
lower.push_back(character);
|
||||
}
|
||||
return lower;
|
||||
}
|
||||
|
||||
void
|
||||
validateService(const Service& service)
|
||||
{
|
||||
if (not service.nodeId)
|
||||
throw ParseError("DNS-SD service requires a node ID");
|
||||
if (service.port == 0)
|
||||
throw ParseError("DNS-SD service requires a port");
|
||||
if (service.addresses.empty())
|
||||
throw ParseError("DNS-SD service requires an address");
|
||||
}
|
||||
|
||||
Message
|
||||
makeAdvertisement(const Service& service, uint32_t serviceTtl, uint32_t hostTtl)
|
||||
{
|
||||
validateService(service);
|
||||
const auto instance = instanceName(service.nodeId);
|
||||
const auto host = hostName(service.nodeId);
|
||||
|
||||
Message message;
|
||||
message.response = true;
|
||||
message.authoritative = true;
|
||||
message.answers.push_back({serviceName(), Type::PTR, CLASS_IN, false, serviceTtl, PtrData {instance}});
|
||||
message.additionals.push_back({
|
||||
instance, Type::SRV, CLASS_IN, true, hostTtl, SrvData {0, 0, service.port, host}
|
||||
});
|
||||
|
||||
TxtData txt;
|
||||
txt.strings.emplace_back(txtString("txtvers=1"));
|
||||
txt.strings.emplace_back(txtString("id=" + service.nodeId.toString()));
|
||||
txt.strings.emplace_back(txtString("net=" + std::to_string(service.network)));
|
||||
message.additionals.push_back({instance, Type::TXT, CLASS_IN, true, serviceTtl, std::move(txt)});
|
||||
|
||||
for (const auto& address : service.addresses) {
|
||||
std::visit(
|
||||
[&](const auto& data) {
|
||||
using Data = std::decay_t<decltype(data)>;
|
||||
constexpr auto type = std::is_same_v<Data, AData> ? Type::A : Type::AAAA;
|
||||
message.additionals.push_back({host, type, CLASS_IN, true, hostTtl, data});
|
||||
},
|
||||
address);
|
||||
}
|
||||
return message;
|
||||
}
|
||||
|
||||
std::optional<InfoHash>
|
||||
nodeIdFromInstance(const Name& instance)
|
||||
{
|
||||
const auto& labels = instance.labels();
|
||||
const auto service = serviceName();
|
||||
const auto& serviceLabels = service.labels();
|
||||
if (labels.size() != serviceLabels.size() + 1
|
||||
or not std::equal(labels.begin() + 1, labels.end(), serviceLabels.begin(), serviceLabels.end()))
|
||||
return {};
|
||||
if (labels.front().size() != 40)
|
||||
return {};
|
||||
const InfoHash nodeId {labels.front()};
|
||||
if (not nodeId)
|
||||
return {};
|
||||
return nodeId;
|
||||
}
|
||||
|
||||
struct TxtValues
|
||||
{
|
||||
InfoHash nodeId;
|
||||
NetId network {};
|
||||
};
|
||||
|
||||
std::optional<TxtValues>
|
||||
parseTxt(const TxtData& txt)
|
||||
{
|
||||
std::map<std::string, std::string> values;
|
||||
for (const auto& raw : txt.strings) {
|
||||
const std::string_view string {reinterpret_cast<const char*>(raw.data()), raw.size()};
|
||||
const auto separator = string.find('=');
|
||||
if (separator == std::string_view::npos or separator == 0)
|
||||
continue;
|
||||
const auto keyView = string.substr(0, separator);
|
||||
if (std::any_of(keyView.begin(), keyView.end(), [](char value) {
|
||||
return value < 0x20 or value > 0x7e or value == '=';
|
||||
}))
|
||||
continue;
|
||||
const auto key = asciiLower(keyView);
|
||||
if ((key == "txtvers" or key == "id" or key == "net") and values.count(key))
|
||||
return {};
|
||||
values.emplace(key, std::string(string.substr(separator + 1)));
|
||||
}
|
||||
|
||||
if (values.size() < 3 or values["txtvers"] != "1" or values["id"].size() != 40)
|
||||
return {};
|
||||
TxtValues parsed;
|
||||
parsed.nodeId = InfoHash {values["id"]};
|
||||
if (not parsed.nodeId)
|
||||
return {};
|
||||
const auto& network = values["net"];
|
||||
const auto result = std::from_chars(network.data(), network.data() + network.size(), parsed.network);
|
||||
if (result.ec != std::errc {} or result.ptr != network.data() + network.size())
|
||||
return {};
|
||||
return parsed;
|
||||
}
|
||||
|
||||
template<typename Callback>
|
||||
void
|
||||
forEachRecord(const Message& message, Callback&& callback)
|
||||
{
|
||||
for (const auto& record : message.answers)
|
||||
callback(record);
|
||||
for (const auto& record : message.authorities)
|
||||
callback(record);
|
||||
for (const auto& record : message.additionals)
|
||||
callback(record);
|
||||
}
|
||||
|
||||
bool
|
||||
matches(const Question& question, const ResourceRecord& record)
|
||||
{
|
||||
return question.name == record.name and (question.type == Type::ANY or question.type == record.type)
|
||||
and (question.classCode == CLASS_ANY or question.classCode == record.classCode);
|
||||
}
|
||||
|
||||
bool
|
||||
sameRecord(const ResourceRecord& lhs, const ResourceRecord& rhs)
|
||||
{
|
||||
return lhs.name == rhs.name and lhs.type == rhs.type and lhs.classCode == rhs.classCode and lhs.data == rhs.data;
|
||||
}
|
||||
|
||||
bool
|
||||
isKnownAnswer(const Message& query, const ResourceRecord& record)
|
||||
{
|
||||
return std::any_of(query.answers.begin(), query.answers.end(), [&](const ResourceRecord& known) {
|
||||
return sameRecord(known, record) and known.ttl > record.ttl / 2;
|
||||
});
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
class Cache::Impl
|
||||
{
|
||||
public:
|
||||
explicit Impl(Now now)
|
||||
: now_(std::move(now))
|
||||
{}
|
||||
|
||||
struct Entry
|
||||
{
|
||||
ResourceRecord record;
|
||||
Clock::time_point expires;
|
||||
};
|
||||
|
||||
std::vector<Service> update(const Message& message)
|
||||
{
|
||||
expire();
|
||||
std::vector<const ResourceRecord*> records;
|
||||
forEachRecord(message, [&](const ResourceRecord& record) { records.push_back(&record); });
|
||||
|
||||
std::vector<const ResourceRecord*> flushed;
|
||||
for (const auto* record : records) {
|
||||
if (not record->cacheFlush)
|
||||
continue;
|
||||
const auto alreadyFlushed = std::any_of(flushed.begin(), flushed.end(), [&](const ResourceRecord* other) {
|
||||
return other->name == record->name and other->type == record->type
|
||||
and other->classCode == record->classCode;
|
||||
});
|
||||
if (not alreadyFlushed) {
|
||||
flushed.push_back(record);
|
||||
entries_.erase(std::remove_if(entries_.begin(),
|
||||
entries_.end(),
|
||||
[&](const Entry& entry) {
|
||||
return entry.record.name == record->name
|
||||
and entry.record.type == record->type
|
||||
and entry.record.classCode == record->classCode;
|
||||
}),
|
||||
entries_.end());
|
||||
}
|
||||
}
|
||||
|
||||
for (const auto* record : records) {
|
||||
if (record->ttl == 0) {
|
||||
entries_.erase(std::remove_if(entries_.begin(),
|
||||
entries_.end(),
|
||||
[&](const Entry& entry) { return sameRecord(entry.record, *record); }),
|
||||
entries_.end());
|
||||
continue;
|
||||
}
|
||||
const auto expires = now_() + std::chrono::seconds(record->ttl);
|
||||
const auto existing = std::find_if(entries_.begin(), entries_.end(), [&](const Entry& entry) {
|
||||
return sameRecord(entry.record, *record);
|
||||
});
|
||||
if (existing == entries_.end())
|
||||
entries_.push_back({*record, expires});
|
||||
else {
|
||||
existing->record = *record;
|
||||
existing->expires = expires;
|
||||
}
|
||||
}
|
||||
|
||||
auto current = collect();
|
||||
std::vector<Service> discovered;
|
||||
for (const auto& service : current) {
|
||||
if (std::find_if(emitted_.begin(),
|
||||
emitted_.end(),
|
||||
[&](const Service& previous) { return equal(previous, service); })
|
||||
== emitted_.end())
|
||||
discovered.push_back(service);
|
||||
}
|
||||
emitted_ = std::move(current);
|
||||
return discovered;
|
||||
}
|
||||
|
||||
std::vector<Service> services()
|
||||
{
|
||||
expire();
|
||||
auto current = collect();
|
||||
emitted_ = current;
|
||||
return current;
|
||||
}
|
||||
|
||||
void clear()
|
||||
{
|
||||
entries_.clear();
|
||||
emitted_.clear();
|
||||
}
|
||||
|
||||
private:
|
||||
static bool equal(const Service& lhs, const Service& rhs)
|
||||
{
|
||||
return lhs.nodeId == rhs.nodeId and lhs.network == rhs.network and lhs.port == rhs.port
|
||||
and lhs.addresses == rhs.addresses;
|
||||
}
|
||||
|
||||
void expire()
|
||||
{
|
||||
const auto now = now_();
|
||||
entries_.erase(std::remove_if(entries_.begin(),
|
||||
entries_.end(),
|
||||
[&](const Entry& entry) { return entry.expires <= now; }),
|
||||
entries_.end());
|
||||
}
|
||||
|
||||
std::vector<Service> collect() const
|
||||
{
|
||||
std::vector<Service> result;
|
||||
for (const auto& entry : entries_) {
|
||||
if (entry.record.type != Type::PTR or entry.record.classCode != CLASS_IN
|
||||
or entry.record.name != serviceName())
|
||||
continue;
|
||||
const auto* ptr = std::get_if<PtrData>(&entry.record.data);
|
||||
if (not ptr)
|
||||
continue;
|
||||
|
||||
Message message;
|
||||
message.response = true;
|
||||
message.answers.push_back(entry.record);
|
||||
for (const auto& additional : entries_) {
|
||||
if (&additional != &entry and additional.record.type != Type::PTR)
|
||||
message.additionals.push_back(additional.record);
|
||||
}
|
||||
const auto service = resolve(message);
|
||||
if (service and std::none_of(result.begin(), result.end(), [&](const Service& existing) {
|
||||
return equal(existing, *service);
|
||||
}))
|
||||
result.push_back(*service);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
Now now_;
|
||||
std::vector<Entry> entries_;
|
||||
std::vector<Service> emitted_;
|
||||
};
|
||||
|
||||
Cache::Cache(Now now)
|
||||
: impl_(std::make_unique<Impl>(std::move(now)))
|
||||
{}
|
||||
|
||||
Cache::~Cache() = default;
|
||||
Cache::Cache(Cache&&) noexcept = default;
|
||||
Cache& Cache::operator=(Cache&&) noexcept = default;
|
||||
|
||||
std::vector<Service>
|
||||
Cache::update(const Message& message)
|
||||
{
|
||||
return impl_->update(message);
|
||||
}
|
||||
|
||||
std::vector<Service>
|
||||
Cache::services()
|
||||
{
|
||||
return impl_->services();
|
||||
}
|
||||
|
||||
void
|
||||
Cache::clear()
|
||||
{
|
||||
impl_->clear();
|
||||
}
|
||||
|
||||
Name
|
||||
serviceName()
|
||||
{
|
||||
return Name::fromString(SERVICE_NAME);
|
||||
}
|
||||
|
||||
Name
|
||||
instanceName(const InfoHash& nodeId)
|
||||
{
|
||||
if (not nodeId)
|
||||
throw ParseError("DNS-SD instance requires a node ID");
|
||||
return Name::fromString(nodeId.toString() + std::string(SERVICE_SUFFIX));
|
||||
}
|
||||
|
||||
Name
|
||||
hostName(const InfoHash& nodeId)
|
||||
{
|
||||
if (not nodeId)
|
||||
throw ParseError("mDNS host requires a node ID");
|
||||
return Name::fromString(nodeId.toString() + std::string(HOST_SUFFIX));
|
||||
}
|
||||
|
||||
Message
|
||||
announcement(const Service& service)
|
||||
{
|
||||
return makeAdvertisement(service, SERVICE_RECORD_TTL, HOST_RECORD_TTL);
|
||||
}
|
||||
|
||||
Message
|
||||
goodbye(const Service& service)
|
||||
{
|
||||
return makeAdvertisement(service, 0, 0);
|
||||
}
|
||||
|
||||
Message
|
||||
browseQuery()
|
||||
{
|
||||
Message query;
|
||||
query.questions.push_back({serviceName(), Type::PTR, CLASS_IN, false});
|
||||
return query;
|
||||
}
|
||||
|
||||
std::optional<Message>
|
||||
respond(const Message& query, const Service& service)
|
||||
{
|
||||
if (query.response)
|
||||
return {};
|
||||
|
||||
const auto advertisement = announcement(service);
|
||||
Message response;
|
||||
response.id = query.id;
|
||||
response.response = true;
|
||||
response.authoritative = true;
|
||||
|
||||
const ResourceRecord serviceType {
|
||||
Name::fromString(SERVICE_ENUMERATION_NAME),
|
||||
Type::PTR,
|
||||
CLASS_IN,
|
||||
false,
|
||||
SERVICE_RECORD_TTL,
|
||||
PtrData {serviceName()},
|
||||
};
|
||||
|
||||
auto addMatching = [&](const ResourceRecord& record) {
|
||||
const auto question = std::find_if(query.questions.begin(), query.questions.end(), [&](const Question& q) {
|
||||
return matches(q, record);
|
||||
});
|
||||
if (question == query.questions.end() or (not question->unicastResponse and isKnownAnswer(query, record)))
|
||||
return;
|
||||
if (std::find_if(response.answers.begin(),
|
||||
response.answers.end(),
|
||||
[&](const ResourceRecord& answer) { return sameRecord(answer, record); })
|
||||
== response.answers.end())
|
||||
response.answers.push_back(record);
|
||||
};
|
||||
|
||||
addMatching(serviceType);
|
||||
for (const auto& record : advertisement.answers)
|
||||
addMatching(record);
|
||||
for (const auto& record : advertisement.additionals)
|
||||
addMatching(record);
|
||||
if (response.answers.empty())
|
||||
return {};
|
||||
|
||||
const auto metaOnly = std::all_of(response.answers.begin(), response.answers.end(), [&](const auto& answer) {
|
||||
return sameRecord(answer, serviceType);
|
||||
});
|
||||
if (metaOnly)
|
||||
return response;
|
||||
|
||||
response.additionals = advertisement.additionals;
|
||||
response.additionals.erase(std::remove_if(response.additionals.begin(),
|
||||
response.additionals.end(),
|
||||
[&](const ResourceRecord& record) {
|
||||
return std::any_of(response.answers.begin(),
|
||||
response.answers.end(),
|
||||
[&](const ResourceRecord& answer) {
|
||||
return sameRecord(answer, record);
|
||||
});
|
||||
}),
|
||||
response.additionals.end());
|
||||
return response;
|
||||
}
|
||||
|
||||
std::optional<Service>
|
||||
resolve(const Message& message)
|
||||
{
|
||||
std::vector<Name> instances;
|
||||
forEachRecord(message, [&](const ResourceRecord& record) {
|
||||
if (record.type == Type::PTR and record.classCode == CLASS_IN and record.name == serviceName()) {
|
||||
if (const auto ptr = std::get_if<PtrData>(&record.data))
|
||||
instances.push_back(ptr->name);
|
||||
}
|
||||
});
|
||||
if (instances.size() != 1)
|
||||
return {};
|
||||
|
||||
const auto nodeId = nodeIdFromInstance(instances.front());
|
||||
if (not nodeId)
|
||||
return {};
|
||||
const auto expectedHost = hostName(*nodeId);
|
||||
const SrvData* srv = nullptr;
|
||||
const TxtData* txt = nullptr;
|
||||
std::vector<AddressData> addresses;
|
||||
auto duplicate = false;
|
||||
|
||||
forEachRecord(message, [&](const ResourceRecord& record) {
|
||||
if (record.classCode != CLASS_IN)
|
||||
return;
|
||||
if (record.name == instances.front()) {
|
||||
if (record.type == Type::SRV) {
|
||||
if (srv)
|
||||
duplicate = true;
|
||||
else
|
||||
srv = std::get_if<SrvData>(&record.data);
|
||||
} else if (record.type == Type::TXT) {
|
||||
if (txt)
|
||||
duplicate = true;
|
||||
else
|
||||
txt = std::get_if<TxtData>(&record.data);
|
||||
}
|
||||
} else if (record.name == expectedHost) {
|
||||
if (record.type == Type::A) {
|
||||
if (const auto data = std::get_if<AData>(&record.data))
|
||||
addresses.emplace_back(*data);
|
||||
} else if (record.type == Type::AAAA) {
|
||||
if (const auto data = std::get_if<AaaaData>(&record.data))
|
||||
addresses.emplace_back(*data);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (duplicate or not srv or not txt or srv->priority != 0 or srv->weight != 0 or srv->port == 0
|
||||
or srv->target != expectedHost or addresses.empty())
|
||||
return {};
|
||||
const auto txtValues = parseTxt(*txt);
|
||||
if (not txtValues or txtValues->nodeId != *nodeId)
|
||||
return {};
|
||||
|
||||
Service service;
|
||||
service.nodeId = *nodeId;
|
||||
service.network = txtValues->network;
|
||||
service.port = srv->port;
|
||||
for (auto& address : addresses) {
|
||||
if (std::find(service.addresses.begin(), service.addresses.end(), address) == service.addresses.end())
|
||||
service.addresses.emplace_back(std::move(address));
|
||||
}
|
||||
return service;
|
||||
}
|
||||
|
||||
} // namespace dht::mdns::dns_sd
|
||||
@@ -0,0 +1,61 @@
|
||||
// Copyright (c) 2014-2026 Savoir-faire Linux Inc.
|
||||
// SPDX-License-Identifier: MIT
|
||||
#pragma once
|
||||
|
||||
#include "mdns.h"
|
||||
|
||||
#include "opendht/infohash.h"
|
||||
#include "opendht/utils.h"
|
||||
|
||||
#include <chrono>
|
||||
#include <functional>
|
||||
#include <memory>
|
||||
#include <optional>
|
||||
|
||||
namespace dht::mdns::dns_sd {
|
||||
|
||||
constexpr uint32_t HOST_RECORD_TTL = 120;
|
||||
constexpr uint32_t SERVICE_RECORD_TTL = 4500;
|
||||
|
||||
using AddressData = std::variant<AData, AaaaData>;
|
||||
|
||||
struct Service
|
||||
{
|
||||
InfoHash nodeId;
|
||||
NetId network {};
|
||||
in_port_t port {};
|
||||
std::vector<AddressData> addresses;
|
||||
};
|
||||
|
||||
class OPENDHT_PUBLIC Cache
|
||||
{
|
||||
public:
|
||||
using Clock = std::chrono::steady_clock;
|
||||
using Now = std::function<Clock::time_point()>;
|
||||
|
||||
explicit Cache(Now now = [] { return Clock::now(); });
|
||||
~Cache();
|
||||
|
||||
Cache(Cache&&) noexcept;
|
||||
Cache& operator=(Cache&&) noexcept;
|
||||
|
||||
std::vector<Service> update(const Message& message);
|
||||
std::vector<Service> services();
|
||||
void clear();
|
||||
|
||||
private:
|
||||
class Impl;
|
||||
std::unique_ptr<Impl> impl_;
|
||||
};
|
||||
|
||||
OPENDHT_PUBLIC Name serviceName();
|
||||
OPENDHT_PUBLIC Name instanceName(const InfoHash& nodeId);
|
||||
OPENDHT_PUBLIC Name hostName(const InfoHash& nodeId);
|
||||
|
||||
OPENDHT_PUBLIC Message announcement(const Service& service);
|
||||
OPENDHT_PUBLIC Message goodbye(const Service& service);
|
||||
OPENDHT_PUBLIC Message browseQuery();
|
||||
OPENDHT_PUBLIC std::optional<Message> respond(const Message& query, const Service& service);
|
||||
OPENDHT_PUBLIC std::optional<Service> resolve(const Message& message);
|
||||
|
||||
} // namespace dht::mdns::dns_sd
|
||||
@@ -0,0 +1,563 @@
|
||||
// Copyright (c) 2014-2026 Savoir-faire Linux Inc.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
#include "mdns.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cctype>
|
||||
#include <limits>
|
||||
#include <map>
|
||||
#include <unordered_map>
|
||||
|
||||
namespace dht::mdns {
|
||||
namespace {
|
||||
|
||||
constexpr uint16_t FLAG_RESPONSE = 0x8000;
|
||||
constexpr uint16_t FLAG_AUTHORITATIVE = 0x0400;
|
||||
constexpr uint16_t FLAG_TRUNCATED = 0x0200;
|
||||
constexpr uint16_t CLASS_TOP_BIT = 0x8000;
|
||||
constexpr uint16_t CLASS_MASK = 0x7fff;
|
||||
|
||||
char
|
||||
asciiLower(char value)
|
||||
{
|
||||
const auto byte = static_cast<unsigned char>(value);
|
||||
return static_cast<char>(std::tolower(byte));
|
||||
}
|
||||
|
||||
void
|
||||
validateLabels(const std::vector<std::string>& labels)
|
||||
{
|
||||
size_t wireSize = 0;
|
||||
for (const auto& label : labels) {
|
||||
if (label.empty() or label.size() > 63)
|
||||
throw ParseError("invalid DNS label length");
|
||||
wireSize += label.size() + 1;
|
||||
if (wireSize > 255)
|
||||
throw ParseError("DNS name is too long");
|
||||
}
|
||||
}
|
||||
|
||||
std::string
|
||||
suffixKey(const std::vector<std::string>& labels, size_t first)
|
||||
{
|
||||
std::string key;
|
||||
for (auto index = first; index < labels.size(); ++index) {
|
||||
key.push_back(static_cast<char>(labels[index].size()));
|
||||
key.append(labels[index]);
|
||||
}
|
||||
key.push_back('\0');
|
||||
return key;
|
||||
}
|
||||
|
||||
class Writer
|
||||
{
|
||||
public:
|
||||
void write8(uint8_t value)
|
||||
{
|
||||
ensure(1);
|
||||
packet_.push_back(value);
|
||||
}
|
||||
|
||||
void write16(uint16_t value)
|
||||
{
|
||||
ensure(2);
|
||||
packet_.push_back(static_cast<uint8_t>(value >> 8));
|
||||
packet_.push_back(static_cast<uint8_t>(value));
|
||||
}
|
||||
|
||||
void write32(uint32_t value)
|
||||
{
|
||||
write16(static_cast<uint16_t>(value >> 16));
|
||||
write16(static_cast<uint16_t>(value));
|
||||
}
|
||||
|
||||
template<size_t Size>
|
||||
void write(const std::array<uint8_t, Size>& bytes)
|
||||
{
|
||||
write(bytes.data(), bytes.size());
|
||||
}
|
||||
|
||||
void write(const uint8_t* bytes, size_t size)
|
||||
{
|
||||
ensure(size);
|
||||
packet_.insert(packet_.end(), bytes, bytes + size);
|
||||
}
|
||||
|
||||
void writeName(const Name& name)
|
||||
{
|
||||
const auto& labels = name.labels();
|
||||
for (size_t index = 0; index < labels.size(); ++index) {
|
||||
const auto key = suffixKey(labels, index);
|
||||
if (const auto found = suffixes_.find(key); found != suffixes_.end()) {
|
||||
write16(static_cast<uint16_t>(0xc000 | found->second));
|
||||
return;
|
||||
}
|
||||
if (packet_.size() < 0x4000)
|
||||
suffixes_.emplace(key, static_cast<uint16_t>(packet_.size()));
|
||||
write8(static_cast<uint8_t>(labels[index].size()));
|
||||
write(reinterpret_cast<const uint8_t*>(labels[index].data()), labels[index].size());
|
||||
}
|
||||
write8(0);
|
||||
}
|
||||
|
||||
size_t size() const noexcept { return packet_.size(); }
|
||||
|
||||
void patch16(size_t offset, uint16_t value)
|
||||
{
|
||||
if (offset + 2 > packet_.size())
|
||||
throw ParseError("invalid DNS packet patch offset");
|
||||
packet_[offset] = static_cast<uint8_t>(value >> 8);
|
||||
packet_[offset + 1] = static_cast<uint8_t>(value);
|
||||
}
|
||||
|
||||
Packet take() { return std::move(packet_); }
|
||||
|
||||
private:
|
||||
void ensure(size_t additional) const
|
||||
{
|
||||
if (additional > MAX_PACKET_SIZE - packet_.size())
|
||||
throw ParseError("mDNS packet exceeds maximum size");
|
||||
}
|
||||
|
||||
Packet packet_;
|
||||
std::unordered_map<std::string, uint16_t> suffixes_;
|
||||
};
|
||||
|
||||
class Reader
|
||||
{
|
||||
public:
|
||||
Reader(const uint8_t* packet, size_t size)
|
||||
: packet_(packet)
|
||||
, size_(size)
|
||||
{}
|
||||
|
||||
uint8_t read8()
|
||||
{
|
||||
require(1);
|
||||
return packet_[position_++];
|
||||
}
|
||||
|
||||
uint16_t read16()
|
||||
{
|
||||
const auto high = read8();
|
||||
const auto low = read8();
|
||||
return static_cast<uint16_t>((high << 8) | low);
|
||||
}
|
||||
|
||||
uint32_t read32() { return (static_cast<uint32_t>(read16()) << 16) | read16(); }
|
||||
|
||||
Name readName()
|
||||
{
|
||||
std::vector<std::string> labels;
|
||||
std::vector<size_t> visited;
|
||||
auto cursor = position_;
|
||||
auto jumped = false;
|
||||
size_t wireSize = 0;
|
||||
|
||||
while (true) {
|
||||
if (cursor >= size_)
|
||||
throw ParseError("truncated DNS name");
|
||||
if (std::find(visited.begin(), visited.end(), cursor) != visited.end())
|
||||
throw ParseError("DNS compression pointer loop");
|
||||
visited.push_back(cursor);
|
||||
|
||||
const auto length = packet_[cursor];
|
||||
if ((length & 0xc0) == 0xc0) {
|
||||
if (cursor + 1 >= size_)
|
||||
throw ParseError("truncated DNS compression pointer");
|
||||
const auto pointer = static_cast<size_t>(((length & 0x3f) << 8) | packet_[cursor + 1]);
|
||||
if (pointer >= cursor)
|
||||
throw ParseError("DNS compression pointer does not point backwards");
|
||||
if (not jumped)
|
||||
position_ = cursor + 2;
|
||||
cursor = pointer;
|
||||
jumped = true;
|
||||
continue;
|
||||
}
|
||||
if (length & 0xc0)
|
||||
throw ParseError("invalid DNS label prefix");
|
||||
|
||||
++cursor;
|
||||
if (length == 0) {
|
||||
if (not jumped)
|
||||
position_ = cursor;
|
||||
break;
|
||||
}
|
||||
if (length > 63 or length > size_ - cursor)
|
||||
throw ParseError("invalid or truncated DNS label");
|
||||
wireSize += length + 1;
|
||||
if (wireSize > 255)
|
||||
throw ParseError("DNS name is too long");
|
||||
labels.emplace_back(reinterpret_cast<const char*>(packet_ + cursor), length);
|
||||
cursor += length;
|
||||
if (not jumped)
|
||||
position_ = cursor;
|
||||
}
|
||||
return Name {std::move(labels)};
|
||||
}
|
||||
|
||||
std::vector<uint8_t> readBytes(size_t size)
|
||||
{
|
||||
require(size);
|
||||
std::vector<uint8_t> bytes(packet_ + position_, packet_ + position_ + size);
|
||||
position_ += size;
|
||||
return bytes;
|
||||
}
|
||||
|
||||
size_t position() const noexcept { return position_; }
|
||||
void setPosition(size_t position)
|
||||
{
|
||||
if (position > size_)
|
||||
throw ParseError("invalid DNS packet position");
|
||||
position_ = position;
|
||||
}
|
||||
|
||||
private:
|
||||
void require(size_t count) const
|
||||
{
|
||||
if (count > size_ - position_)
|
||||
throw ParseError("truncated DNS packet");
|
||||
}
|
||||
|
||||
const uint8_t* packet_;
|
||||
size_t size_;
|
||||
size_t position_ {};
|
||||
};
|
||||
|
||||
uint16_t
|
||||
checkedCount(size_t count)
|
||||
{
|
||||
if (count > std::numeric_limits<uint16_t>::max())
|
||||
throw ParseError("too many DNS records");
|
||||
return static_cast<uint16_t>(count);
|
||||
}
|
||||
|
||||
void
|
||||
encodeQuestion(Writer& writer, const Question& question)
|
||||
{
|
||||
writer.writeName(question.name);
|
||||
writer.write16(static_cast<uint16_t>(question.type));
|
||||
writer.write16(static_cast<uint16_t>(question.classCode | (question.unicastResponse ? CLASS_TOP_BIT : 0)));
|
||||
}
|
||||
|
||||
void
|
||||
encodeNsec(Writer& writer, const NsecData& nsec)
|
||||
{
|
||||
writer.writeName(nsec.nextDomain);
|
||||
std::map<uint8_t, std::array<uint8_t, 32>> windows;
|
||||
std::map<uint8_t, uint8_t> lengths;
|
||||
for (const auto type : nsec.types) {
|
||||
const auto value = static_cast<uint16_t>(type);
|
||||
const auto window = static_cast<uint8_t>(value >> 8);
|
||||
const auto low = static_cast<uint8_t>(value);
|
||||
const auto byte = static_cast<uint8_t>(low / 8);
|
||||
const auto bit = static_cast<uint8_t>(low % 8);
|
||||
windows[window][byte] |= static_cast<uint8_t>(1u << (7 - bit));
|
||||
lengths[window] = std::max<uint8_t>(lengths[window], byte + 1);
|
||||
}
|
||||
for (const auto& [window, bitmap] : windows) {
|
||||
writer.write8(window);
|
||||
writer.write8(lengths[window]);
|
||||
writer.write(bitmap.data(), lengths[window]);
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
encodeRecord(Writer& writer, const ResourceRecord& record)
|
||||
{
|
||||
writer.writeName(record.name);
|
||||
writer.write16(static_cast<uint16_t>(record.type));
|
||||
writer.write16(static_cast<uint16_t>(record.classCode | (record.cacheFlush ? CLASS_TOP_BIT : 0)));
|
||||
writer.write32(record.ttl);
|
||||
const auto lengthOffset = writer.size();
|
||||
writer.write16(0);
|
||||
const auto dataOffset = writer.size();
|
||||
|
||||
switch (record.type) {
|
||||
case Type::A:
|
||||
writer.write(std::get<AData>(record.data).address);
|
||||
break;
|
||||
case Type::AAAA:
|
||||
writer.write(std::get<AaaaData>(record.data).address);
|
||||
break;
|
||||
case Type::PTR:
|
||||
writer.writeName(std::get<PtrData>(record.data).name);
|
||||
break;
|
||||
case Type::TXT:
|
||||
for (const auto& string : std::get<TxtData>(record.data).strings) {
|
||||
if (string.size() > 255)
|
||||
throw ParseError("DNS TXT string is too long");
|
||||
writer.write8(static_cast<uint8_t>(string.size()));
|
||||
writer.write(string.data(), string.size());
|
||||
}
|
||||
break;
|
||||
case Type::SRV: {
|
||||
const auto& srv = std::get<SrvData>(record.data);
|
||||
writer.write16(srv.priority);
|
||||
writer.write16(srv.weight);
|
||||
writer.write16(srv.port);
|
||||
writer.writeName(srv.target);
|
||||
break;
|
||||
}
|
||||
case Type::NSEC:
|
||||
encodeNsec(writer, std::get<NsecData>(record.data));
|
||||
break;
|
||||
default: {
|
||||
const auto& bytes = std::get<UnknownData>(record.data).bytes;
|
||||
writer.write(bytes.data(), bytes.size());
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
const auto dataSize = writer.size() - dataOffset;
|
||||
if (dataSize > std::numeric_limits<uint16_t>::max())
|
||||
throw ParseError("DNS record data is too large");
|
||||
writer.patch16(lengthOffset, static_cast<uint16_t>(dataSize));
|
||||
}
|
||||
|
||||
Question
|
||||
decodeQuestion(Reader& reader)
|
||||
{
|
||||
Question question;
|
||||
question.name = reader.readName();
|
||||
question.type = static_cast<Type>(reader.read16());
|
||||
const auto classField = reader.read16();
|
||||
question.classCode = classField & CLASS_MASK;
|
||||
question.unicastResponse = classField & CLASS_TOP_BIT;
|
||||
return question;
|
||||
}
|
||||
|
||||
NsecData
|
||||
decodeNsec(Reader& reader, size_t end)
|
||||
{
|
||||
NsecData nsec;
|
||||
nsec.nextDomain = reader.readName();
|
||||
while (reader.position() < end) {
|
||||
const auto window = reader.read8();
|
||||
const auto length = reader.read8();
|
||||
if (length == 0 or length > 32 or length > end - reader.position())
|
||||
throw ParseError("invalid DNS NSEC bitmap");
|
||||
const auto bitmap = reader.readBytes(length);
|
||||
for (size_t byte = 0; byte < bitmap.size(); ++byte) {
|
||||
for (uint8_t bit = 0; bit < 8; ++bit) {
|
||||
if (bitmap[byte] & (1u << (7 - bit))) {
|
||||
const auto value = static_cast<uint16_t>((window << 8) | (byte * 8 + bit));
|
||||
nsec.types.push_back(static_cast<Type>(value));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return nsec;
|
||||
}
|
||||
|
||||
ResourceRecord
|
||||
decodeRecord(Reader& reader)
|
||||
{
|
||||
ResourceRecord record;
|
||||
record.name = reader.readName();
|
||||
record.type = static_cast<Type>(reader.read16());
|
||||
const auto classField = reader.read16();
|
||||
record.classCode = classField & CLASS_MASK;
|
||||
record.cacheFlush = classField & CLASS_TOP_BIT;
|
||||
record.ttl = reader.read32();
|
||||
const auto dataLength = reader.read16();
|
||||
const auto dataStart = reader.position();
|
||||
const auto dataEnd = dataStart + dataLength;
|
||||
if (dataEnd < dataStart)
|
||||
throw ParseError("invalid DNS record length");
|
||||
|
||||
switch (record.type) {
|
||||
case Type::A: {
|
||||
if (dataLength != 4)
|
||||
throw ParseError("invalid DNS A record length");
|
||||
AData data;
|
||||
const auto bytes = reader.readBytes(data.address.size());
|
||||
std::copy(bytes.begin(), bytes.end(), data.address.begin());
|
||||
record.data = data;
|
||||
break;
|
||||
}
|
||||
case Type::AAAA: {
|
||||
if (dataLength != 16)
|
||||
throw ParseError("invalid DNS AAAA record length");
|
||||
AaaaData data;
|
||||
const auto bytes = reader.readBytes(data.address.size());
|
||||
std::copy(bytes.begin(), bytes.end(), data.address.begin());
|
||||
record.data = data;
|
||||
break;
|
||||
}
|
||||
case Type::PTR:
|
||||
record.data = PtrData {reader.readName()};
|
||||
break;
|
||||
case Type::TXT: {
|
||||
TxtData data;
|
||||
while (reader.position() < dataEnd) {
|
||||
const auto length = reader.read8();
|
||||
if (length > dataEnd - reader.position())
|
||||
throw ParseError("truncated DNS TXT string");
|
||||
data.strings.push_back(reader.readBytes(length));
|
||||
}
|
||||
record.data = std::move(data);
|
||||
break;
|
||||
}
|
||||
case Type::SRV: {
|
||||
if (dataLength < 7)
|
||||
throw ParseError("invalid DNS SRV record length");
|
||||
SrvData data;
|
||||
data.priority = reader.read16();
|
||||
data.weight = reader.read16();
|
||||
data.port = reader.read16();
|
||||
data.target = reader.readName();
|
||||
record.data = std::move(data);
|
||||
break;
|
||||
}
|
||||
case Type::NSEC:
|
||||
record.data = decodeNsec(reader, dataEnd);
|
||||
break;
|
||||
default:
|
||||
record.data = UnknownData {reader.readBytes(dataLength)};
|
||||
break;
|
||||
}
|
||||
|
||||
if (reader.position() != dataEnd)
|
||||
throw ParseError("DNS record data length mismatch");
|
||||
return record;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
Name::Name(std::vector<std::string> labels)
|
||||
: labels_(std::move(labels))
|
||||
{
|
||||
validateLabels(labels_);
|
||||
}
|
||||
|
||||
Name
|
||||
Name::fromString(std::string_view name)
|
||||
{
|
||||
if (name.empty() or name == ".")
|
||||
return {};
|
||||
if (name.back() == '.')
|
||||
name.remove_suffix(1);
|
||||
|
||||
std::vector<std::string> labels;
|
||||
size_t start = 0;
|
||||
while (start <= name.size()) {
|
||||
const auto end = name.find('.', start);
|
||||
const auto length = (end == std::string_view::npos ? name.size() : end) - start;
|
||||
if (length == 0)
|
||||
throw ParseError("empty DNS label");
|
||||
labels.emplace_back(name.substr(start, length));
|
||||
if (end == std::string_view::npos)
|
||||
break;
|
||||
start = end + 1;
|
||||
}
|
||||
return Name {std::move(labels)};
|
||||
}
|
||||
|
||||
std::string
|
||||
Name::toString() const
|
||||
{
|
||||
if (labels_.empty())
|
||||
return ".";
|
||||
std::string name;
|
||||
for (const auto& label : labels_) {
|
||||
name.append(label);
|
||||
name.push_back('.');
|
||||
}
|
||||
return name;
|
||||
}
|
||||
|
||||
bool
|
||||
operator==(const Name& lhs, const Name& rhs) noexcept
|
||||
{
|
||||
if (lhs.labels_.size() != rhs.labels_.size())
|
||||
return false;
|
||||
for (size_t index = 0; index < lhs.labels_.size(); ++index) {
|
||||
const auto& left = lhs.labels_[index];
|
||||
const auto& right = rhs.labels_[index];
|
||||
if (left.size() != right.size())
|
||||
return false;
|
||||
if (not std::equal(left.begin(), left.end(), right.begin(), [](char a, char b) {
|
||||
return asciiLower(a) == asciiLower(b);
|
||||
}))
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
Packet
|
||||
encode(const Message& message)
|
||||
{
|
||||
const auto recordCount = message.questions.size() + message.answers.size() + message.authorities.size()
|
||||
+ message.additionals.size();
|
||||
if (recordCount > MAX_RECORD_COUNT)
|
||||
throw ParseError("too many DNS records");
|
||||
|
||||
Writer writer;
|
||||
writer.write16(message.id);
|
||||
uint16_t flags = 0;
|
||||
if (message.response)
|
||||
flags |= FLAG_RESPONSE;
|
||||
if (message.authoritative)
|
||||
flags |= FLAG_AUTHORITATIVE;
|
||||
if (message.truncated)
|
||||
flags |= FLAG_TRUNCATED;
|
||||
writer.write16(flags);
|
||||
writer.write16(checkedCount(message.questions.size()));
|
||||
writer.write16(checkedCount(message.answers.size()));
|
||||
writer.write16(checkedCount(message.authorities.size()));
|
||||
writer.write16(checkedCount(message.additionals.size()));
|
||||
|
||||
for (const auto& question : message.questions)
|
||||
encodeQuestion(writer, question);
|
||||
for (const auto& record : message.answers)
|
||||
encodeRecord(writer, record);
|
||||
for (const auto& record : message.authorities)
|
||||
encodeRecord(writer, record);
|
||||
for (const auto& record : message.additionals)
|
||||
encodeRecord(writer, record);
|
||||
return writer.take();
|
||||
}
|
||||
|
||||
Message
|
||||
decode(const uint8_t* packet, size_t size)
|
||||
{
|
||||
if (packet == nullptr)
|
||||
throw ParseError("null DNS packet");
|
||||
if (size > MAX_PACKET_SIZE)
|
||||
throw ParseError("mDNS packet exceeds maximum size");
|
||||
|
||||
Reader reader(packet, size);
|
||||
Message message;
|
||||
message.id = reader.read16();
|
||||
const auto flags = reader.read16();
|
||||
message.response = flags & FLAG_RESPONSE;
|
||||
message.authoritative = flags & FLAG_AUTHORITATIVE;
|
||||
message.truncated = flags & FLAG_TRUNCATED;
|
||||
const auto questionCount = reader.read16();
|
||||
const auto answerCount = reader.read16();
|
||||
const auto authorityCount = reader.read16();
|
||||
const auto additionalCount = reader.read16();
|
||||
const auto recordCount = static_cast<size_t>(questionCount) + answerCount + authorityCount + additionalCount;
|
||||
if (recordCount > MAX_RECORD_COUNT)
|
||||
throw ParseError("too many DNS records");
|
||||
|
||||
message.questions.reserve(questionCount);
|
||||
message.answers.reserve(answerCount);
|
||||
message.authorities.reserve(authorityCount);
|
||||
message.additionals.reserve(additionalCount);
|
||||
for (size_t index = 0; index < questionCount; ++index)
|
||||
message.questions.push_back(decodeQuestion(reader));
|
||||
for (size_t index = 0; index < answerCount; ++index)
|
||||
message.answers.push_back(decodeRecord(reader));
|
||||
for (size_t index = 0; index < authorityCount; ++index)
|
||||
message.authorities.push_back(decodeRecord(reader));
|
||||
for (size_t index = 0; index < additionalCount; ++index)
|
||||
message.additionals.push_back(decodeRecord(reader));
|
||||
if (reader.position() != size)
|
||||
throw ParseError("trailing bytes in DNS packet");
|
||||
return message;
|
||||
}
|
||||
|
||||
} // namespace dht::mdns
|
||||
@@ -0,0 +1,194 @@
|
||||
// Copyright (c) 2014-2026 Savoir-faire Linux Inc.
|
||||
// SPDX-License-Identifier: MIT
|
||||
#pragma once
|
||||
|
||||
#include "opendht/def.h"
|
||||
|
||||
#include <array>
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <variant>
|
||||
#include <vector>
|
||||
|
||||
namespace dht::mdns {
|
||||
|
||||
constexpr size_t MAX_PACKET_SIZE = 9000;
|
||||
constexpr size_t MAX_RECORD_COUNT = 1024;
|
||||
constexpr uint16_t CLASS_IN = 1;
|
||||
constexpr uint16_t CLASS_ANY = 255;
|
||||
|
||||
enum class Type : uint16_t {
|
||||
A = 1,
|
||||
PTR = 12,
|
||||
TXT = 16,
|
||||
AAAA = 28,
|
||||
SRV = 33,
|
||||
NSEC = 47,
|
||||
ANY = 255,
|
||||
};
|
||||
|
||||
class OPENDHT_PUBLIC ParseError : public std::runtime_error
|
||||
{
|
||||
public:
|
||||
using std::runtime_error::runtime_error;
|
||||
};
|
||||
|
||||
class OPENDHT_PUBLIC Name
|
||||
{
|
||||
public:
|
||||
Name() = default;
|
||||
explicit Name(std::vector<std::string> labels);
|
||||
|
||||
static Name fromString(std::string_view name);
|
||||
|
||||
const std::vector<std::string>& labels() const noexcept { return labels_; }
|
||||
std::string toString() const;
|
||||
|
||||
friend bool operator==(const Name& lhs, const Name& rhs) noexcept;
|
||||
friend bool operator!=(const Name& lhs, const Name& rhs) noexcept { return not(lhs == rhs); }
|
||||
|
||||
private:
|
||||
std::vector<std::string> labels_;
|
||||
};
|
||||
|
||||
OPENDHT_PUBLIC bool operator==(const Name& lhs, const Name& rhs) noexcept;
|
||||
|
||||
struct Question
|
||||
{
|
||||
Name name;
|
||||
Type type {Type::ANY};
|
||||
uint16_t classCode {CLASS_IN};
|
||||
bool unicastResponse {false};
|
||||
};
|
||||
|
||||
struct AData
|
||||
{
|
||||
std::array<uint8_t, 4> address {};
|
||||
};
|
||||
|
||||
struct AaaaData
|
||||
{
|
||||
std::array<uint8_t, 16> address {};
|
||||
};
|
||||
|
||||
struct PtrData
|
||||
{
|
||||
Name name;
|
||||
};
|
||||
|
||||
struct TxtData
|
||||
{
|
||||
std::vector<std::vector<uint8_t>> strings;
|
||||
};
|
||||
|
||||
struct SrvData
|
||||
{
|
||||
uint16_t priority {};
|
||||
uint16_t weight {};
|
||||
uint16_t port {};
|
||||
Name target;
|
||||
};
|
||||
|
||||
struct NsecData
|
||||
{
|
||||
Name nextDomain;
|
||||
std::vector<Type> types;
|
||||
};
|
||||
|
||||
struct UnknownData
|
||||
{
|
||||
std::vector<uint8_t> bytes;
|
||||
};
|
||||
|
||||
using RData = std::variant<AData, AaaaData, PtrData, TxtData, SrvData, NsecData, UnknownData>;
|
||||
|
||||
struct ResourceRecord
|
||||
{
|
||||
Name name;
|
||||
Type type {Type::ANY};
|
||||
uint16_t classCode {CLASS_IN};
|
||||
bool cacheFlush {false};
|
||||
uint32_t ttl {};
|
||||
RData data {UnknownData {}};
|
||||
};
|
||||
|
||||
struct Message
|
||||
{
|
||||
uint16_t id {};
|
||||
bool response {false};
|
||||
bool authoritative {false};
|
||||
bool truncated {false};
|
||||
std::vector<Question> questions;
|
||||
std::vector<ResourceRecord> answers;
|
||||
std::vector<ResourceRecord> authorities;
|
||||
std::vector<ResourceRecord> additionals;
|
||||
};
|
||||
|
||||
using Packet = std::vector<uint8_t>;
|
||||
|
||||
OPENDHT_PUBLIC Packet encode(const Message& message);
|
||||
OPENDHT_PUBLIC Message decode(const uint8_t* packet, size_t size);
|
||||
|
||||
inline bool
|
||||
operator==(const Question& lhs, const Question& rhs)
|
||||
{
|
||||
return lhs.name == rhs.name and lhs.type == rhs.type and lhs.classCode == rhs.classCode
|
||||
and lhs.unicastResponse == rhs.unicastResponse;
|
||||
}
|
||||
|
||||
inline bool
|
||||
operator==(const AData& lhs, const AData& rhs)
|
||||
{
|
||||
return lhs.address == rhs.address;
|
||||
}
|
||||
inline bool
|
||||
operator==(const AaaaData& lhs, const AaaaData& rhs)
|
||||
{
|
||||
return lhs.address == rhs.address;
|
||||
}
|
||||
inline bool
|
||||
operator==(const PtrData& lhs, const PtrData& rhs)
|
||||
{
|
||||
return lhs.name == rhs.name;
|
||||
}
|
||||
inline bool
|
||||
operator==(const TxtData& lhs, const TxtData& rhs)
|
||||
{
|
||||
return lhs.strings == rhs.strings;
|
||||
}
|
||||
inline bool
|
||||
operator==(const SrvData& lhs, const SrvData& rhs)
|
||||
{
|
||||
return lhs.priority == rhs.priority and lhs.weight == rhs.weight and lhs.port == rhs.port
|
||||
and lhs.target == rhs.target;
|
||||
}
|
||||
inline bool
|
||||
operator==(const NsecData& lhs, const NsecData& rhs)
|
||||
{
|
||||
return lhs.nextDomain == rhs.nextDomain and lhs.types == rhs.types;
|
||||
}
|
||||
inline bool
|
||||
operator==(const UnknownData& lhs, const UnknownData& rhs)
|
||||
{
|
||||
return lhs.bytes == rhs.bytes;
|
||||
}
|
||||
|
||||
inline bool
|
||||
operator==(const ResourceRecord& lhs, const ResourceRecord& rhs)
|
||||
{
|
||||
return lhs.name == rhs.name and lhs.type == rhs.type and lhs.classCode == rhs.classCode
|
||||
and lhs.cacheFlush == rhs.cacheFlush and lhs.ttl == rhs.ttl and lhs.data == rhs.data;
|
||||
}
|
||||
|
||||
inline bool
|
||||
operator==(const Message& lhs, const Message& rhs)
|
||||
{
|
||||
return lhs.id == rhs.id and lhs.response == rhs.response and lhs.authoritative == rhs.authoritative
|
||||
and lhs.truncated == rhs.truncated and lhs.questions == rhs.questions and lhs.answers == rhs.answers
|
||||
and lhs.authorities == rhs.authorities and lhs.additionals == rhs.additionals;
|
||||
}
|
||||
|
||||
} // namespace dht::mdns
|
||||
@@ -0,0 +1,455 @@
|
||||
// Copyright (c) 2014-2026 Savoir-faire Linux Inc.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
#include "mdns_transport.h"
|
||||
|
||||
#include <asio.hpp>
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <cerrno>
|
||||
#include <cstring>
|
||||
#include <future>
|
||||
#include <mutex>
|
||||
|
||||
#ifdef _WIN32
|
||||
#include <iphlpapi.h>
|
||||
#else
|
||||
#include <ifaddrs.h>
|
||||
#include <net/if.h>
|
||||
#endif
|
||||
|
||||
namespace dht::mdns {
|
||||
namespace {
|
||||
|
||||
constexpr in_port_t MDNS_PORT = 5353;
|
||||
constexpr std::string_view MDNS_IPV4 = "224.0.0.251";
|
||||
constexpr std::string_view MDNS_IPV6 = "ff02::fb";
|
||||
|
||||
std::vector<InterfaceAddress>
|
||||
interfaceAddresses([[maybe_unused]] asio::io_context& ioContext)
|
||||
{
|
||||
std::vector<InterfaceAddress> result;
|
||||
#ifdef _WIN32
|
||||
ULONG size = 15000;
|
||||
std::vector<uint8_t> buffer(size);
|
||||
auto* adapters = reinterpret_cast<IP_ADAPTER_ADDRESSES*>(buffer.data());
|
||||
auto status = GetAdaptersAddresses(AF_UNSPEC,
|
||||
GAA_FLAG_SKIP_ANYCAST | GAA_FLAG_SKIP_MULTICAST | GAA_FLAG_SKIP_DNS_SERVER,
|
||||
nullptr,
|
||||
adapters,
|
||||
&size);
|
||||
if (status == ERROR_BUFFER_OVERFLOW) {
|
||||
buffer.resize(size);
|
||||
adapters = reinterpret_cast<IP_ADAPTER_ADDRESSES*>(buffer.data());
|
||||
status = GetAdaptersAddresses(AF_UNSPEC,
|
||||
GAA_FLAG_SKIP_ANYCAST | GAA_FLAG_SKIP_MULTICAST | GAA_FLAG_SKIP_DNS_SERVER,
|
||||
nullptr,
|
||||
adapters,
|
||||
&size);
|
||||
}
|
||||
if (status == NO_ERROR) {
|
||||
InterfaceId id = 0;
|
||||
for (auto* adapter = adapters; adapter; adapter = adapter->Next) {
|
||||
if (adapter->OperStatus != IfOperStatusUp)
|
||||
continue;
|
||||
++id;
|
||||
const auto multicast = not(adapter->Flags & IP_ADAPTER_NO_MULTICAST);
|
||||
const auto loopback = adapter->IfType == IF_TYPE_SOFTWARE_LOOPBACK;
|
||||
for (auto* current = adapter->FirstUnicastAddress; current; current = current->Next) {
|
||||
const auto* socketAddress = current->Address.lpSockaddr;
|
||||
if (socketAddress->sa_family == AF_INET) {
|
||||
const auto* address = reinterpret_cast<const sockaddr_in*>(socketAddress);
|
||||
asio::ip::address_v4::bytes_type bytes;
|
||||
std::memcpy(bytes.data(), &address->sin_addr, bytes.size());
|
||||
result.push_back({id, adapter->IfIndex, asio::ip::address_v4(bytes), multicast, loopback});
|
||||
} else if (socketAddress->sa_family == AF_INET6) {
|
||||
const auto* address = reinterpret_cast<const sockaddr_in6*>(socketAddress);
|
||||
asio::ip::address_v6::bytes_type bytes;
|
||||
std::memcpy(bytes.data(), &address->sin6_addr, bytes.size());
|
||||
result.push_back({id,
|
||||
adapter->Ipv6IfIndex,
|
||||
asio::ip::address_v6(bytes, address->sin6_scope_id),
|
||||
multicast,
|
||||
loopback});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
#else
|
||||
ifaddrs* addresses = nullptr;
|
||||
if (getifaddrs(&addresses) != 0)
|
||||
return result;
|
||||
for (auto* current = addresses; current; current = current->ifa_next) {
|
||||
if (not current->ifa_addr or not(current->ifa_flags & IFF_UP))
|
||||
continue;
|
||||
const auto family = current->ifa_addr->sa_family;
|
||||
if (family == AF_INET) {
|
||||
const auto* address = reinterpret_cast<const sockaddr_in*>(current->ifa_addr);
|
||||
asio::ip::address_v4::bytes_type bytes;
|
||||
std::memcpy(bytes.data(), &address->sin_addr, bytes.size());
|
||||
const auto index = if_nametoindex(current->ifa_name);
|
||||
result.push_back({index,
|
||||
index,
|
||||
asio::ip::address_v4(bytes),
|
||||
bool(current->ifa_flags & IFF_MULTICAST),
|
||||
bool(current->ifa_flags & IFF_LOOPBACK)});
|
||||
} else if (family == AF_INET6) {
|
||||
const auto* address = reinterpret_cast<const sockaddr_in6*>(current->ifa_addr);
|
||||
asio::ip::address_v6::bytes_type bytes;
|
||||
std::memcpy(bytes.data(), &address->sin6_addr, bytes.size());
|
||||
const auto index = if_nametoindex(current->ifa_name);
|
||||
result.push_back({index,
|
||||
index,
|
||||
asio::ip::address_v6(bytes, address->sin6_scope_id),
|
||||
bool(current->ifa_flags & IFF_MULTICAST),
|
||||
bool(current->ifa_flags & IFF_LOOPBACK)});
|
||||
}
|
||||
}
|
||||
freeifaddrs(addresses);
|
||||
#endif
|
||||
return result;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
std::vector<NetworkInterface>
|
||||
groupInterfaces(const std::vector<InterfaceAddress>& addresses)
|
||||
{
|
||||
std::vector<NetworkInterface> result;
|
||||
const auto hasNonLoopback = std::any_of(addresses.begin(), addresses.end(), [](const InterfaceAddress& address) {
|
||||
return address.multicast and not address.loopback and not address.address.is_unspecified()
|
||||
and not address.address.is_multicast();
|
||||
});
|
||||
|
||||
for (const auto& address : addresses) {
|
||||
if (address.id == 0 or not address.multicast or (hasNonLoopback and address.loopback)
|
||||
or address.address.is_unspecified() or address.address.is_multicast())
|
||||
continue;
|
||||
|
||||
auto scope = std::find_if(result.begin(), result.end(), [&](const NetworkInterface& interface) {
|
||||
return interface.id == address.id;
|
||||
});
|
||||
if (scope == result.end()) {
|
||||
NetworkInterface interface;
|
||||
interface.id = address.id;
|
||||
result.push_back(std::move(interface));
|
||||
scope = std::prev(result.end());
|
||||
}
|
||||
|
||||
dns_sd::AddressData record;
|
||||
if (address.address.is_v4()) {
|
||||
scope->ipv4Index = address.index;
|
||||
if (not scope->ipv4Outbound)
|
||||
scope->ipv4Outbound = address.address.to_v4();
|
||||
record = AData {address.address.to_v4().to_bytes()};
|
||||
} else {
|
||||
const auto bytes = address.address.to_v6().to_bytes();
|
||||
const auto mapped = std::all_of(bytes.begin(), bytes.begin() + 10, [](uint8_t byte) { return byte == 0; })
|
||||
and bytes[10] == 0xff and bytes[11] == 0xff;
|
||||
if (mapped)
|
||||
continue;
|
||||
scope->ipv6Index = address.index;
|
||||
record = AaaaData {bytes};
|
||||
}
|
||||
if (std::find(scope->addresses.begin(), scope->addresses.end(), record) == scope->addresses.end())
|
||||
scope->addresses.emplace_back(std::move(record));
|
||||
}
|
||||
result.erase(std::remove_if(result.begin(),
|
||||
result.end(),
|
||||
[](const NetworkInterface& interface) { return interface.addresses.empty(); }),
|
||||
result.end());
|
||||
return result;
|
||||
}
|
||||
|
||||
class MdnsTransport::Impl : public std::enable_shared_from_this<MdnsTransport::Impl>
|
||||
{
|
||||
public:
|
||||
Impl(std::shared_ptr<asio::io_context> ioContext, std::shared_ptr<Logger> logger)
|
||||
: ioContext_(std::move(ioContext))
|
||||
, logger_(std::move(logger))
|
||||
, interfaces_(groupInterfaces(interfaceAddresses(*ioContext_)))
|
||||
{}
|
||||
|
||||
struct Socket
|
||||
{
|
||||
explicit Socket(asio::io_context& ioContext)
|
||||
: socket(ioContext)
|
||||
{}
|
||||
|
||||
asio::ip::udp::socket socket;
|
||||
std::array<uint8_t, MAX_PACKET_SIZE> receiveBuffer {};
|
||||
asio::ip::udp::endpoint sender;
|
||||
asio::ip::udp::endpoint multicast;
|
||||
InterfaceId interface {};
|
||||
unsigned ipv6Index {};
|
||||
};
|
||||
|
||||
void start(ReceiveCallback callback)
|
||||
{
|
||||
callback_ = std::move(callback);
|
||||
configure();
|
||||
}
|
||||
|
||||
void send(Packet packet) { send(std::move(packet), {}, {}); }
|
||||
|
||||
void send(Packet packet, std::optional<asio::ip::udp::endpoint> destination, std::optional<InterfaceId> interface)
|
||||
{
|
||||
auto sharedPacket = std::make_shared<Packet>(std::move(packet));
|
||||
asio::post(*ioContext_, [weak = weak_from_this(), sharedPacket, destination, interface] {
|
||||
const auto self = weak.lock();
|
||||
if (not self)
|
||||
return;
|
||||
for (const auto& state : self->sockets_) {
|
||||
if (interface and state->interface != *interface)
|
||||
continue;
|
||||
const auto target = destination.value_or(state->multicast);
|
||||
if (target.address().is_v4() != state->multicast.address().is_v4())
|
||||
continue;
|
||||
state->socket.async_send_to(asio::buffer(*sharedPacket),
|
||||
target,
|
||||
[logger = self->logger_, sharedPacket](const asio::error_code& error,
|
||||
size_t) {
|
||||
if (error and error != asio::error::operation_aborted and logger)
|
||||
logger->warn("Unable to send mDNS packet: {}", error.message());
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void sendAndWait(Packet packet, InterfaceId interface)
|
||||
{
|
||||
if (ioContext_->get_executor().running_in_this_thread()) {
|
||||
sendNow(packet, interface);
|
||||
return;
|
||||
}
|
||||
auto completed = std::make_shared<std::promise<void>>();
|
||||
auto future = completed->get_future();
|
||||
asio::post(*ioContext_, [weak = weak_from_this(), packet = std::move(packet), interface, completed] {
|
||||
if (const auto self = weak.lock())
|
||||
self->sendNow(packet, interface);
|
||||
completed->set_value();
|
||||
});
|
||||
future.wait_for(std::chrono::milliseconds(250));
|
||||
}
|
||||
|
||||
void refresh()
|
||||
{
|
||||
{
|
||||
std::lock_guard lock(interfacesMutex_);
|
||||
interfaces_ = groupInterfaces(interfaceAddresses(*ioContext_));
|
||||
}
|
||||
asio::post(*ioContext_, [weak = weak_from_this()] {
|
||||
if (const auto self = weak.lock())
|
||||
self->configure();
|
||||
});
|
||||
}
|
||||
|
||||
void stop()
|
||||
{
|
||||
if (ioContext_->get_executor().running_in_this_thread()) {
|
||||
stopNow();
|
||||
return;
|
||||
}
|
||||
auto completed = std::make_shared<std::promise<void>>();
|
||||
auto future = completed->get_future();
|
||||
asio::post(*ioContext_, [weak = weak_from_this(), completed] {
|
||||
if (const auto self = weak.lock())
|
||||
self->stopNow();
|
||||
completed->set_value();
|
||||
});
|
||||
future.wait_for(std::chrono::milliseconds(250));
|
||||
}
|
||||
|
||||
std::vector<NetworkInterface> interfaces() const
|
||||
{
|
||||
std::lock_guard lock(interfacesMutex_);
|
||||
return interfaces_;
|
||||
}
|
||||
|
||||
private:
|
||||
void sendNow(const Packet& packet, InterfaceId interface)
|
||||
{
|
||||
for (const auto& state : sockets_) {
|
||||
if (state->interface != interface)
|
||||
continue;
|
||||
asio::error_code error;
|
||||
state->socket.send_to(asio::buffer(packet), state->multicast, 0, error);
|
||||
if (error and logger_)
|
||||
logger_->warn("Unable to send final mDNS packet: {}", error.message());
|
||||
}
|
||||
}
|
||||
|
||||
void stopNow()
|
||||
{
|
||||
callback_ = {};
|
||||
stopSockets();
|
||||
}
|
||||
|
||||
void configure()
|
||||
{
|
||||
stopSockets();
|
||||
std::vector<NetworkInterface> currentInterfaces;
|
||||
{
|
||||
std::lock_guard lock(interfacesMutex_);
|
||||
currentInterfaces = interfaces_;
|
||||
}
|
||||
for (const auto& interface : currentInterfaces) {
|
||||
if (interface.ipv4Outbound)
|
||||
configure(interface, asio::ip::udp::v4(), asio::ip::make_address(MDNS_IPV4));
|
||||
if (interface.ipv6Index)
|
||||
configure(interface, asio::ip::udp::v6(), asio::ip::make_address(MDNS_IPV6));
|
||||
}
|
||||
}
|
||||
|
||||
void configure(const NetworkInterface& interface, asio::ip::udp protocol, const asio::ip::address& multicast)
|
||||
{
|
||||
auto state = std::make_shared<Socket>(*ioContext_);
|
||||
asio::error_code error;
|
||||
state->socket.open(protocol, error);
|
||||
if (error)
|
||||
return logSocketError(protocol, error);
|
||||
state->socket.set_option(asio::ip::udp::socket::reuse_address(true), error);
|
||||
#if defined(SO_REUSEPORT) && !defined(_WIN32)
|
||||
if (not error) {
|
||||
const int enabled = 1;
|
||||
if (::setsockopt(state->socket.native_handle(), SOL_SOCKET, SO_REUSEPORT, &enabled, sizeof(enabled)) != 0)
|
||||
error.assign(errno, asio::error::get_system_category());
|
||||
}
|
||||
#endif
|
||||
if (error)
|
||||
return logSocketError(protocol, error);
|
||||
if (protocol == asio::ip::udp::v6())
|
||||
state->socket.set_option(asio::ip::v6_only(true), error);
|
||||
state->socket.bind({protocol, MDNS_PORT}, error);
|
||||
if (error)
|
||||
return logSocketError(protocol, error);
|
||||
state->socket.set_option(asio::ip::multicast::hops(255), error);
|
||||
if (error)
|
||||
return logSocketError(protocol, error);
|
||||
state->socket.set_option(asio::ip::multicast::enable_loopback(true), error);
|
||||
if (protocol == asio::ip::udp::v4()) {
|
||||
state->socket.set_option(asio::ip::multicast::join_group(multicast.to_v4(), *interface.ipv4Outbound), error);
|
||||
if (not error)
|
||||
state->socket.set_option(asio::ip::multicast::outbound_interface(*interface.ipv4Outbound), error);
|
||||
} else {
|
||||
state->socket.set_option(asio::ip::multicast::join_group(multicast.to_v6(), interface.ipv6Index), error);
|
||||
if (not error)
|
||||
state->socket.set_option(asio::ip::multicast::outbound_interface(interface.ipv6Index), error);
|
||||
}
|
||||
if (error)
|
||||
return logSocketError(protocol, error);
|
||||
|
||||
if (protocol == asio::ip::udp::v6())
|
||||
state->multicast = {asio::ip::address_v6(multicast.to_v6().to_bytes(), interface.ipv6Index), MDNS_PORT};
|
||||
else
|
||||
state->multicast = {multicast, MDNS_PORT};
|
||||
state->interface = interface.id;
|
||||
state->ipv6Index = interface.ipv6Index;
|
||||
sockets_.push_back(state);
|
||||
receive(state);
|
||||
}
|
||||
|
||||
void receive(const std::shared_ptr<Socket>& state)
|
||||
{
|
||||
state->socket.async_receive_from(asio::buffer(state->receiveBuffer),
|
||||
state->sender,
|
||||
[weak = weak_from_this(), state](const asio::error_code& error, size_t size) {
|
||||
const auto self = weak.lock();
|
||||
if (not self)
|
||||
return;
|
||||
if (not error and self->callback_) {
|
||||
Packet packet(state->receiveBuffer.begin(),
|
||||
state->receiveBuffer.begin() + size);
|
||||
self->callback_(packet,
|
||||
{state->sender, state->interface, state->ipv6Index});
|
||||
} else if (error != asio::error::operation_aborted and self->logger_) {
|
||||
self->logger_->warn("Unable to receive mDNS packet: {}",
|
||||
error.message());
|
||||
}
|
||||
if (state->socket.is_open())
|
||||
self->receive(state);
|
||||
});
|
||||
}
|
||||
|
||||
void stopSockets()
|
||||
{
|
||||
for (const auto& state : sockets_) {
|
||||
asio::error_code error;
|
||||
state->socket.cancel(error);
|
||||
state->socket.close(error);
|
||||
}
|
||||
sockets_.clear();
|
||||
}
|
||||
|
||||
void logSocketError(asio::ip::udp protocol, const asio::error_code& error)
|
||||
{
|
||||
if (logger_)
|
||||
logger_->warn("Unable to start {} mDNS transport: {}",
|
||||
protocol == asio::ip::udp::v4() ? "IPv4" : "IPv6",
|
||||
error.message());
|
||||
}
|
||||
|
||||
std::shared_ptr<asio::io_context> ioContext_;
|
||||
std::shared_ptr<Logger> logger_;
|
||||
mutable std::mutex interfacesMutex_;
|
||||
std::vector<NetworkInterface> interfaces_;
|
||||
std::vector<std::shared_ptr<Socket>> sockets_;
|
||||
ReceiveCallback callback_;
|
||||
};
|
||||
|
||||
MdnsTransport::MdnsTransport(std::shared_ptr<asio::io_context> ioContext, std::shared_ptr<Logger> logger)
|
||||
: impl_(std::make_shared<Impl>(std::move(ioContext), std::move(logger)))
|
||||
{}
|
||||
|
||||
MdnsTransport::~MdnsTransport() = default;
|
||||
|
||||
void
|
||||
MdnsTransport::start(ReceiveCallback callback)
|
||||
{
|
||||
impl_->start(std::move(callback));
|
||||
}
|
||||
|
||||
void
|
||||
MdnsTransport::send(Packet packet)
|
||||
{
|
||||
impl_->send(std::move(packet));
|
||||
}
|
||||
|
||||
void
|
||||
MdnsTransport::send(Packet packet, InterfaceId interface)
|
||||
{
|
||||
impl_->send(std::move(packet), {}, interface);
|
||||
}
|
||||
|
||||
void
|
||||
MdnsTransport::send(Packet packet, const asio::ip::udp::endpoint& destination, InterfaceId interface)
|
||||
{
|
||||
impl_->send(std::move(packet), destination, interface);
|
||||
}
|
||||
|
||||
void
|
||||
MdnsTransport::sendAndWait(Packet packet, InterfaceId interface)
|
||||
{
|
||||
impl_->sendAndWait(std::move(packet), interface);
|
||||
}
|
||||
|
||||
void
|
||||
MdnsTransport::refresh()
|
||||
{
|
||||
impl_->refresh();
|
||||
}
|
||||
|
||||
void
|
||||
MdnsTransport::stop()
|
||||
{
|
||||
if (impl_)
|
||||
impl_->stop();
|
||||
}
|
||||
|
||||
std::vector<NetworkInterface>
|
||||
MdnsTransport::interfaces() const
|
||||
{
|
||||
return impl_->interfaces();
|
||||
}
|
||||
|
||||
} // namespace dht::mdns
|
||||
@@ -0,0 +1,74 @@
|
||||
// Copyright (c) 2014-2026 Savoir-faire Linux Inc.
|
||||
// SPDX-License-Identifier: MIT
|
||||
#pragma once
|
||||
|
||||
#include "dns_sd.h"
|
||||
|
||||
#include "opendht/logger.h"
|
||||
|
||||
#include <asio/ip/address.hpp>
|
||||
#include <asio/ip/udp.hpp>
|
||||
#include <cstdint>
|
||||
#include <functional>
|
||||
#include <memory>
|
||||
#include <optional>
|
||||
|
||||
namespace asio {
|
||||
class io_context;
|
||||
}
|
||||
|
||||
namespace dht::mdns {
|
||||
|
||||
using InterfaceId = uint64_t;
|
||||
|
||||
struct InterfaceAddress
|
||||
{
|
||||
InterfaceId id {};
|
||||
unsigned index {};
|
||||
asio::ip::address address;
|
||||
bool multicast {true};
|
||||
bool loopback {false};
|
||||
};
|
||||
|
||||
struct NetworkInterface
|
||||
{
|
||||
InterfaceId id {};
|
||||
unsigned ipv4Index {};
|
||||
unsigned ipv6Index {};
|
||||
std::optional<asio::ip::address_v4> ipv4Outbound;
|
||||
std::vector<dns_sd::AddressData> addresses;
|
||||
};
|
||||
|
||||
OPENDHT_PUBLIC std::vector<NetworkInterface> groupInterfaces(const std::vector<InterfaceAddress>& addresses);
|
||||
|
||||
struct ReceiveContext
|
||||
{
|
||||
asio::ip::udp::endpoint source;
|
||||
InterfaceId interface {};
|
||||
unsigned ipv6Index {};
|
||||
};
|
||||
|
||||
class MdnsTransport
|
||||
{
|
||||
public:
|
||||
using ReceiveCallback = std::function<void(const Packet&, const ReceiveContext&)>;
|
||||
|
||||
MdnsTransport(std::shared_ptr<asio::io_context> ioContext, std::shared_ptr<Logger> logger = {});
|
||||
~MdnsTransport();
|
||||
|
||||
void start(ReceiveCallback callback);
|
||||
void send(Packet packet);
|
||||
void send(Packet packet, InterfaceId interface);
|
||||
void send(Packet packet, const asio::ip::udp::endpoint& destination, InterfaceId interface);
|
||||
void sendAndWait(Packet packet, InterfaceId interface);
|
||||
void refresh();
|
||||
void stop();
|
||||
|
||||
std::vector<NetworkInterface> interfaces() const;
|
||||
|
||||
private:
|
||||
class Impl;
|
||||
std::shared_ptr<Impl> impl_;
|
||||
};
|
||||
|
||||
} // namespace dht::mdns
|
||||
@@ -0,0 +1,438 @@
|
||||
// Copyright (c) 2014-2026 Savoir-faire Linux Inc.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
#include "peer_discovery.h"
|
||||
|
||||
#include "dns_sd.h"
|
||||
#include "mdns_transport.h"
|
||||
|
||||
#include <asio.hpp>
|
||||
#include <chrono>
|
||||
#include <cstring>
|
||||
#include <map>
|
||||
#include <mutex>
|
||||
#include <optional>
|
||||
#include <random>
|
||||
|
||||
namespace dht {
|
||||
namespace {
|
||||
|
||||
constexpr auto BROWSE_INTERVAL = std::chrono::seconds(60);
|
||||
constexpr auto CONNECTIVITY_RETRY = std::chrono::seconds(10);
|
||||
constexpr auto CONNECTIVITY_RETRY_MAX = std::chrono::minutes(1);
|
||||
|
||||
SockAddr
|
||||
socketAddress(const mdns::dns_sd::AddressData& data, in_port_t port, const mdns::ReceiveContext& context)
|
||||
{
|
||||
SockAddr address;
|
||||
std::visit(
|
||||
[&](const auto& record) {
|
||||
using Record = std::decay_t<decltype(record)>;
|
||||
if constexpr (std::is_same_v<Record, mdns::AData>) {
|
||||
address.setFamily(AF_INET);
|
||||
std::memcpy(&address.getIPv4().sin_addr, record.address.data(), record.address.size());
|
||||
} else {
|
||||
address.setFamily(AF_INET6);
|
||||
std::memcpy(&address.getIPv6().sin6_addr, record.address.data(), record.address.size());
|
||||
const asio::ip::address_v6 parsed(record.address);
|
||||
if (parsed.is_link_local())
|
||||
address.getIPv6().sin6_scope_id = context.ipv6Index;
|
||||
}
|
||||
},
|
||||
data);
|
||||
address.setPort(port);
|
||||
return address;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
class PeerDiscovery::Impl : public std::enable_shared_from_this<PeerDiscovery::Impl>
|
||||
{
|
||||
public:
|
||||
Impl(std::shared_ptr<asio::io_context> ioContext, std::shared_ptr<Logger> logger)
|
||||
: ioContext_(std::move(ioContext))
|
||||
, logger_(std::move(logger))
|
||||
, transport_(ioContext_, logger_)
|
||||
, browseTimer_(*ioContext_)
|
||||
, announceTimer_(*ioContext_)
|
||||
, connectivityTimer_(*ioContext_)
|
||||
{}
|
||||
|
||||
void start()
|
||||
{
|
||||
transport_.start([weak = weak_from_this()](const mdns::Packet& packet, const mdns::ReceiveContext& context) {
|
||||
if (const auto self = weak.lock())
|
||||
self->receive(packet, context);
|
||||
});
|
||||
if (logger_)
|
||||
logger_->debug("[peer discovery {:p}] Started.", fmt::ptr(this));
|
||||
}
|
||||
|
||||
void startDiscovery(NetId network, PeerDiscoveredCallback callback)
|
||||
{
|
||||
{
|
||||
std::lock_guard lock(mutex_);
|
||||
discoveryNetwork_ = network;
|
||||
discoveryCallback_ = std::move(callback);
|
||||
}
|
||||
asio::post(*ioContext_, [weak = weak_from_this()] {
|
||||
if (const auto self = weak.lock())
|
||||
self->browse();
|
||||
});
|
||||
}
|
||||
|
||||
void startPublish(const InfoHash& nodeId, NetId network, in_port_t port)
|
||||
{
|
||||
mdns::dns_sd::Service service {nodeId, network, port, {}};
|
||||
if (transport_.interfaces().empty())
|
||||
throw mdns::ParseError("DNS-SD service requires a multicast interface");
|
||||
{
|
||||
std::lock_guard lock(mutex_);
|
||||
published_ = std::move(service);
|
||||
}
|
||||
announce();
|
||||
asio::post(*ioContext_, [weak = weak_from_this()] {
|
||||
const auto self = weak.lock();
|
||||
if (not self)
|
||||
return;
|
||||
self->announceTimer_.expires_after(std::chrono::seconds(1));
|
||||
self->announceTimer_.async_wait([weak](const asio::error_code& error) {
|
||||
if (not error) {
|
||||
if (const auto locked = weak.lock())
|
||||
locked->announce();
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
bool stopDiscovery()
|
||||
{
|
||||
std::lock_guard lock(mutex_);
|
||||
const auto stopped = bool(discoveryCallback_);
|
||||
discoveryCallback_ = {};
|
||||
discoveryNetwork_.reset();
|
||||
asio::post(*ioContext_, [weak = weak_from_this()] {
|
||||
if (const auto self = weak.lock())
|
||||
self->browseTimer_.cancel();
|
||||
});
|
||||
return stopped;
|
||||
}
|
||||
|
||||
bool stopPublish()
|
||||
{
|
||||
std::optional<mdns::dns_sd::Service> service;
|
||||
{
|
||||
std::lock_guard lock(mutex_);
|
||||
service = std::exchange(published_, {});
|
||||
}
|
||||
asio::post(*ioContext_, [weak = weak_from_this()] {
|
||||
if (const auto self = weak.lock())
|
||||
self->announceTimer_.cancel();
|
||||
});
|
||||
if (service)
|
||||
advertise(*service, true, false);
|
||||
return bool(service);
|
||||
}
|
||||
|
||||
void stop()
|
||||
{
|
||||
stopConnectivityChanged();
|
||||
stopDiscovery();
|
||||
std::optional<mdns::dns_sd::Service> service;
|
||||
{
|
||||
std::lock_guard lock(mutex_);
|
||||
service = std::exchange(published_, {});
|
||||
}
|
||||
asio::post(*ioContext_, [weak = weak_from_this()] {
|
||||
if (const auto self = weak.lock())
|
||||
self->announceTimer_.cancel();
|
||||
});
|
||||
if (service)
|
||||
advertise(*service, true, true);
|
||||
transport_.stop();
|
||||
}
|
||||
|
||||
void connectivityChanged()
|
||||
{
|
||||
asio::post(*ioContext_, [weak = weak_from_this()] {
|
||||
if (const auto self = weak.lock())
|
||||
self->handleConnectivityChanged();
|
||||
});
|
||||
}
|
||||
|
||||
void stopConnectivityChanged()
|
||||
{
|
||||
asio::post(*ioContext_, [weak = weak_from_this()] {
|
||||
if (const auto self = weak.lock()) {
|
||||
self->connectivityTimer_.cancel();
|
||||
self->connectivityRetry_ = CONNECTIVITY_RETRY;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private:
|
||||
void handleConnectivityChanged()
|
||||
{
|
||||
caches_.clear();
|
||||
transport_.refresh();
|
||||
refreshPublication();
|
||||
browse();
|
||||
|
||||
connectivityTimer_.expires_after(connectivityRetry_);
|
||||
connectivityTimer_.async_wait([weak = weak_from_this()](const asio::error_code& error) {
|
||||
if (not error) {
|
||||
if (const auto self = weak.lock())
|
||||
self->handleConnectivityChanged();
|
||||
}
|
||||
});
|
||||
connectivityRetry_ = std::min(connectivityRetry_ * 2,
|
||||
std::chrono::duration_cast<std::chrono::steady_clock::duration>(
|
||||
CONNECTIVITY_RETRY_MAX));
|
||||
}
|
||||
|
||||
void browse()
|
||||
{
|
||||
bool active;
|
||||
{
|
||||
std::lock_guard lock(mutex_);
|
||||
active = bool(discoveryCallback_);
|
||||
}
|
||||
if (not active)
|
||||
return;
|
||||
if (logger_)
|
||||
logger_->debug("[peer discovery {:p}] Browsing.", fmt::ptr(this));
|
||||
transport_.send(mdns::encode(mdns::dns_sd::browseQuery()));
|
||||
browseTimer_.expires_after(BROWSE_INTERVAL);
|
||||
browseTimer_.async_wait([weak = weak_from_this()](const asio::error_code& error) {
|
||||
if (not error) {
|
||||
if (const auto self = weak.lock())
|
||||
self->browse();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void announce()
|
||||
{
|
||||
std::optional<mdns::dns_sd::Service> service;
|
||||
{
|
||||
std::lock_guard lock(mutex_);
|
||||
service = published_;
|
||||
}
|
||||
if (logger_)
|
||||
logger_->debug("[peer discovery {:p}] Announcing.", fmt::ptr(this));
|
||||
if (service)
|
||||
advertise(*service, false, false);
|
||||
}
|
||||
|
||||
void refreshPublication()
|
||||
{
|
||||
std::optional<mdns::dns_sd::Service> service;
|
||||
{
|
||||
std::lock_guard lock(mutex_);
|
||||
if (published_)
|
||||
service = published_;
|
||||
}
|
||||
if (service)
|
||||
advertise(*service, false, false);
|
||||
}
|
||||
|
||||
void advertise(const mdns::dns_sd::Service& service, bool goodbye, bool wait)
|
||||
{
|
||||
for (const auto& interface : transport_.interfaces()) {
|
||||
auto scoped = service;
|
||||
scoped.addresses = interface.addresses;
|
||||
auto packet = mdns::encode(goodbye ? mdns::dns_sd::goodbye(scoped) : mdns::dns_sd::announcement(scoped));
|
||||
if (wait)
|
||||
transport_.sendAndWait(std::move(packet), interface.id);
|
||||
else
|
||||
transport_.send(std::move(packet), interface.id);
|
||||
}
|
||||
}
|
||||
|
||||
void receive(const mdns::Packet& packet, const mdns::ReceiveContext& context)
|
||||
{
|
||||
if (logger_)
|
||||
logger_->debug("[peer discovery {:p}] Got packet of size {}.", fmt::ptr(this), packet.size());
|
||||
try {
|
||||
const auto message = mdns::decode(packet.data(), packet.size());
|
||||
if (message.response)
|
||||
discover(message, context);
|
||||
else
|
||||
answer(message, context);
|
||||
} catch (const std::exception& error) {
|
||||
if (logger_)
|
||||
logger_->warn("Ignoring invalid mDNS packet: {}", error.what());
|
||||
}
|
||||
}
|
||||
|
||||
void answer(const mdns::Message& query, const mdns::ReceiveContext& context)
|
||||
{
|
||||
std::optional<mdns::dns_sd::Service> service;
|
||||
{
|
||||
std::lock_guard lock(mutex_);
|
||||
service = published_;
|
||||
}
|
||||
if (service) {
|
||||
const auto interfaces = transport_.interfaces();
|
||||
const auto interface = std::find_if(interfaces.begin(), interfaces.end(), [&](const auto& candidate) {
|
||||
return candidate.id == context.interface;
|
||||
});
|
||||
if (interface == interfaces.end())
|
||||
return;
|
||||
service->addresses = interface->addresses;
|
||||
if (auto response = mdns::dns_sd::respond(query, *service)) {
|
||||
const auto unicast = context.source.port() != 5353
|
||||
or std::any_of(query.questions.begin(),
|
||||
query.questions.end(),
|
||||
[](const mdns::Question& question) {
|
||||
return question.unicastResponse;
|
||||
});
|
||||
if (context.source.port() != 5353)
|
||||
response->questions = query.questions;
|
||||
auto packet = mdns::encode(*response);
|
||||
const auto sharedAnswer = std::any_of(response->answers.begin(),
|
||||
response->answers.end(),
|
||||
[](const mdns::ResourceRecord& record) {
|
||||
return not record.cacheFlush;
|
||||
});
|
||||
if (not sharedAnswer) {
|
||||
if (unicast)
|
||||
transport_.send(std::move(packet), context.source, context.interface);
|
||||
else
|
||||
transport_.send(std::move(packet), context.interface);
|
||||
return;
|
||||
}
|
||||
|
||||
static thread_local std::mt19937 random {std::random_device {}()};
|
||||
const auto delay = std::chrono::milliseconds(std::uniform_int_distribution<int> {20, 120}(random));
|
||||
auto timer = std::make_shared<asio::steady_timer>(*ioContext_, delay);
|
||||
timer->async_wait([weak = weak_from_this(), timer, packet = std::move(packet), context, unicast](
|
||||
const asio::error_code& error) mutable {
|
||||
if (error)
|
||||
return;
|
||||
const auto self = weak.lock();
|
||||
if (not self)
|
||||
return;
|
||||
if (unicast)
|
||||
self->transport_.send(std::move(packet), context.source, context.interface);
|
||||
else
|
||||
self->transport_.send(std::move(packet), context.interface);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void discover(const mdns::Message& message, const mdns::ReceiveContext& context)
|
||||
{
|
||||
PeerDiscoveredCallback callback;
|
||||
std::optional<NetId> network;
|
||||
std::optional<InfoHash> localNode;
|
||||
{
|
||||
std::lock_guard lock(mutex_);
|
||||
callback = discoveryCallback_;
|
||||
network = discoveryNetwork_;
|
||||
if (published_)
|
||||
localNode = published_->nodeId;
|
||||
}
|
||||
if (not callback or not network)
|
||||
return;
|
||||
|
||||
const auto cache = caches_.try_emplace(context.interface).first;
|
||||
for (const auto& service : cache->second.update(message)) {
|
||||
if (service.network != *network or (localNode and service.nodeId == *localNode))
|
||||
continue;
|
||||
for (const auto& address : service.addresses)
|
||||
callback(service.nodeId, socketAddress(address, service.port, context));
|
||||
}
|
||||
}
|
||||
|
||||
std::shared_ptr<asio::io_context> ioContext_;
|
||||
std::shared_ptr<Logger> logger_;
|
||||
mdns::MdnsTransport transport_;
|
||||
asio::steady_timer browseTimer_;
|
||||
asio::steady_timer announceTimer_;
|
||||
asio::steady_timer connectivityTimer_;
|
||||
std::chrono::steady_clock::duration connectivityRetry_ {CONNECTIVITY_RETRY};
|
||||
std::mutex mutex_;
|
||||
std::optional<NetId> discoveryNetwork_;
|
||||
PeerDiscoveredCallback discoveryCallback_;
|
||||
std::optional<mdns::dns_sd::Service> published_;
|
||||
std::map<mdns::InterfaceId, mdns::dns_sd::Cache> caches_;
|
||||
};
|
||||
|
||||
PeerDiscovery::PeerDiscovery(std::shared_ptr<asio::io_context> ioContext, std::shared_ptr<Logger> logger)
|
||||
{
|
||||
if (not ioContext) {
|
||||
ioContext = std::make_shared<asio::io_context>();
|
||||
ioContext_ = ioContext;
|
||||
}
|
||||
impl_ = std::make_shared<Impl>(ioContext, logger);
|
||||
impl_->start();
|
||||
|
||||
if (ioContext_) {
|
||||
ioRunner_ = std::thread([logger = std::move(logger), ioContext = std::move(ioContext)] {
|
||||
try {
|
||||
auto work = asio::make_work_guard(*ioContext);
|
||||
ioContext->run();
|
||||
} catch (const std::exception& error) {
|
||||
if (logger)
|
||||
logger->error("Peer discovery event loop failed: {}", error.what());
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
PeerDiscovery::~PeerDiscovery()
|
||||
{
|
||||
stop();
|
||||
impl_.reset();
|
||||
if (ioContext_)
|
||||
ioContext_->stop();
|
||||
if (ioRunner_.joinable())
|
||||
ioRunner_.join();
|
||||
}
|
||||
|
||||
void
|
||||
PeerDiscovery::startDiscovery(NetId network, PeerDiscoveredCallback callback)
|
||||
{
|
||||
impl_->startDiscovery(network, std::move(callback));
|
||||
}
|
||||
|
||||
void
|
||||
PeerDiscovery::startPublish(const InfoHash& nodeId, NetId network, in_port_t port)
|
||||
{
|
||||
impl_->startPublish(nodeId, network, port);
|
||||
}
|
||||
|
||||
bool
|
||||
PeerDiscovery::stopDiscovery()
|
||||
{
|
||||
return impl_->stopDiscovery();
|
||||
}
|
||||
|
||||
bool
|
||||
PeerDiscovery::stopPublish()
|
||||
{
|
||||
return impl_->stopPublish();
|
||||
}
|
||||
|
||||
void
|
||||
PeerDiscovery::stop()
|
||||
{
|
||||
if (impl_)
|
||||
impl_->stop();
|
||||
}
|
||||
|
||||
void
|
||||
PeerDiscovery::connectivityChanged()
|
||||
{
|
||||
impl_->connectivityChanged();
|
||||
}
|
||||
|
||||
void
|
||||
PeerDiscovery::stopConnectivityChanged()
|
||||
{
|
||||
impl_->stopConnectivityChanged();
|
||||
}
|
||||
|
||||
} // namespace dht
|
||||
+18
-158
@@ -65,16 +65,6 @@ struct Dht::SearchNode
|
||||
Sp<Scheduler::Job> refresh {};
|
||||
Sp<Scheduler::Job> cacheExpirationJob {};
|
||||
Sp<net::Request> req {};
|
||||
Sp<Scheduler::Job> syncDeadline {};
|
||||
uint64_t registration {0};
|
||||
std::optional<net::ListenSync> manifest;
|
||||
std::map<std::pair<uint64_t, uint64_t>, net::RequestAnswer> pendingUpdates;
|
||||
uint64_t nextSequence {1};
|
||||
uint64_t watermark {0};
|
||||
size_t pendingBytes {0};
|
||||
bool baselineComplete {false};
|
||||
bool initialized {false};
|
||||
bool legacy {false};
|
||||
Tid socketId {0};
|
||||
CachedListenStatus(ValueStateCallback&& cb, SyncCallback scb, Tid sid)
|
||||
: cache(std::forward<ValueStateCallback>(cb), std::forward<SyncCallback>(scb))
|
||||
@@ -84,12 +74,6 @@ struct Dht::SearchNode
|
||||
CachedListenStatus(const CachedListenStatus&) = delete;
|
||||
~CachedListenStatus()
|
||||
{
|
||||
if (syncDeadline)
|
||||
syncDeadline->cancel();
|
||||
if (refresh)
|
||||
refresh->cancel();
|
||||
if (cacheExpirationJob)
|
||||
cacheExpirationJob->cancel();
|
||||
if (socketId and req and req->node) {
|
||||
req->node->closeSocket(socketId);
|
||||
}
|
||||
@@ -269,130 +253,17 @@ struct Dht::SearchNode
|
||||
getStatus.clear();
|
||||
}
|
||||
|
||||
bool confirmListen(const Sp<Query>& query,
|
||||
const std::optional<net::ListenSync>& manifest,
|
||||
const TypeStore& types,
|
||||
Scheduler& scheduler)
|
||||
void onValues(const Sp<Query>& q, net::RequestAnswer&& answer, const TypeStore& types, Scheduler& scheduler)
|
||||
{
|
||||
auto found = listenStatus.find(query);
|
||||
if (found == listenStatus.end())
|
||||
return false;
|
||||
auto& status = found->second;
|
||||
if (!manifest) {
|
||||
if (status.manifest)
|
||||
return false;
|
||||
status.legacy = true;
|
||||
status.initialized = true;
|
||||
status.pendingUpdates.clear();
|
||||
status.pendingBytes = 0;
|
||||
scheduler.cancel(status.syncDeadline);
|
||||
status.cache.onSynced(true);
|
||||
return true;
|
||||
auto l = listenStatus.find(q);
|
||||
if (l != listenStatus.end()) {
|
||||
auto next = l->second.cache.onValues(answer.values,
|
||||
answer.refreshed_values,
|
||||
answer.expired_values,
|
||||
types,
|
||||
scheduler.time());
|
||||
scheduler.edit(l->second.cacheExpirationJob, next);
|
||||
}
|
||||
if (manifest->registration != status.registration || !manifest->epoch || manifest->snapshotEnd > 4096
|
||||
|| manifest->sequence < manifest->snapshotEnd
|
||||
|| (status.manifest
|
||||
&& (status.manifest->epoch != manifest->epoch || status.manifest->snapshotEnd != manifest->snapshotEnd)))
|
||||
return false;
|
||||
status.legacy = false;
|
||||
status.manifest = manifest;
|
||||
status.watermark = std::max(status.watermark, manifest->sequence);
|
||||
return onValues(query, {}, types, scheduler);
|
||||
}
|
||||
|
||||
bool onValues(const Sp<Query>& query, net::RequestAnswer&& answer, const TypeStore& types, Scheduler& scheduler)
|
||||
{
|
||||
auto found = listenStatus.find(query);
|
||||
if (found == listenStatus.end())
|
||||
return true;
|
||||
auto& status = found->second;
|
||||
auto apply = [&](net::RequestAnswer&& update) {
|
||||
auto next = status.cache.onValues(update.values,
|
||||
update.refreshed_values,
|
||||
update.expired_values,
|
||||
types,
|
||||
scheduler.time());
|
||||
scheduler.edit(status.cacheExpirationJob, next);
|
||||
};
|
||||
if (!answer.listenSync
|
||||
&& (!answer.values.empty() || !answer.refreshed_values.empty() || !answer.expired_values.empty())) {
|
||||
if (status.manifest)
|
||||
return true;
|
||||
status.legacy = true;
|
||||
apply(std::move(answer));
|
||||
return true;
|
||||
}
|
||||
if (answer.listenSync) {
|
||||
auto sync = *answer.listenSync;
|
||||
if (sync.registration != status.registration || (status.manifest && sync.epoch != status.manifest->epoch))
|
||||
return true;
|
||||
if (!sync.epoch || !sync.sequence || sync.snapshotEnd > 4096
|
||||
|| (status.manifest && sync.snapshotEnd != status.manifest->snapshotEnd))
|
||||
return false;
|
||||
if (!answer.fields.empty()
|
||||
|| (sync.sequence <= sync.snapshotEnd
|
||||
&& (answer.values.empty() || !answer.expired_values.empty() || !answer.refreshed_values.empty())))
|
||||
return false;
|
||||
if (sync.sequence < status.nextSequence)
|
||||
return true;
|
||||
auto key = std::make_pair(sync.epoch, sync.sequence);
|
||||
if (!status.pendingUpdates.count(key)) {
|
||||
size_t bytes = 0;
|
||||
for (const auto& value : answer.values)
|
||||
bytes += value->size();
|
||||
bytes += (answer.refreshed_values.size() + answer.expired_values.size()) * sizeof(Value::Id);
|
||||
if (status.pendingUpdates.size() >= 4096 || bytes > 64 * 1024 * 1024 - status.pendingBytes)
|
||||
return false;
|
||||
status.pendingBytes += bytes;
|
||||
status.pendingUpdates.emplace(key, std::move(answer));
|
||||
}
|
||||
}
|
||||
if (!status.manifest)
|
||||
return true;
|
||||
auto epoch = status.manifest->epoch;
|
||||
for (auto pending = status.pendingUpdates.begin(); pending != status.pendingUpdates.end();) {
|
||||
if (pending->first.first != epoch)
|
||||
pending = status.pendingUpdates.erase(pending);
|
||||
else {
|
||||
if (pending->second.listenSync->snapshotEnd != status.manifest->snapshotEnd)
|
||||
return false;
|
||||
status.watermark = std::max(status.watermark, pending->first.second);
|
||||
++pending;
|
||||
}
|
||||
}
|
||||
if (!status.baselineComplete) {
|
||||
size_t values = 0;
|
||||
for (uint64_t sequence = 1; sequence <= status.manifest->snapshotEnd; ++sequence) {
|
||||
auto batch = status.pendingUpdates.find({epoch, sequence});
|
||||
if (batch == status.pendingUpdates.end())
|
||||
return true;
|
||||
values += batch->second.values.size();
|
||||
}
|
||||
if (values > 4096)
|
||||
return false;
|
||||
status.baselineComplete = true;
|
||||
}
|
||||
while (true) {
|
||||
auto pending = status.pendingUpdates.find({epoch, status.nextSequence});
|
||||
if (pending == status.pendingUpdates.end())
|
||||
break;
|
||||
auto update = std::move(pending->second);
|
||||
status.pendingUpdates.erase(pending);
|
||||
++status.nextSequence;
|
||||
apply(std::move(update));
|
||||
}
|
||||
status.pendingBytes = 0;
|
||||
for (const auto& pending : status.pendingUpdates) {
|
||||
for (const auto& value : pending.second.values)
|
||||
status.pendingBytes += value->size();
|
||||
status.pendingBytes += (pending.second.refreshed_values.size() + pending.second.expired_values.size())
|
||||
* sizeof(Value::Id);
|
||||
}
|
||||
status.initialized = status.nextSequence > status.watermark;
|
||||
if (status.initialized)
|
||||
scheduler.cancel(status.syncDeadline);
|
||||
status.cache.onSynced(status.initialized);
|
||||
return true;
|
||||
}
|
||||
|
||||
void onListenSynced(const Sp<Query>& q, bool synced = true, Sp<Scheduler::Job> refreshJob = {})
|
||||
@@ -596,7 +467,7 @@ struct Dht::Search
|
||||
size_t listener_token = 1;
|
||||
|
||||
/* Cache */
|
||||
SearchCache cache {true, true};
|
||||
SearchCache cache;
|
||||
Sp<Scheduler::Job> opExpirationJob;
|
||||
|
||||
~Search()
|
||||
@@ -725,25 +596,16 @@ struct Dht::Search
|
||||
}
|
||||
}
|
||||
|
||||
size_t listen(const ValueCallback& cb,
|
||||
Value::Filter&& f,
|
||||
const Sp<Query>& q,
|
||||
Scheduler& scheduler,
|
||||
DoneCallbackSimple ready = {})
|
||||
size_t listen(const ValueCallback& cb, Value::Filter&& f, const Sp<Query>& q, Scheduler& scheduler)
|
||||
{
|
||||
// DHT_LOG.e(id, "[search %s IPv%c] listen", id.toString().c_str(), (af == AF_INET) ? '4' : '6');
|
||||
return cache.listen(
|
||||
cb,
|
||||
q,
|
||||
std::move(f),
|
||||
[&](const Sp<Query>& q, ValueCallback vcb, SyncCallback scb) {
|
||||
done = false;
|
||||
auto token = ++listener_token;
|
||||
listeners.emplace(token, SearchListener {q, vcb, scb});
|
||||
scheduler.edit(nextSearchStep, scheduler.time());
|
||||
return token;
|
||||
},
|
||||
std::move(ready));
|
||||
return cache.listen(cb, q, std::move(f), [&](const Sp<Query>& q, ValueCallback vcb, SyncCallback scb) {
|
||||
done = false;
|
||||
auto token = ++listener_token;
|
||||
listeners.emplace(token, SearchListener {q, vcb, scb});
|
||||
scheduler.edit(nextSearchStep, scheduler.time());
|
||||
return token;
|
||||
});
|
||||
}
|
||||
|
||||
void cancelListen(size_t token, Scheduler& scheduler)
|
||||
@@ -907,7 +769,6 @@ struct Dht::Search
|
||||
{
|
||||
// no nodes or all expired nodes. This is most likely a connectivity change event.
|
||||
expired = true;
|
||||
cache.onReady(false);
|
||||
|
||||
nodes.clear();
|
||||
if (announce.empty() && listeners.empty())
|
||||
@@ -980,7 +841,6 @@ struct Dht::Search
|
||||
|
||||
void clear()
|
||||
{
|
||||
cache.onReady(false);
|
||||
announce.clear();
|
||||
callbacks.clear();
|
||||
listeners.clear();
|
||||
|
||||
+1
-11
@@ -333,17 +333,7 @@ SecureDht::get(const InfoHash& id, GetCallback cb, DoneCallback donecb, Value::F
|
||||
size_t
|
||||
SecureDht::listen(const InfoHash& id, ValueCallback cb, Value::Filter f, Where w)
|
||||
{
|
||||
return listen(id, std::move(cb), std::move(f), std::move(w), {});
|
||||
}
|
||||
|
||||
size_t
|
||||
SecureDht::listen(const InfoHash& id, ValueCallback cb, Value::Filter f, Where w, DoneCallbackSimple ready)
|
||||
{
|
||||
return dht_->listen(id,
|
||||
getCallbackFilter(cb, std::forward<Value::Filter>(f)),
|
||||
{},
|
||||
std::forward<Where>(w),
|
||||
std::move(ready));
|
||||
return dht_->listen(id, getCallbackFilter(cb, std::forward<Value::Filter>(f)), {}, std::forward<Where>(w));
|
||||
}
|
||||
|
||||
size_t
|
||||
|
||||
+1
-3
@@ -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());
|
||||
|
||||
+7
-16
@@ -245,18 +245,10 @@ from_system_time(system_clock::time_point t, time_point steady_now, system_clock
|
||||
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)
|
||||
auto dt = t - system_now;
|
||||
if (dt > system_clock::duration(0) and steady_now > time_point::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)
|
||||
else if (dt < system_clock::duration(0) and steady_now < time_point::min() - dt)
|
||||
return time_point::min();
|
||||
return steady_now + dt;
|
||||
}
|
||||
@@ -274,13 +266,12 @@ to_system_time(time_point t, time_point steady_now, system_clock::time_point sys
|
||||
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)
|
||||
auto dt = t - steady_now;
|
||||
if (dt > 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)
|
||||
else if (dt < duration(0) and system_now <= system_clock::time_point::min() - dt)
|
||||
return system_clock::time_point::min();
|
||||
return system_now + dt;
|
||||
return system_now + std::chrono::duration_cast<system_clock::duration>(dt);
|
||||
}
|
||||
|
||||
system_clock::time_point
|
||||
|
||||
+6
-27
@@ -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 != *v->second.data and value->seq > v->second.data->seq) {
|
||||
// edited value (same rule as OpValueCache): emit as add
|
||||
v->second.data = value;
|
||||
nvals.emplace_back(value);
|
||||
}
|
||||
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;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -327,16 +327,6 @@ CryptoTester::testCertificateSerialNumber()
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
CryptoTester::testOcsp()
|
||||
{
|
||||
|
||||
@@ -19,7 +19,6 @@ class CryptoTester : public CppUnit::TestFixture
|
||||
CPPUNIT_TEST(testRevocationListPartitionedNumber);
|
||||
CPPUNIT_TEST(testCertificateRequest);
|
||||
CPPUNIT_TEST(testCertificateSerialNumber);
|
||||
CPPUNIT_TEST(testEcKeyCurve);
|
||||
CPPUNIT_TEST(testOcsp);
|
||||
CPPUNIT_TEST(testAesEncryption);
|
||||
CPPUNIT_TEST(testAesEncryptionWithMultipleKeySizes);
|
||||
@@ -69,10 +68,6 @@ public:
|
||||
* Test certificate serial number extraction
|
||||
*/
|
||||
void testCertificateSerialNumber();
|
||||
/**
|
||||
* Test EC private key generation with an explicit curve
|
||||
*/
|
||||
void testEcKeyCurve();
|
||||
/**
|
||||
* Test OCSP
|
||||
*/
|
||||
|
||||
+1
-1814
File diff suppressed because it is too large.
Load diff
+1
-64
@@ -7,26 +7,8 @@
|
||||
#include <cppunit/extensions/HelperMacros.h>
|
||||
|
||||
#include <opendht/dhtrunner.h>
|
||||
#include <opendht/http.h>
|
||||
#include <restinio/all.hpp>
|
||||
#include <restinio/tls.hpp>
|
||||
#include <json/json.h>
|
||||
|
||||
#if defined(__clang__)
|
||||
#pragma clang diagnostic push
|
||||
#pragma clang diagnostic ignored "-Wkeyword-macro"
|
||||
#elif defined(__GNUC__)
|
||||
#pragma GCC diagnostic push
|
||||
#pragma GCC diagnostic ignored "-Wpedantic"
|
||||
#endif
|
||||
#define private public
|
||||
#include <opendht/dht_proxy_server.h>
|
||||
#undef private
|
||||
#if defined(__clang__)
|
||||
#pragma clang diagnostic pop
|
||||
#elif defined(__GNUC__)
|
||||
#pragma GCC diagnostic pop
|
||||
#endif
|
||||
#include <opendht/log.h>
|
||||
|
||||
namespace test {
|
||||
|
||||
@@ -35,28 +17,10 @@ class DhtProxyTester : public CppUnit::TestFixture
|
||||
CPPUNIT_TEST_SUITE(DhtProxyTester);
|
||||
CPPUNIT_TEST(testGetPut);
|
||||
CPPUNIT_TEST(testListen);
|
||||
CPPUNIT_TEST(testListenReadiness);
|
||||
#ifdef OPENDHT_PUSH_NOTIFICATIONS
|
||||
CPPUNIT_TEST(testInitializingSubscriptionCancellation);
|
||||
#endif
|
||||
CPPUNIT_TEST(testListenValueEdit);
|
||||
CPPUNIT_TEST(testPushListenValueEdit);
|
||||
CPPUNIT_TEST(testResubscribeGetValues);
|
||||
#ifdef OPENDHT_PUSH_NOTIFICATIONS
|
||||
CPPUNIT_TEST(testFullSubscribeReturnsValuesWithoutExistingListener);
|
||||
#endif
|
||||
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);
|
||||
CPPUNIT_TEST_SUITE_END();
|
||||
|
||||
public:
|
||||
@@ -77,31 +41,11 @@ public:
|
||||
* Test listen
|
||||
*/
|
||||
void testListen();
|
||||
void testListenReadiness();
|
||||
void testInitializingSubscriptionCancellation();
|
||||
/**
|
||||
* 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();
|
||||
void testFullSubscribeReturnsValuesWithoutExistingListener();
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
@@ -110,13 +54,6 @@ public:
|
||||
void testFuzzy();
|
||||
|
||||
void testShutdownStop();
|
||||
void testGetAfterListen();
|
||||
void testListenDuplicatePut();
|
||||
void testPushMultiValueEditExpire();
|
||||
void testPushRefreshNoSpuriousExpire();
|
||||
void testProxyListenEditChain();
|
||||
void testPushRapidEditsNoPhantom();
|
||||
void testPushRefreshAfterEditKeepsLatest();
|
||||
|
||||
private:
|
||||
dht::DhtRunner::Config clientConfig {};
|
||||
|
||||
@@ -31,318 +31,14 @@ CPPUNIT_TEST_SUITE_REGISTRATION(DhtProxyClientTester);
|
||||
|
||||
using namespace dht;
|
||||
|
||||
#ifdef OPENDHT_PUSH_NOTIFICATIONS
|
||||
namespace {
|
||||
void
|
||||
finishSync(DhtProxyClient& client,
|
||||
const std::shared_ptr<dht::http::Request>& request,
|
||||
const std::vector<Sp<Value>>& values,
|
||||
uint64_t revision = 1,
|
||||
bool complete = true,
|
||||
bool snapshot = true,
|
||||
const std::string& epoch = "test-epoch")
|
||||
{
|
||||
Json::Value marker;
|
||||
marker["sync"]["epoch"] = epoch;
|
||||
marker["sync"]["revision"] = Json::UInt64(revision);
|
||||
marker["sync"]["complete"] = complete;
|
||||
marker["sync"]["snapshot"] = snapshot;
|
||||
auto body = std::string {};
|
||||
for (const auto& value : values)
|
||||
body += Json::writeString(client.jsonBuilder_, value->toJson()) + "\n";
|
||||
body += Json::writeString(client.jsonBuilder_, marker) + "\n";
|
||||
request->cbs_.on_body(body.data(), body.size());
|
||||
dht::http::Response response;
|
||||
response.status_code = 200;
|
||||
request->cbs_.on_state_change(dht::http::Request::State::DONE, response);
|
||||
client.periodic(nullptr, 0, {}, clock::now());
|
||||
}
|
||||
} // namespace
|
||||
#endif
|
||||
|
||||
void
|
||||
DhtProxyClientTester::testReadinessShutdown()
|
||||
{
|
||||
for (bool push : {false, true}) {
|
||||
DhtProxyClient client({}, {}, [] {}, "", "OpenDHT-Test", "client-id", push ? "push-token" : "");
|
||||
client.httpContext_.stop();
|
||||
client.httpClientThread_.join();
|
||||
client.proxyUrl_ = "http://127.0.0.1:1";
|
||||
auto key = InfoHash::get("readiness-shutdown");
|
||||
std::vector<bool> results;
|
||||
auto values = [](const std::vector<Sp<Value>>&, bool) {
|
||||
return true;
|
||||
};
|
||||
CPPUNIT_ASSERT(client.listen(key, values, {}, {}, [&](bool ok) {
|
||||
results.push_back(ok);
|
||||
CPPUNIT_ASSERT(client.getLocal(key, {}).empty());
|
||||
}) != 0);
|
||||
CPPUNIT_ASSERT(results.empty());
|
||||
client.shutdown({}, true);
|
||||
CPPUNIT_ASSERT(results == std::vector<bool>({false}));
|
||||
client.shutdown({}, true);
|
||||
CPPUNIT_ASSERT_EQUAL(size_t(1), results.size());
|
||||
CPPUNIT_ASSERT_EQUAL(size_t(0), client.listen(key, values, {}, {}, [&](bool ok) { results.push_back(ok); }));
|
||||
CPPUNIT_ASSERT(results == std::vector<bool>({false, false}));
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
DhtProxyClientTester::testReadinessReentrancy()
|
||||
{
|
||||
DhtProxyClient client({}, {}, [] {}, "", "OpenDHT-Test", "client-id", "");
|
||||
client.httpContext_.stop();
|
||||
client.httpClientThread_.join();
|
||||
client.proxyUrl_ = "http://127.0.0.1:1";
|
||||
auto key = InfoHash::get("ready-reentrancy");
|
||||
std::vector<bool> results;
|
||||
client.listen(
|
||||
key,
|
||||
[](const std::vector<Sp<Value>>&, bool) { return true; },
|
||||
{},
|
||||
{},
|
||||
[&](bool ok) {
|
||||
auto unlocked = client.searchLock_.try_lock();
|
||||
if (unlocked)
|
||||
client.searchLock_.unlock();
|
||||
CPPUNIT_ASSERT(unlocked);
|
||||
CPPUNIT_ASSERT(client.getLocal(key, {}).empty());
|
||||
results.push_back(ok);
|
||||
});
|
||||
auto request = client.searches_.at(key).listeners.begin()->second.request;
|
||||
std::string marker = "{\"sync\":{\"complete\":true}}\n";
|
||||
request->cbs_.on_body(marker.data(), marker.size());
|
||||
client.periodic(nullptr, 0, {}, clock::now());
|
||||
CPPUNIT_ASSERT(results == std::vector<bool>({true}));
|
||||
}
|
||||
|
||||
void
|
||||
DhtProxyClientTester::testCachedReadinessDispatch()
|
||||
{
|
||||
for (unsigned action = 0; action < 3; ++action) {
|
||||
DhtProxyClient client({}, {}, [] {}, "", "OpenDHT-Test", "client-id", "");
|
||||
client.httpContext_.stop();
|
||||
client.httpClientThread_.join();
|
||||
client.proxyUrl_ = "http://127.0.0.1:1";
|
||||
auto key = InfoHash::get("cached-readiness-dispatch");
|
||||
auto values = [](const std::vector<Sp<Value>>&, bool) {
|
||||
return true;
|
||||
};
|
||||
auto first = client.listen(key, values, {}, {});
|
||||
CPPUNIT_ASSERT(first != 0);
|
||||
auto value = std::make_shared<Value>("cached");
|
||||
value->id = 42;
|
||||
auto request = client.searches_.at(key).listeners.begin()->second.request;
|
||||
auto body = Json::writeString(client.jsonBuilder_, value->toJson()) + "\n{\"sync\":{\"complete\":true}}\n";
|
||||
request->cbs_.on_body(body.data(), body.size());
|
||||
client.periodic(nullptr, 0, {}, clock::now());
|
||||
CPPUNIT_ASSERT(client.getLocalById(key, 42));
|
||||
|
||||
std::vector<bool> results;
|
||||
size_t delivered = 0;
|
||||
auto attached = client.listen(
|
||||
key,
|
||||
[&](const std::vector<Sp<Value>>& cached, bool expired) {
|
||||
if (!expired)
|
||||
delivered += cached.size();
|
||||
return true;
|
||||
},
|
||||
{},
|
||||
{},
|
||||
[&](bool ok) {
|
||||
results.emplace_back(ok);
|
||||
if (ok) {
|
||||
CPPUNIT_ASSERT(client.getLocalById(key, 42));
|
||||
auto nested = client.listen(key, values, {}, {});
|
||||
CPPUNIT_ASSERT(nested != 0);
|
||||
CPPUNIT_ASSERT(client.cancelListen(key, nested));
|
||||
} else
|
||||
CPPUNIT_ASSERT(client.getLocal(key, {}).empty());
|
||||
});
|
||||
CPPUNIT_ASSERT(attached != 0);
|
||||
CPPUNIT_ASSERT_EQUAL(size_t(1), delivered);
|
||||
CPPUNIT_ASSERT(results.empty());
|
||||
CPPUNIT_ASSERT_EQUAL(size_t(1), client.readinessCallbacks_.size());
|
||||
if (action == 1)
|
||||
CPPUNIT_ASSERT(client.cancelListen(key, attached));
|
||||
else if (action == 2) {
|
||||
client.shutdown({}, true);
|
||||
CPPUNIT_ASSERT(results == std::vector<bool>({false}));
|
||||
}
|
||||
client.periodic(nullptr, 0, {}, clock::now());
|
||||
if (action == 1)
|
||||
CPPUNIT_ASSERT(results.empty());
|
||||
else
|
||||
CPPUNIT_ASSERT(results == std::vector<bool>({action == 0}));
|
||||
client.shutdown({}, true);
|
||||
CPPUNIT_ASSERT_EQUAL(action == 1 ? size_t(0) : size_t(1), results.size());
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
DhtProxyClientTester::testReadinessCancelsAnotherListener()
|
||||
{
|
||||
DhtProxyClient client({}, {}, [] {}, "", "OpenDHT-Test", "client-id", "");
|
||||
client.httpContext_.stop();
|
||||
client.httpClientThread_.join();
|
||||
client.proxyUrl_ = "http://127.0.0.1:1";
|
||||
auto key = InfoHash::get("ready-cancel-neighbor");
|
||||
size_t second = 0;
|
||||
unsigned firstCalls = 0;
|
||||
unsigned secondCalls = 0;
|
||||
auto values = [](const std::vector<Sp<Value>>&, bool) {
|
||||
return true;
|
||||
};
|
||||
client.listen(key, values, {}, {}, [&](bool ok) {
|
||||
auto unlocked = client.searchLock_.try_lock();
|
||||
if (unlocked)
|
||||
client.searchLock_.unlock();
|
||||
CPPUNIT_ASSERT(unlocked);
|
||||
CPPUNIT_ASSERT(ok);
|
||||
++firstCalls;
|
||||
CPPUNIT_ASSERT(client.cancelListen(key, second));
|
||||
});
|
||||
second = client.listen(key, values, {}, {}, [&](bool) { ++secondCalls; });
|
||||
auto request = client.searches_.at(key).listeners.begin()->second.request;
|
||||
std::string marker = "{\"sync\":{\"complete\":true}}\n";
|
||||
request->cbs_.on_body(marker.data(), marker.size());
|
||||
client.periodic(nullptr, 0, {}, clock::now());
|
||||
CPPUNIT_ASSERT_EQUAL(1u, firstCalls);
|
||||
CPPUNIT_ASSERT_EQUAL(0u, secondCalls);
|
||||
}
|
||||
|
||||
#ifdef OPENDHT_PUSH_NOTIFICATIONS
|
||||
void
|
||||
DhtProxyClientTester::testRejectsUnrevisionedPushes()
|
||||
{
|
||||
DhtProxyClient client({}, {}, [] {}, "", "OpenDHT-Test", "client-id", "push-token");
|
||||
client.httpContext_.stop();
|
||||
client.httpClientThread_.join();
|
||||
client.proxyUrl_ = "http://127.0.0.1:1";
|
||||
auto key = InfoHash::get("unrevisioned-push-rejection");
|
||||
client.listen(key, [](const std::vector<Sp<Value>>&, bool) { return true; }, {}, {});
|
||||
auto& listener = client.searches_.at(key).listeners.begin()->second;
|
||||
auto value = std::make_shared<Value>("obsolete");
|
||||
value->id = 42;
|
||||
finishSync(client, listener.request, {value}, 5);
|
||||
auto synchronized = listener.request;
|
||||
auto requestCount = client.requests_.size();
|
||||
for (const auto& notification : std::vector<std::map<std::string, std::string>> {
|
||||
{{"key", key.toString()}, {"t", "10000"}},
|
||||
{{"key", key.toString()}, {"expired", "true"}, {"ids", "42"}},
|
||||
{{"key", key.toString()}, {"epoch", "test-epoch"}},
|
||||
{{"key", key.toString()}, {"revision", "6"}}
|
||||
}) {
|
||||
CPPUNIT_ASSERT(client.pushNotificationReceived(notification) == PushNotificationResult::Error);
|
||||
CPPUNIT_ASSERT(listener.request == synchronized);
|
||||
CPPUNIT_ASSERT_EQUAL(requestCount, client.requests_.size());
|
||||
CPPUNIT_ASSERT_EQUAL(uint64_t(5), listener.syncRevision);
|
||||
CPPUNIT_ASSERT(listener.opstate->ok.load());
|
||||
CPPUNIT_ASSERT(client.getLocalById(key, 42));
|
||||
}
|
||||
CPPUNIT_ASSERT(
|
||||
client.pushNotificationReceived({
|
||||
{"key", key.toString()},
|
||||
{"epoch", "test-epoch" },
|
||||
{"revision", "6" },
|
||||
{"expired", "true" },
|
||||
{"ids", "42" }
|
||||
})
|
||||
== PushNotificationResult::ValuesExpired);
|
||||
client.periodic(nullptr, 0, {}, clock::now());
|
||||
CPPUNIT_ASSERT(!client.getLocalById(key, 42));
|
||||
client.restartListeners({});
|
||||
auto validation = listener.request;
|
||||
CPPUNIT_ASSERT(client.pushNotificationReceived({
|
||||
{"key", key.toString()}
|
||||
})
|
||||
== PushNotificationResult::Error);
|
||||
finishSync(client, validation, {}, 6, true, false);
|
||||
CPPUNIT_ASSERT(listener.request == validation);
|
||||
CPPUNIT_ASSERT(listener.opstate->ok.load());
|
||||
CPPUNIT_ASSERT(!client.getLocalById(key, 42));
|
||||
}
|
||||
|
||||
void
|
||||
DhtProxyClientTester::testInitialReadinessFailures()
|
||||
{
|
||||
for (unsigned failure = 0; failure < 4; ++failure) {
|
||||
bool push = failure == 1 || failure == 3;
|
||||
DhtProxyClient client({}, {}, [] {}, "", "OpenDHT-Test", "client-id", push ? "push-token" : "");
|
||||
client.httpContext_.stop();
|
||||
client.httpClientThread_.join();
|
||||
client.proxyUrl_ = "http://127.0.0.1:1";
|
||||
auto key = InfoHash::get("initial-readiness-failure");
|
||||
std::vector<bool> results;
|
||||
CPPUNIT_ASSERT(client.listen(
|
||||
key,
|
||||
[](const std::vector<Sp<Value>>&, bool) { return true; },
|
||||
{},
|
||||
{},
|
||||
[&](bool ok) { results.push_back(ok); })
|
||||
!= 0);
|
||||
auto& listener = client.searches_.at(key).listeners.begin()->second;
|
||||
auto request = listener.request;
|
||||
if (failure == 0) {
|
||||
std::string marker = "{\"sync\":{\"complete\":false}}\n";
|
||||
request->cbs_.on_body(marker.data(), marker.size());
|
||||
} else if (failure == 3) {
|
||||
finishSync(client, request, {}, 1, false);
|
||||
} else {
|
||||
dht::http::Response response;
|
||||
response.status_code = 503;
|
||||
request->cbs_.on_state_change(dht::http::Request::State::DONE, response);
|
||||
}
|
||||
client.periodic(nullptr, 0, {}, clock::now());
|
||||
CPPUNIT_ASSERT(results == std::vector<bool>({false}));
|
||||
client.shutdown({}, true);
|
||||
CPPUNIT_ASSERT_EQUAL(size_t(1), results.size());
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
DhtProxyClientTester::testReadinessRecoveryIsOneShot()
|
||||
{
|
||||
DhtProxyClient client({}, {}, [] {}, "", "OpenDHT-Test", "client-id", "push-token");
|
||||
client.httpContext_.stop();
|
||||
client.httpClientThread_.join();
|
||||
client.proxyUrl_ = "http://127.0.0.1:1";
|
||||
auto key = InfoHash::get("readiness-recovery");
|
||||
auto values = [](const std::vector<Sp<Value>>&, bool) {
|
||||
return true;
|
||||
};
|
||||
std::vector<bool> first;
|
||||
CPPUNIT_ASSERT(client.listen(key, values, {}, {}, [&](bool ok) { first.push_back(ok); }) != 0);
|
||||
auto& listener = client.searches_.at(key).listeners.begin()->second;
|
||||
auto request = listener.request;
|
||||
dht::http::Response response;
|
||||
response.status_code = 503;
|
||||
request->cbs_.on_state_change(dht::http::Request::State::DONE, response);
|
||||
client.periodic(nullptr, 0, {}, clock::now());
|
||||
CPPUNIT_ASSERT(first == std::vector<bool>({false}));
|
||||
std::vector<bool> second;
|
||||
CPPUNIT_ASSERT(client.listen(key, values, {}, {}, [&](bool ok) { second.push_back(ok); }) != 0);
|
||||
CPPUNIT_ASSERT(second.empty());
|
||||
client.handleResubscribe({}, key, 1, listener.opstate);
|
||||
finishSync(client, listener.request, {}, 1);
|
||||
CPPUNIT_ASSERT(first == std::vector<bool>({false}));
|
||||
CPPUNIT_ASSERT(second == std::vector<bool>({true}));
|
||||
client.shutdown({}, true);
|
||||
CPPUNIT_ASSERT_EQUAL(size_t(1), first.size());
|
||||
CPPUNIT_ASSERT_EQUAL(size_t(1), second.size());
|
||||
}
|
||||
#endif
|
||||
|
||||
void
|
||||
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.needsRecovery = false;
|
||||
listener.syncEpoch = "test-epoch";
|
||||
listener.cb = [](const std::vector<Sp<Value>>&, bool, system_clock::time_point) {
|
||||
return true;
|
||||
};
|
||||
@@ -352,7 +48,8 @@ DhtProxyClientTester::testResubscribeUsesKeyRoute()
|
||||
|
||||
CPPUNIT_ASSERT(listener.request);
|
||||
CPPUNIT_ASSERT_EQUAL(std::string("/key/") + key.toString(), std::string(listener.request->header_.request_target()));
|
||||
CPPUNIT_ASSERT(listener.request->body_.find("test-epoch") != std::string::npos);
|
||||
CPPUNIT_ASSERT(listener.request->body_.find("\"refresh\"") != std::string::npos);
|
||||
CPPUNIT_ASSERT(listener.request->body_.find("true") != std::string::npos);
|
||||
#else
|
||||
CPPUNIT_ASSERT(true);
|
||||
#endif
|
||||
@@ -370,15 +67,12 @@ 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);
|
||||
|
||||
auto& listener = it->second;
|
||||
listener.opstate = std::make_shared<DhtProxyClient::OperationState>();
|
||||
listener.needsRecovery = false;
|
||||
listener.syncEpoch = "test-epoch";
|
||||
listener.syncRevision = 4;
|
||||
listener.cb = [](const std::vector<Sp<Value>>&, bool, system_clock::time_point) {
|
||||
return true;
|
||||
};
|
||||
@@ -389,644 +83,11 @@ DhtProxyClientTester::testSetPushNotificationTokenResubscribesWithNewToken()
|
||||
CPPUNIT_ASSERT_EQUAL(std::string("/key/") + key.toString(), std::string(listener.request->header_.request_target()));
|
||||
CPPUNIT_ASSERT(listener.request->body_.find("new-token") != std::string::npos);
|
||||
CPPUNIT_ASSERT(listener.request->body_.find("old-token") == std::string::npos);
|
||||
CPPUNIT_ASSERT(listener.request->body_.find("test-epoch") != std::string::npos);
|
||||
CPPUNIT_ASSERT(listener.request->body_.find("\"refresh\"") != std::string::npos);
|
||||
CPPUNIT_ASSERT(listener.request->body_.find("true") != std::string::npos);
|
||||
#else
|
||||
CPPUNIT_ASSERT(true);
|
||||
#endif
|
||||
}
|
||||
|
||||
void
|
||||
DhtProxyClientTester::testRestartListenersFetchesValuesAfterFailedSubscribe()
|
||||
{
|
||||
#ifdef OPENDHT_PUSH_NOTIFICATIONS
|
||||
DhtProxyClient client({}, {}, [] {}, "http://127.0.0.1:8080", "OpenDHT-Test", "client-id", "push-token");
|
||||
|
||||
auto addListener = [&](const InfoHash& key, bool ok) -> DhtProxyClient::Listener& {
|
||||
auto& search = client.searches_[key];
|
||||
auto [it, inserted] = search.listeners.emplace(std::piecewise_construct,
|
||||
std::forward_as_tuple(1),
|
||||
std::forward_as_tuple(ValueCallback(
|
||||
[](const std::vector<Sp<Value>>&, bool) { return true; })));
|
||||
CPPUNIT_ASSERT(inserted);
|
||||
auto& listener = it->second;
|
||||
listener.opstate = std::make_shared<DhtProxyClient::OperationState>();
|
||||
listener.opstate->ok = ok;
|
||||
listener.cb = [](const std::vector<Sp<Value>>&, bool, system_clock::time_point) {
|
||||
return true;
|
||||
};
|
||||
return listener;
|
||||
};
|
||||
|
||||
// The subscription of this listener failed (e.g. no network): values stored on the
|
||||
// key since then were never received.
|
||||
auto failedKey = InfoHash::get("proxy-client-failed-subscribe");
|
||||
auto& failed = addListener(failedKey, false);
|
||||
auto healthyKey = InfoHash::get("proxy-client-healthy-subscribe");
|
||||
auto& healthy = addListener(healthyKey, true);
|
||||
|
||||
client.restartListeners({});
|
||||
|
||||
CPPUNIT_ASSERT(failed.request);
|
||||
CPPUNIT_ASSERT_EQUAL(std::string("/key/") + failedKey.toString(),
|
||||
std::string(failed.request->header_.request_target()));
|
||||
CPPUNIT_ASSERT(failed.request->body_.find("push-token") != std::string::npos);
|
||||
// Without "refresh", the server answers with the values it stores for the key.
|
||||
CPPUNIT_ASSERT(failed.request->body_.find("\"refresh\"") == std::string::npos);
|
||||
|
||||
CPPUNIT_ASSERT(healthy.request);
|
||||
CPPUNIT_ASSERT(healthy.request->body_.find("\"sync\"") != std::string::npos);
|
||||
#else
|
||||
CPPUNIT_ASSERT(true);
|
||||
#endif
|
||||
}
|
||||
|
||||
#ifdef OPENDHT_PUSH_NOTIFICATIONS
|
||||
void
|
||||
DhtProxyClientTester::testWrongSessionPushDoesNotTouchSubscriptions()
|
||||
{
|
||||
DhtProxyClient client({}, {}, [] {}, "", "OpenDHT-Test", "client-id", "push-token");
|
||||
client.httpContext_.stop();
|
||||
client.httpClientThread_.join();
|
||||
client.proxyUrl_ = "http://127.0.0.1:1";
|
||||
auto key = InfoHash::get("wrong-session-isolation");
|
||||
client.listen(key, [](const std::vector<Sp<Value>>&, bool) { return true; }, {}, {});
|
||||
auto& listener = client.searches_.at(key).listeners.begin()->second;
|
||||
auto value = std::make_shared<Value>("current-session-value");
|
||||
value->id = 42;
|
||||
finishSync(client, listener.request, {value}, 5);
|
||||
client.statusIpv4_ = NodeStatus::Disconnected;
|
||||
client.statusIpv6_ = NodeStatus::Disconnected;
|
||||
client.launchConnectedCbs_ = false;
|
||||
auto request = listener.request;
|
||||
auto generation = listener.opstate->generation.load();
|
||||
auto expiration = listener.refreshSubscriberTimer->expiry();
|
||||
std::vector<std::map<std::string, std::string>> notifications {
|
||||
{{"s", "old-session"}, {"key", key.toString()}, {"epoch", "old-epoch"}, {"revision", "99"}},
|
||||
{{"s", "old-session"}, {"key", key.toString()}, {"exp", "42"}},
|
||||
{{"s", "old-session"}, {"timeout", key.toString()}},
|
||||
{{"s", "old-session"}, {"timeout", key.toString()}, {"vid", "42"}}
|
||||
};
|
||||
for (const auto& notification : notifications) {
|
||||
CPPUNIT_ASSERT(client.pushNotificationReceived(notification) == PushNotificationResult::IgnoredWrongSession);
|
||||
CPPUNIT_ASSERT(listener.request == request);
|
||||
CPPUNIT_ASSERT_EQUAL(generation, listener.opstate->generation.load());
|
||||
CPPUNIT_ASSERT(!listener.needsRecovery);
|
||||
CPPUNIT_ASSERT(listener.opstate->ok.load());
|
||||
CPPUNIT_ASSERT(listener.refreshSubscriberTimer->expiry() == expiration);
|
||||
CPPUNIT_ASSERT_EQUAL(std::string("test-epoch"), listener.syncEpoch);
|
||||
CPPUNIT_ASSERT_EQUAL(uint64_t(5), listener.syncRevision);
|
||||
CPPUNIT_ASSERT(client.getLocalById(key, 42));
|
||||
CPPUNIT_ASSERT(client.statusIpv4_ == NodeStatus::Disconnected);
|
||||
CPPUNIT_ASSERT(client.statusIpv6_ == NodeStatus::Disconnected);
|
||||
CPPUNIT_ASSERT(!client.launchConnectedCbs_.load());
|
||||
CPPUNIT_ASSERT(client.callbacks_.empty());
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
DhtProxyClientTester::testNewSessionSynchronizesIndependently()
|
||||
{
|
||||
DhtProxyClient client({}, {}, [] {}, "", "OpenDHT-Test", "client-id", "push-token");
|
||||
client.httpContext_.stop();
|
||||
client.httpClientThread_.join();
|
||||
client.proxyUrl_ = "http://127.0.0.1:1";
|
||||
auto key = InfoHash::get("new-session-independent-sync");
|
||||
CPPUNIT_ASSERT(client.pushNotificationReceived({
|
||||
{"s", "old-session" },
|
||||
{"key", key.toString()}
|
||||
})
|
||||
== PushNotificationResult::IgnoredWrongSession);
|
||||
CPPUNIT_ASSERT(client.searches_.empty());
|
||||
client.listen(key, [](const std::vector<Sp<Value>>&, bool) { return true; }, {}, {});
|
||||
auto& listener = client.searches_.at(key).listeners.begin()->second;
|
||||
Json::Value body;
|
||||
std::string error;
|
||||
auto reader = std::unique_ptr<Json::CharReader>(Json::CharReaderBuilder {}.newCharReader());
|
||||
const auto& requestBody = listener.request->body_;
|
||||
CPPUNIT_ASSERT(reader->parse(requestBody.data(), requestBody.data() + requestBody.size(), &body, &error));
|
||||
CPPUNIT_ASSERT_EQUAL(client.pushSessionId_, body["session_id"].asString());
|
||||
CPPUNIT_ASSERT(body["sync"].isObject() && body["sync"].empty());
|
||||
auto value = std::make_shared<Value>("new-session-snapshot");
|
||||
value->id = 43;
|
||||
finishSync(client, listener.request, {value});
|
||||
CPPUNIT_ASSERT(listener.opstate->ok.load());
|
||||
CPPUNIT_ASSERT(!listener.needsRecovery);
|
||||
CPPUNIT_ASSERT(client.getLocalById(key, 43)->data == value->data);
|
||||
}
|
||||
|
||||
void
|
||||
DhtProxyClientTester::testFailedTimerRetryFetchesValues()
|
||||
{
|
||||
DhtProxyClient client({}, {}, [] {}, "", "OpenDHT-Test", "client-id", "push-token");
|
||||
client.httpContext_.stop();
|
||||
client.httpClientThread_.join();
|
||||
client.proxyUrl_ = "http://127.0.0.1:1";
|
||||
auto key = InfoHash::get("failed-timer-retry");
|
||||
client.listen(key, [](const std::vector<Sp<Value>>&, bool) { return true; }, {}, {});
|
||||
auto& listener = client.searches_.at(key).listeners.begin()->second;
|
||||
listener.opstate->ok = false;
|
||||
client.handleResubscribe({}, key, 1, listener.opstate);
|
||||
CPPUNIT_ASSERT(listener.request->body_.find("\"refresh\"") == std::string::npos);
|
||||
}
|
||||
|
||||
void
|
||||
DhtProxyClientTester::testFullRetryRemovesAbsentValues()
|
||||
{
|
||||
DhtProxyClient client({}, {}, [] {}, "", "OpenDHT-Test", "client-id", "push-token");
|
||||
client.httpContext_.stop();
|
||||
client.httpClientThread_.join();
|
||||
client.proxyUrl_ = "http://127.0.0.1:1";
|
||||
auto key = InfoHash::get("full-retry-absent-values");
|
||||
client.listen(key, [](const std::vector<Sp<Value>>&, bool) { return true; }, {}, {});
|
||||
auto& listener = client.searches_.at(key).listeners.begin()->second;
|
||||
auto value = std::make_shared<Value>("expired-while-offline");
|
||||
value->id = 42;
|
||||
listener.cb({value}, false, system_clock::time_point(std::chrono::seconds(10)));
|
||||
listener.opstate->ok = false;
|
||||
client.restartListeners({});
|
||||
auto request = listener.request;
|
||||
finishSync(client, request, {});
|
||||
CPPUNIT_ASSERT(!client.getLocalById(key, value->id));
|
||||
}
|
||||
|
||||
void
|
||||
DhtProxyClientTester::testStreamingFallbackExpiresSnapshot()
|
||||
{
|
||||
DhtProxyClient client({}, {}, [] {}, "", "OpenDHT-Test", "client-id", "push-token");
|
||||
client.httpContext_.stop();
|
||||
client.httpClientThread_.join();
|
||||
client.proxyUrl_ = "http://127.0.0.1:1";
|
||||
auto key = InfoHash::get("full-retry-push-ordering");
|
||||
client.listen(key, [](const std::vector<Sp<Value>>&, bool) { return true; }, {}, {});
|
||||
auto& listener = client.searches_.at(key).listeners.begin()->second;
|
||||
auto value = std::make_shared<Value>("live-value");
|
||||
value->id = 42;
|
||||
listener.cb({value}, false, system_clock::time_point(std::chrono::seconds(10)));
|
||||
listener.opstate->ok = false;
|
||||
client.restartListeners({});
|
||||
finishSync(client, listener.request, {value}, 10);
|
||||
client.deviceKey_.clear();
|
||||
client.restartListeners({});
|
||||
auto expired = value->toJson();
|
||||
expired["expired"] = true;
|
||||
auto body = Json::writeString(client.jsonBuilder_, expired) + "\n";
|
||||
listener.request->cbs_.on_body(body.data(), body.size());
|
||||
client.periodic(nullptr, 0, {}, clock::now());
|
||||
CPPUNIT_ASSERT(!client.getLocalById(key, value->id));
|
||||
}
|
||||
|
||||
void
|
||||
DhtProxyClientTester::testLateRetryResponseDoesNotResurrectExpiredValue()
|
||||
{
|
||||
DhtProxyClient client({}, {}, [] {}, "", "OpenDHT-Test", "client-id", "push-token");
|
||||
client.httpContext_.stop();
|
||||
client.httpClientThread_.join();
|
||||
client.proxyUrl_ = "http://127.0.0.1:1";
|
||||
auto key = InfoHash::get("full-retry-late-response");
|
||||
client.listen(key, [](const std::vector<Sp<Value>>&, bool) { return true; }, {}, {});
|
||||
auto& listener = client.searches_.at(key).listeners.begin()->second;
|
||||
auto value = std::make_shared<Value>("already-expired-value");
|
||||
value->id = 42;
|
||||
finishSync(client, listener.request, {value}, 10);
|
||||
listener.opstate->ok = false;
|
||||
client.restartListeners({});
|
||||
auto staleRequest = listener.request;
|
||||
client.pushNotificationReceived({
|
||||
{"key", key.toString()},
|
||||
{"exp", "42" },
|
||||
{"epoch", "test-epoch" },
|
||||
{"revision", "11" }
|
||||
});
|
||||
finishSync(client, staleRequest, {value}, 10);
|
||||
client.handleResubscribe({}, key, 1, listener.opstate);
|
||||
finishSync(client, listener.request, {}, 11);
|
||||
finishSync(client, staleRequest, {value}, 10);
|
||||
CPPUNIT_ASSERT(!client.getLocalById(key, value->id));
|
||||
}
|
||||
|
||||
void
|
||||
DhtProxyClientTester::testTruncatedRecoveryRemainsFailed()
|
||||
{
|
||||
DhtProxyClient client({}, {}, [] {}, "", "OpenDHT-Test", "client-id", "push-token");
|
||||
client.httpContext_.stop();
|
||||
client.httpClientThread_.join();
|
||||
client.proxyUrl_ = "http://127.0.0.1:1";
|
||||
auto key = InfoHash::get("truncated-full-retry");
|
||||
client.listen(key, [](const std::vector<Sp<Value>>&, bool) { return true; }, {}, {});
|
||||
auto& listener = client.searches_.at(key).listeners.begin()->second;
|
||||
listener.opstate->ok = false;
|
||||
client.restartListeners({});
|
||||
auto request = listener.request;
|
||||
request->response_.status_code = 200;
|
||||
request->cbs_.on_body("{\"id\":", 6);
|
||||
request->terminate(asio::error::connection_reset);
|
||||
CPPUNIT_ASSERT(!listener.opstate->ok.load());
|
||||
}
|
||||
|
||||
void
|
||||
DhtProxyClientTester::testTruncatedRecoveryAtEofFails()
|
||||
{
|
||||
DhtProxyClient client({}, {}, [] {}, "", "OpenDHT-Test", "client-id", "push-token");
|
||||
client.httpContext_.stop();
|
||||
client.httpClientThread_.join();
|
||||
client.proxyUrl_ = "http://127.0.0.1:1";
|
||||
auto key = InfoHash::get("truncated-recovery-eof");
|
||||
client.listen(key, [](const std::vector<Sp<Value>>&, bool) { return true; }, {}, {});
|
||||
auto& listener = client.searches_.at(key).listeners.begin()->second;
|
||||
auto request = listener.request;
|
||||
request->init_parser();
|
||||
std::string response = "HTTP/1.1 200 OK\r\nContent-Length: 10\r\n\r\n{";
|
||||
CPPUNIT_ASSERT(llhttp_execute(request->parser_.get(), response.data(), response.size()) == HPE_OK);
|
||||
request->handle_response(asio::error::eof, 0);
|
||||
CPPUNIT_ASSERT(!listener.opstate->ok.load());
|
||||
}
|
||||
|
||||
void
|
||||
DhtProxyClientTester::testMissingCompletionDoesNotReconcile()
|
||||
{
|
||||
DhtProxyClient client({}, {}, [] {}, "", "OpenDHT-Test", "client-id", "push-token");
|
||||
client.httpContext_.stop();
|
||||
client.httpClientThread_.join();
|
||||
client.proxyUrl_ = "http://127.0.0.1:1";
|
||||
auto key = InfoHash::get("missing-completion");
|
||||
client.listen(key, [](const std::vector<Sp<Value>>&, bool) { return true; }, {}, {});
|
||||
auto& listener = client.searches_.at(key).listeners.begin()->second;
|
||||
auto value = std::make_shared<Value>("keep-value");
|
||||
value->id = 42;
|
||||
finishSync(client, listener.request, {value});
|
||||
client.restartListeners({});
|
||||
auto request = listener.request;
|
||||
request->cbs_.on_body("{}\n", 3);
|
||||
dht::http::Response response;
|
||||
response.status_code = 200;
|
||||
request->cbs_.on_state_change(dht::http::Request::State::DONE, response);
|
||||
client.periodic(nullptr, 0, {}, clock::now());
|
||||
CPPUNIT_ASSERT(!listener.opstate->ok.load());
|
||||
CPPUNIT_ASSERT(client.getLocalById(key, 42));
|
||||
CPPUNIT_ASSERT(listener.refreshSubscriberTimer->expiry() <= system_clock::now() + std::chrono::seconds(2));
|
||||
}
|
||||
|
||||
void
|
||||
DhtProxyClientTester::testSnapshotExpirationIgnoresClockRollback()
|
||||
{
|
||||
DhtProxyClient client({}, {}, [] {}, "", "OpenDHT-Test", "client-id", "push-token");
|
||||
client.httpContext_.stop();
|
||||
client.httpClientThread_.join();
|
||||
client.proxyUrl_ = "http://127.0.0.1:1";
|
||||
auto key = InfoHash::get("snapshot-clock-rollback");
|
||||
client.listen(key, [](const std::vector<Sp<Value>>&, bool) { return true; }, {}, {});
|
||||
auto& listener = client.searches_.at(key).listeners.begin()->second;
|
||||
auto value = std::make_shared<Value>("expired-value");
|
||||
value->id = 42;
|
||||
listener.cb({value}, false, system_clock::now() + std::chrono::hours(1));
|
||||
finishSync(client, listener.request, {});
|
||||
CPPUNIT_ASSERT(!client.getLocalById(key, 42));
|
||||
}
|
||||
|
||||
void
|
||||
DhtProxyClientTester::testSupersededFailureDoesNotPoisonSuccessfulRetry()
|
||||
{
|
||||
DhtProxyClient client({}, {}, [] {}, "", "OpenDHT-Test", "client-id", "push-token");
|
||||
client.httpContext_.stop();
|
||||
client.httpClientThread_.join();
|
||||
client.proxyUrl_ = "http://127.0.0.1:1";
|
||||
auto key = InfoHash::get("superseded-full-retry");
|
||||
client.listen(key, [](const std::vector<Sp<Value>>&, bool) { return true; }, {}, {});
|
||||
auto& listener = client.searches_.at(key).listeners.begin()->second;
|
||||
auto oldRequest = listener.request;
|
||||
oldRequest->response_.status_code = 503;
|
||||
oldRequest->terminate(asio::error::eof);
|
||||
client.periodic(nullptr, 0, {}, clock::now());
|
||||
client.restartListeners({});
|
||||
auto newRequest = listener.request;
|
||||
CPPUNIT_ASSERT(newRequest != oldRequest);
|
||||
finishSync(client, newRequest, {});
|
||||
dht::http::Response response;
|
||||
response.status_code = 503;
|
||||
oldRequest->cbs_.on_state_change(dht::http::Request::State::DONE, response);
|
||||
client.periodic(nullptr, 0, {}, clock::now());
|
||||
CPPUNIT_ASSERT(listener.opstate->ok.load());
|
||||
}
|
||||
|
||||
void
|
||||
DhtProxyClientTester::testSyncCoalescesConnectivityChanges()
|
||||
{
|
||||
DhtProxyClient client({}, {}, [] {}, "", "OpenDHT-Test", "client-id", "push-token");
|
||||
client.httpContext_.stop();
|
||||
client.httpClientThread_.join();
|
||||
client.proxyUrl_ = "http://127.0.0.1:1";
|
||||
auto key = InfoHash::get("coalesced-sync");
|
||||
client.listen(key, [](const std::vector<Sp<Value>>&, bool) { return true; }, {}, {});
|
||||
auto& listener = client.searches_.at(key).listeners.begin()->second;
|
||||
auto request = listener.request;
|
||||
client.restartListeners({});
|
||||
client.restartListeners({});
|
||||
CPPUNIT_ASSERT(listener.request == request);
|
||||
finishSync(client, request, {}, 5);
|
||||
CPPUNIT_ASSERT(listener.request == request);
|
||||
client.restartListeners({});
|
||||
CPPUNIT_ASSERT(listener.request != request);
|
||||
auto body = listener.request->body_;
|
||||
CPPUNIT_ASSERT(body.find("test-epoch") != std::string::npos);
|
||||
CPPUNIT_ASSERT(body.find("\"revision\":5") != std::string::npos);
|
||||
auto validation = listener.request;
|
||||
client.restartListeners({});
|
||||
client.restartListeners({});
|
||||
CPPUNIT_ASSERT(listener.request == validation);
|
||||
finishSync(client, listener.request, {}, 5, true, false);
|
||||
CPPUNIT_ASSERT(listener.opstate->ok.load());
|
||||
}
|
||||
|
||||
void
|
||||
DhtProxyClientTester::testTokenRotationDuringRecovery()
|
||||
{
|
||||
DhtProxyClient client({}, {}, [] {}, "", "OpenDHT-Test", "client-id", "old-token");
|
||||
client.httpContext_.stop();
|
||||
client.httpClientThread_.join();
|
||||
client.proxyUrl_ = "http://127.0.0.1:1";
|
||||
client.statusIpv4_ = NodeStatus::Connected;
|
||||
auto key = InfoHash::get("rotation-during-recovery");
|
||||
client.listen(key, [](const std::vector<Sp<Value>>&, bool) { return true; }, {}, {});
|
||||
auto& listener = client.searches_.at(key).listeners.begin()->second;
|
||||
auto recovery = listener.request;
|
||||
client.setPushNotificationToken("new-token");
|
||||
CPPUNIT_ASSERT(listener.request == recovery);
|
||||
finishSync(client, recovery, {}, 1);
|
||||
CPPUNIT_ASSERT(listener.request != recovery);
|
||||
CPPUNIT_ASSERT(listener.request->body_.find("new-token") != std::string::npos);
|
||||
finishSync(client, listener.request, {}, 1, true, false);
|
||||
CPPUNIT_ASSERT(listener.opstate->ok.load());
|
||||
}
|
||||
|
||||
void
|
||||
DhtProxyClientTester::testTokenRotationBackToPreviousEpoch()
|
||||
{
|
||||
DhtProxyClient client({}, {}, [] {}, "", "OpenDHT-Test", "client-id", "first-token");
|
||||
client.httpContext_.stop();
|
||||
client.httpClientThread_.join();
|
||||
client.proxyUrl_ = "http://127.0.0.1:1";
|
||||
client.statusIpv4_ = NodeStatus::Connected;
|
||||
auto key = InfoHash::get("rotation-back-to-previous-epoch");
|
||||
client.listen(key, [](const std::vector<Sp<Value>>&, bool) { return true; }, {}, {});
|
||||
auto& listener = client.searches_.at(key).listeners.begin()->second;
|
||||
finishSync(client, listener.request, {}, 1);
|
||||
client.setPushNotificationToken("second-token");
|
||||
finishSync(client, listener.request, {}, 1, true, true, "second-epoch");
|
||||
CPPUNIT_ASSERT(listener.opstate->ok.load());
|
||||
client.setPushNotificationToken("first-token");
|
||||
finishSync(client, listener.request, {}, 2);
|
||||
CPPUNIT_ASSERT(listener.opstate->ok.load());
|
||||
CPPUNIT_ASSERT_EQUAL(std::string("test-epoch"), listener.syncEpoch);
|
||||
}
|
||||
|
||||
void
|
||||
DhtProxyClientTester::testAuthoritativeEpochAfterStalePush()
|
||||
{
|
||||
DhtProxyClient client({}, {}, [] {}, "", "OpenDHT-Test", "client-id", "push-token");
|
||||
client.httpContext_.stop();
|
||||
client.httpClientThread_.join();
|
||||
client.proxyUrl_ = "http://127.0.0.1:1";
|
||||
auto key = InfoHash::get("authoritative-epoch-after-stale-push");
|
||||
client.listen(key, [](const std::vector<Sp<Value>>&, bool) { return true; }, {}, {});
|
||||
auto& listener = client.searches_.at(key).listeners.begin()->second;
|
||||
finishSync(client, listener.request, {}, 1);
|
||||
client.restartListeners({});
|
||||
auto stale = listener.request;
|
||||
client.pushNotificationReceived({
|
||||
{"key", key.toString() },
|
||||
{"epoch", "intervening-epoch"},
|
||||
{"revision", "1" }
|
||||
});
|
||||
finishSync(client, stale, {}, 1);
|
||||
finishSync(client, listener.request, {}, 1, true, true, "current-epoch");
|
||||
CPPUNIT_ASSERT(listener.opstate->ok.load());
|
||||
CPPUNIT_ASSERT_EQUAL(std::string("current-epoch"), listener.syncEpoch);
|
||||
auto current = listener.request;
|
||||
client.pushNotificationReceived({
|
||||
{"key", key.toString() },
|
||||
{"epoch", "intervening-epoch"},
|
||||
{"revision", "2" }
|
||||
});
|
||||
CPPUNIT_ASSERT(listener.request != current);
|
||||
CPPUNIT_ASSERT(listener.request->body_.find("current-epoch") != std::string::npos);
|
||||
finishSync(client, listener.request, {}, 1, true, false, "current-epoch");
|
||||
CPPUNIT_ASSERT(listener.opstate->ok.load());
|
||||
}
|
||||
|
||||
void
|
||||
DhtProxyClientTester::testSyncEditsAndDuplicateValues()
|
||||
{
|
||||
DhtProxyClient client({}, {}, [] {}, "", "OpenDHT-Test", "client-id", "push-token");
|
||||
client.httpContext_.stop();
|
||||
client.httpClientThread_.join();
|
||||
client.proxyUrl_ = "http://127.0.0.1:1";
|
||||
auto key = InfoHash::get("snapshot-edits");
|
||||
unsigned additions = 0;
|
||||
unsigned expirations = 0;
|
||||
client.listen(key,
|
||||
[&](const std::vector<Sp<Value>>& values, bool expired) {
|
||||
(expired ? expirations : additions) += values.size();
|
||||
return true;
|
||||
},
|
||||
{},
|
||||
{});
|
||||
auto& listener = client.searches_.at(key).listeners.begin()->second;
|
||||
auto identity = dht::crypto::generateIdentity("SnapshotEditAuthor");
|
||||
auto original = std::make_shared<Value>("original");
|
||||
original->id = 42;
|
||||
original->seq = 1;
|
||||
original->sign(*identity.first);
|
||||
finishSync(client, listener.request, {original}, 1);
|
||||
client.restartListeners({});
|
||||
finishSync(client, listener.request, {}, 1, true, false);
|
||||
CPPUNIT_ASSERT_EQUAL(1u, additions);
|
||||
auto edited = std::make_shared<Value>("edited");
|
||||
edited->id = 42;
|
||||
edited->seq = 2;
|
||||
edited->sign(*identity.first);
|
||||
client.pushNotificationReceived({
|
||||
{"key", key.toString()},
|
||||
{"epoch", "test-epoch" },
|
||||
{"revision", "2" }
|
||||
});
|
||||
finishSync(client, listener.request, {edited}, 2);
|
||||
CPPUNIT_ASSERT_EQUAL(2u, additions);
|
||||
CPPUNIT_ASSERT_EQUAL(0u, expirations);
|
||||
CPPUNIT_ASSERT(client.getLocalById(key, 42)->data == edited->data);
|
||||
client.pushNotificationReceived({
|
||||
{"key", key.toString()},
|
||||
{"epoch", "test-epoch" },
|
||||
{"revision", "1" }
|
||||
});
|
||||
CPPUNIT_ASSERT(client.getLocalById(key, 42)->data == edited->data);
|
||||
}
|
||||
|
||||
void
|
||||
DhtProxyClientTester::testIncompleteSnapshotDoesNotReconcile()
|
||||
{
|
||||
DhtProxyClient client({}, {}, [] {}, "", "OpenDHT-Test", "client-id", "push-token");
|
||||
client.httpContext_.stop();
|
||||
client.httpClientThread_.join();
|
||||
client.proxyUrl_ = "http://127.0.0.1:1";
|
||||
auto key = InfoHash::get("incomplete-snapshot");
|
||||
client.listen(key, [](const std::vector<Sp<Value>>&, bool) { return true; }, {}, {});
|
||||
auto& listener = client.searches_.at(key).listeners.begin()->second;
|
||||
auto value = std::make_shared<Value>("keep-until-complete");
|
||||
value->id = 42;
|
||||
finishSync(client, listener.request, {value}, 1);
|
||||
listener.opstate->ok = false;
|
||||
client.restartListeners({});
|
||||
finishSync(client, listener.request, {}, 2, false);
|
||||
CPPUNIT_ASSERT(client.getLocalById(key, 42));
|
||||
CPPUNIT_ASSERT(!listener.opstate->ok.load());
|
||||
client.handleResubscribe({}, key, 1, listener.opstate);
|
||||
auto recovery = listener.request;
|
||||
finishSync(client, recovery, {}, 2);
|
||||
CPPUNIT_ASSERT(!client.getLocalById(key, 42));
|
||||
}
|
||||
void
|
||||
DhtProxyClientTester::testSubscriptionDeadline()
|
||||
{
|
||||
DhtProxyClient client({}, {}, [] {}, "", "OpenDHT-Test", "client-id", "push-token");
|
||||
client.httpContext_.stop();
|
||||
client.httpClientThread_.join();
|
||||
client.proxyUrl_ = "http://127.0.0.1:1";
|
||||
client.subscriptionRequestTimeout_ = std::chrono::seconds(0);
|
||||
auto key = InfoHash::get("subscription-deadline");
|
||||
client.listen(key, [](const std::vector<Sp<Value>>&, bool) { return true; }, {}, {});
|
||||
auto& listener = client.searches_.at(key).listeners.begin()->second;
|
||||
auto request = listener.request;
|
||||
CPPUNIT_ASSERT(listener.opstate->inFlight.load());
|
||||
client.httpContext_.restart();
|
||||
client.httpContext_.poll();
|
||||
client.httpContext_.stop();
|
||||
client.periodic(nullptr, 0, {}, clock::now());
|
||||
CPPUNIT_ASSERT(!listener.opstate->inFlight.load());
|
||||
CPPUNIT_ASSERT(!listener.opstate->ok.load());
|
||||
CPPUNIT_ASSERT(request->finishing_.load());
|
||||
CPPUNIT_ASSERT(listener.needsRecovery);
|
||||
CPPUNIT_ASSERT(listener.refreshSubscriberTimer->expiry() <= system_clock::now() + std::chrono::seconds(2));
|
||||
client.handleResubscribe({}, key, 1, listener.opstate);
|
||||
CPPUNIT_ASSERT(listener.request != request);
|
||||
}
|
||||
|
||||
void
|
||||
DhtProxyClientTester::testInvalidPushRevision()
|
||||
{
|
||||
DhtProxyClient client({}, {}, [] {}, "", "OpenDHT-Test", "client-id", "push-token");
|
||||
client.httpContext_.stop();
|
||||
client.httpClientThread_.join();
|
||||
client.proxyUrl_ = "http://127.0.0.1:1";
|
||||
auto key = InfoHash::get("invalid-push-revision");
|
||||
client.listen(key, [](const std::vector<Sp<Value>>&, bool) { return true; }, {}, {});
|
||||
auto& listener = client.searches_.at(key).listeners.begin()->second;
|
||||
finishSync(client, listener.request, {}, 5);
|
||||
auto request = listener.request;
|
||||
for (const auto& revision : {"-1", "+6", " 6", "6 ", "6x", "", "18446744073709551616"}) {
|
||||
CPPUNIT_ASSERT(client.pushNotificationReceived({
|
||||
{"key", key.toString()},
|
||||
{"epoch", "test-epoch" },
|
||||
{"revision", revision }
|
||||
})
|
||||
== PushNotificationResult::Error);
|
||||
CPPUNIT_ASSERT_EQUAL(uint64_t(5), listener.observedRevision);
|
||||
CPPUNIT_ASSERT_EQUAL(uint64_t(5), listener.syncRevision);
|
||||
CPPUNIT_ASSERT(listener.request == request);
|
||||
CPPUNIT_ASSERT(!listener.needsRecovery);
|
||||
}
|
||||
client.pushNotificationReceived({
|
||||
{"key", key.toString()},
|
||||
{"epoch", "test-epoch" },
|
||||
{"revision", "6" }
|
||||
});
|
||||
CPPUNIT_ASSERT(listener.request != request);
|
||||
finishSync(client, listener.request, {}, 6);
|
||||
CPPUNIT_ASSERT(listener.opstate->ok.load());
|
||||
}
|
||||
|
||||
void
|
||||
DhtProxyClientTester::testRecoveryBackoff()
|
||||
{
|
||||
DhtProxyClient client({}, {}, [] {}, "", "OpenDHT-Test", "client-id", "push-token");
|
||||
client.httpContext_.stop();
|
||||
client.httpClientThread_.join();
|
||||
client.proxyUrl_ = "http://127.0.0.1:1";
|
||||
auto key = InfoHash::get("recovery-backoff");
|
||||
client.listen(key, [](const std::vector<Sp<Value>>&, bool) { return true; }, {}, {});
|
||||
auto& listener = client.searches_.at(key).listeners.begin()->second;
|
||||
for (unsigned attempt = 0; attempt < 8; ++attempt) {
|
||||
auto request = listener.request;
|
||||
auto generation = listener.opstate->generation.load();
|
||||
Json::Value marker;
|
||||
marker["sync"]["epoch"] = "test-epoch";
|
||||
marker["sync"]["revision"] = Json::UInt64(1);
|
||||
marker["sync"]["snapshot"] = true;
|
||||
marker["sync"]["complete"] = false;
|
||||
marker["sync"]["retry"] = Json::Value(Json::objectValue);
|
||||
auto body = Json::writeString(client.jsonBuilder_, marker) + "\n";
|
||||
request->cbs_.on_body(body.data(), body.size());
|
||||
dht::http::Response response;
|
||||
response.status_code = 200;
|
||||
request->cbs_.on_state_change(dht::http::Request::State::DONE, response);
|
||||
client.periodic(nullptr, 0, {}, clock::now());
|
||||
CPPUNIT_ASSERT(listener.request == request);
|
||||
CPPUNIT_ASSERT_EQUAL(generation, listener.opstate->generation.load());
|
||||
CPPUNIT_ASSERT(!listener.opstate->inFlight.load());
|
||||
CPPUNIT_ASSERT(!listener.opstate->ok.load());
|
||||
CPPUNIT_ASSERT(listener.retryDelay <= std::chrono::seconds(60));
|
||||
client.handleResubscribe({}, key, 1, listener.opstate);
|
||||
CPPUNIT_ASSERT(listener.request != request);
|
||||
}
|
||||
CPPUNIT_ASSERT(listener.retryDelay == std::chrono::seconds(60));
|
||||
finishSync(client, listener.request, {}, 1);
|
||||
CPPUNIT_ASSERT(listener.retryDelay == std::chrono::seconds(1));
|
||||
CPPUNIT_ASSERT(listener.refreshSubscriberTimer->expiry() > system_clock::now() + std::chrono::minutes(1));
|
||||
}
|
||||
|
||||
void
|
||||
DhtProxyClientTester::testContiguousExpiration()
|
||||
{
|
||||
DhtProxyClient client({}, {}, [] {}, "", "OpenDHT-Test", "client-id", "push-token");
|
||||
client.httpContext_.stop();
|
||||
client.httpClientThread_.join();
|
||||
client.proxyUrl_ = "http://127.0.0.1:1";
|
||||
auto key = InfoHash::get("contiguous-expiration");
|
||||
client.listen(key, [](const std::vector<Sp<Value>>&, bool) { return true; }, {}, {});
|
||||
auto& listener = client.searches_.at(key).listeners.begin()->second;
|
||||
auto first = std::make_shared<Value>("first");
|
||||
first->id = 42;
|
||||
auto second = std::make_shared<Value>("second");
|
||||
second->id = 43;
|
||||
finishSync(client, listener.request, {first, second}, 5);
|
||||
CPPUNIT_ASSERT(listener.cache.onValuesAdded({first, second}, system_clock::now()));
|
||||
auto request = listener.request;
|
||||
client.pushNotificationReceived({
|
||||
{"key", key.toString()},
|
||||
{"epoch", "test-epoch" },
|
||||
{"revision", "6" },
|
||||
{"expired", "true" },
|
||||
{"ids", "42,43" }
|
||||
});
|
||||
client.periodic(nullptr, 0, {}, clock::now());
|
||||
CPPUNIT_ASSERT(listener.request == request);
|
||||
CPPUNIT_ASSERT(!client.getLocalById(key, 42));
|
||||
CPPUNIT_ASSERT(!client.getLocalById(key, 43));
|
||||
CPPUNIT_ASSERT_EQUAL(uint64_t(6), listener.syncRevision);
|
||||
client.resubscribe(key, 1, listener);
|
||||
finishSync(client, listener.request, {}, 6, true, false);
|
||||
CPPUNIT_ASSERT(!client.getLocalById(key, 42));
|
||||
CPPUNIT_ASSERT(!client.getLocalById(key, 43));
|
||||
CPPUNIT_ASSERT(listener.opstate->ok.load());
|
||||
request = listener.request;
|
||||
client.pushNotificationReceived({
|
||||
{"key", key.toString()},
|
||||
{"epoch", "test-epoch" },
|
||||
{"revision", "8" },
|
||||
{"exp", "42" }
|
||||
});
|
||||
CPPUNIT_ASSERT(listener.request != request);
|
||||
CPPUNIT_ASSERT(listener.needsRecovery);
|
||||
}
|
||||
#endif
|
||||
|
||||
} // namespace test
|
||||
@@ -12,73 +12,11 @@ class DhtProxyClientTester : public CppUnit::TestFixture
|
||||
CPPUNIT_TEST_SUITE(DhtProxyClientTester);
|
||||
CPPUNIT_TEST(testResubscribeUsesKeyRoute);
|
||||
CPPUNIT_TEST(testSetPushNotificationTokenResubscribesWithNewToken);
|
||||
CPPUNIT_TEST(testRestartListenersFetchesValuesAfterFailedSubscribe);
|
||||
CPPUNIT_TEST(testReadinessShutdown);
|
||||
CPPUNIT_TEST(testReadinessReentrancy);
|
||||
CPPUNIT_TEST(testCachedReadinessDispatch);
|
||||
CPPUNIT_TEST(testReadinessCancelsAnotherListener);
|
||||
#ifdef OPENDHT_PUSH_NOTIFICATIONS
|
||||
CPPUNIT_TEST(testInitialReadinessFailures);
|
||||
CPPUNIT_TEST(testReadinessRecoveryIsOneShot);
|
||||
CPPUNIT_TEST(testWrongSessionPushDoesNotTouchSubscriptions);
|
||||
CPPUNIT_TEST(testNewSessionSynchronizesIndependently);
|
||||
CPPUNIT_TEST(testFailedTimerRetryFetchesValues);
|
||||
CPPUNIT_TEST(testFullRetryRemovesAbsentValues);
|
||||
CPPUNIT_TEST(testStreamingFallbackExpiresSnapshot);
|
||||
CPPUNIT_TEST(testLateRetryResponseDoesNotResurrectExpiredValue);
|
||||
CPPUNIT_TEST(testTruncatedRecoveryRemainsFailed);
|
||||
CPPUNIT_TEST(testTruncatedRecoveryAtEofFails);
|
||||
CPPUNIT_TEST(testMissingCompletionDoesNotReconcile);
|
||||
CPPUNIT_TEST(testSnapshotExpirationIgnoresClockRollback);
|
||||
CPPUNIT_TEST(testSupersededFailureDoesNotPoisonSuccessfulRetry);
|
||||
CPPUNIT_TEST(testSyncCoalescesConnectivityChanges);
|
||||
CPPUNIT_TEST(testTokenRotationDuringRecovery);
|
||||
CPPUNIT_TEST(testTokenRotationBackToPreviousEpoch);
|
||||
CPPUNIT_TEST(testAuthoritativeEpochAfterStalePush);
|
||||
CPPUNIT_TEST(testSyncEditsAndDuplicateValues);
|
||||
CPPUNIT_TEST(testIncompleteSnapshotDoesNotReconcile);
|
||||
CPPUNIT_TEST(testSubscriptionDeadline);
|
||||
CPPUNIT_TEST(testInvalidPushRevision);
|
||||
CPPUNIT_TEST(testRecoveryBackoff);
|
||||
CPPUNIT_TEST(testContiguousExpiration);
|
||||
CPPUNIT_TEST(testRejectsUnrevisionedPushes);
|
||||
#endif
|
||||
CPPUNIT_TEST_SUITE_END();
|
||||
|
||||
public:
|
||||
void testResubscribeUsesKeyRoute();
|
||||
void testSetPushNotificationTokenResubscribesWithNewToken();
|
||||
void testRestartListenersFetchesValuesAfterFailedSubscribe();
|
||||
void testReadinessShutdown();
|
||||
void testReadinessReentrancy();
|
||||
void testCachedReadinessDispatch();
|
||||
void testReadinessCancelsAnotherListener();
|
||||
#ifdef OPENDHT_PUSH_NOTIFICATIONS
|
||||
void testInitialReadinessFailures();
|
||||
void testReadinessRecoveryIsOneShot();
|
||||
void testWrongSessionPushDoesNotTouchSubscriptions();
|
||||
void testNewSessionSynchronizesIndependently();
|
||||
void testFailedTimerRetryFetchesValues();
|
||||
void testFullRetryRemovesAbsentValues();
|
||||
void testStreamingFallbackExpiresSnapshot();
|
||||
void testLateRetryResponseDoesNotResurrectExpiredValue();
|
||||
void testTruncatedRecoveryRemainsFailed();
|
||||
void testTruncatedRecoveryAtEofFails();
|
||||
void testMissingCompletionDoesNotReconcile();
|
||||
void testSnapshotExpirationIgnoresClockRollback();
|
||||
void testSupersededFailureDoesNotPoisonSuccessfulRetry();
|
||||
void testSyncCoalescesConnectivityChanges();
|
||||
void testTokenRotationDuringRecovery();
|
||||
void testTokenRotationBackToPreviousEpoch();
|
||||
void testAuthoritativeEpochAfterStalePush();
|
||||
void testSyncEditsAndDuplicateValues();
|
||||
void testIncompleteSnapshotDoesNotReconcile();
|
||||
void testSubscriptionDeadline();
|
||||
void testInvalidPushRevision();
|
||||
void testRecoveryBackoff();
|
||||
void testContiguousExpiration();
|
||||
void testRejectsUnrevisionedPushes();
|
||||
#endif
|
||||
};
|
||||
|
||||
} // namespace test
|
||||
+1
-793
@@ -16,58 +16,6 @@ using namespace std::literals;
|
||||
|
||||
namespace test {
|
||||
CPPUNIT_TEST_SUITE_REGISTRATION(DhtRunnerTester);
|
||||
CPPUNIT_TEST_SUITE_REGISTRATION(DhtRunnerPushTester);
|
||||
|
||||
namespace {
|
||||
#if defined(OPENDHT_PROXY_CLIENT) && defined(OPENDHT_PUSH_NOTIFICATIONS)
|
||||
constexpr auto stoppedPushResult = dht::PushNotificationResult::IgnoredStopped;
|
||||
#else
|
||||
constexpr auto stoppedPushResult = dht::PushNotificationResult::IgnoredDisabled;
|
||||
#endif
|
||||
} // namespace
|
||||
|
||||
void
|
||||
DhtRunnerPushTester::testPushBeforeRun()
|
||||
{
|
||||
dht::DhtRunner node;
|
||||
auto result = node.pushNotificationReceived({});
|
||||
CPPUNIT_ASSERT(result.wait_for(0s) == std::future_status::ready);
|
||||
CPPUNIT_ASSERT(result.get() == stoppedPushResult);
|
||||
}
|
||||
|
||||
void
|
||||
DhtRunnerPushTester::testPushAfterJoin()
|
||||
{
|
||||
dht::DhtRunner node;
|
||||
node.run(0);
|
||||
node.join();
|
||||
auto result = node.pushNotificationReceived({});
|
||||
CPPUNIT_ASSERT(result.wait_for(0s) == std::future_status::ready);
|
||||
CPPUNIT_ASSERT(result.get() == stoppedPushResult);
|
||||
}
|
||||
|
||||
void
|
||||
DhtRunnerPushTester::testPushBurstAfterJoin()
|
||||
{
|
||||
dht::DhtRunner node;
|
||||
node.run(0);
|
||||
node.join();
|
||||
for (unsigned i = 0; i < 32; ++i) {
|
||||
auto result = node.pushNotificationReceived({});
|
||||
CPPUNIT_ASSERT(result.wait_for(0s) == std::future_status::ready);
|
||||
CPPUNIT_ASSERT(result.get() == stoppedPushResult);
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
DhtRunnerPushTester::testPushWhileRunning()
|
||||
{
|
||||
dht::DhtRunner node;
|
||||
node.run(0);
|
||||
auto result = node.pushNotificationReceived({});
|
||||
CPPUNIT_ASSERT(result.wait_for(5s) == std::future_status::ready);
|
||||
CPPUNIT_ASSERT(result.get() != dht::PushNotificationResult::IgnoredStopped);
|
||||
}
|
||||
|
||||
template<typename T>
|
||||
T
|
||||
@@ -77,152 +25,6 @@ getFutureValue(std::future<T> future, std::chrono::steady_clock::duration timeou
|
||||
return future.get();
|
||||
}
|
||||
|
||||
void
|
||||
DhtRunnerTester::testListenReadiness()
|
||||
{
|
||||
auto key = dht::InfoHash::get("native-listen-readiness");
|
||||
auto value = std::make_shared<dht::Value>("initial-value");
|
||||
value->id = 42;
|
||||
std::promise<bool> stored;
|
||||
auto storedFuture = stored.get_future();
|
||||
node2.put(key, value, [&](bool ok) { stored.set_value(ok); });
|
||||
CPPUNIT_ASSERT(storedFuture.wait_for(10s) == std::future_status::ready);
|
||||
CPPUNIT_ASSERT(storedFuture.get());
|
||||
bool received = false;
|
||||
std::promise<bool> ready;
|
||||
auto readyFuture = ready.get_future();
|
||||
auto token = node1.listen(
|
||||
key,
|
||||
[&](const std::vector<dht::Sp<dht::Value>>& values, bool expired) {
|
||||
if (!expired)
|
||||
for (const auto& current : values)
|
||||
received = received || current->id == 42;
|
||||
return true;
|
||||
},
|
||||
{},
|
||||
{},
|
||||
[&](bool ok) { ready.set_value(ok && received); });
|
||||
CPPUNIT_ASSERT(readyFuture.wait_for(10s) == std::future_status::ready);
|
||||
CPPUNIT_ASSERT(readyFuture.get());
|
||||
node1.cancelListen(key, token.get());
|
||||
|
||||
dht::DhtRunner isolated;
|
||||
isolated.run(0);
|
||||
std::promise<bool> emptyReady;
|
||||
auto emptyFuture = emptyReady.get_future();
|
||||
auto emptyToken = isolated.listen(
|
||||
key,
|
||||
[](const std::vector<dht::Sp<dht::Value>>&, bool) { return true; },
|
||||
{},
|
||||
{},
|
||||
[&](bool ok) { emptyReady.set_value(ok); });
|
||||
CPPUNIT_ASSERT(emptyFuture.wait_for(5s) == std::future_status::ready);
|
||||
CPPUNIT_ASSERT(emptyFuture.get());
|
||||
isolated.cancelListen(key, emptyToken.get());
|
||||
isolated.join();
|
||||
}
|
||||
|
||||
void
|
||||
DhtRunnerTester::testListenReadinessStopped()
|
||||
{
|
||||
dht::DhtRunner stopped;
|
||||
auto key = dht::InfoHash::get("stopped-listen-readiness");
|
||||
std::vector<bool> results;
|
||||
unsigned deliveries = 0;
|
||||
auto listen = [&] {
|
||||
auto token = stopped.listen(
|
||||
key,
|
||||
[&](const std::vector<dht::Sp<dht::Value>>&, bool) {
|
||||
++deliveries;
|
||||
return true;
|
||||
},
|
||||
{},
|
||||
{},
|
||||
[&](bool ok) { results.push_back(ok); });
|
||||
CPPUNIT_ASSERT(token.wait_for(0s) == std::future_status::ready);
|
||||
CPPUNIT_ASSERT_EQUAL(size_t(0), token.get());
|
||||
};
|
||||
listen();
|
||||
CPPUNIT_ASSERT(results == std::vector<bool>({false}));
|
||||
stopped.run(0);
|
||||
stopped.join();
|
||||
listen();
|
||||
CPPUNIT_ASSERT(results == std::vector<bool>({false, false}));
|
||||
CPPUNIT_ASSERT_EQUAL(0u, deliveries);
|
||||
}
|
||||
|
||||
void
|
||||
DhtRunnerTester::testListenReadinessInvalidKey()
|
||||
{
|
||||
auto ready = std::make_shared<std::promise<bool>>();
|
||||
auto future = ready->get_future();
|
||||
auto deliveries = std::make_shared<std::atomic<unsigned>>(0);
|
||||
auto token = node1.listen(
|
||||
dht::InfoHash {},
|
||||
[deliveries](const std::vector<dht::Sp<dht::Value>>&, bool) {
|
||||
++*deliveries;
|
||||
return true;
|
||||
},
|
||||
{},
|
||||
{},
|
||||
[ready](bool ok) { ready->set_value(ok); });
|
||||
CPPUNIT_ASSERT_EQUAL(size_t(0), getFutureValue(std::move(token), 5s));
|
||||
CPPUNIT_ASSERT(!getFutureValue(std::move(future), 5s));
|
||||
CPPUNIT_ASSERT_EQUAL(0u, deliveries->load());
|
||||
}
|
||||
|
||||
void
|
||||
DhtRunnerTester::testListenReadinessFilteredSignedValues()
|
||||
{
|
||||
dht::DhtRunner source;
|
||||
dht::DhtRunner::Config config;
|
||||
config.dht_config.id = dht::crypto::generateIdentity();
|
||||
source.run(0, config);
|
||||
auto bound = node1.getBound();
|
||||
if (bound.isUnspecified())
|
||||
bound.setLoopback();
|
||||
source.bootstrap(bound);
|
||||
auto key = dht::InfoHash::get("signed-listen-readiness");
|
||||
auto initial = std::make_shared<dht::Value>("initial-signed-value");
|
||||
initial->id = 42;
|
||||
auto stored = std::make_shared<std::promise<bool>>();
|
||||
auto storedFuture = stored->get_future();
|
||||
source.putSigned(key, initial, [stored](bool ok) { stored->set_value(ok); });
|
||||
CPPUNIT_ASSERT(getFutureValue(std::move(storedFuture), 10s));
|
||||
|
||||
auto ignored = std::make_shared<dht::Value>("filtered-value");
|
||||
ignored->id = 43;
|
||||
auto ignoredStored = std::make_shared<std::promise<bool>>();
|
||||
auto ignoredFuture = ignoredStored->get_future();
|
||||
source.put(key, ignored, [ignoredStored](bool ok) { ignoredStored->set_value(ok); });
|
||||
CPPUNIT_ASSERT(getFutureValue(std::move(ignoredFuture), 10s));
|
||||
|
||||
struct InitialValues
|
||||
{
|
||||
unsigned count {0};
|
||||
bool valid {true};
|
||||
std::promise<bool> ready;
|
||||
};
|
||||
auto state = std::make_shared<InitialValues>();
|
||||
auto future = state->ready.get_future();
|
||||
auto token = source.listen(
|
||||
key,
|
||||
[state](const std::vector<dht::Sp<dht::Value>>& values, bool expired) {
|
||||
if (!expired)
|
||||
for (const auto& value : values) {
|
||||
++state->count;
|
||||
state->valid = state->valid && value->id == 42 && value->isSigned() && value->checkSignature();
|
||||
}
|
||||
return true;
|
||||
},
|
||||
[](const dht::Value& value) { return value.id == 42; },
|
||||
{},
|
||||
[state](bool ok) { state->ready.set_value(ok && state->valid && state->count == 1); });
|
||||
CPPUNIT_ASSERT(getFutureValue(std::move(future), 10s));
|
||||
source.cancelListen(key, getFutureValue(std::move(token), 5s));
|
||||
source.join();
|
||||
}
|
||||
|
||||
void
|
||||
DhtRunnerTester::setUp()
|
||||
{
|
||||
@@ -289,42 +91,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()
|
||||
{
|
||||
@@ -821,115 +587,6 @@ DhtRunnerTester::testSavedPortFallback()
|
||||
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()
|
||||
{
|
||||
@@ -1142,7 +799,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));
|
||||
@@ -1186,8 +843,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
|
||||
@@ -1278,451 +933,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
|
||||
@@ -10,22 +10,6 @@
|
||||
|
||||
namespace test {
|
||||
|
||||
class DhtRunnerPushTester : public CppUnit::TestFixture
|
||||
{
|
||||
CPPUNIT_TEST_SUITE(DhtRunnerPushTester);
|
||||
CPPUNIT_TEST(testPushBeforeRun);
|
||||
CPPUNIT_TEST(testPushAfterJoin);
|
||||
CPPUNIT_TEST(testPushBurstAfterJoin);
|
||||
CPPUNIT_TEST(testPushWhileRunning);
|
||||
CPPUNIT_TEST_SUITE_END();
|
||||
|
||||
public:
|
||||
void testPushBeforeRun();
|
||||
void testPushAfterJoin();
|
||||
void testPushBurstAfterJoin();
|
||||
void testPushWhileRunning();
|
||||
};
|
||||
|
||||
class DhtRunnerTester : public CppUnit::TestFixture
|
||||
{
|
||||
CPPUNIT_TEST_SUITE(DhtRunnerTester);
|
||||
@@ -34,13 +18,7 @@ class DhtRunnerTester : public CppUnit::TestFixture
|
||||
CPPUNIT_TEST(testPutDuplicate);
|
||||
CPPUNIT_TEST(testPutOverride);
|
||||
CPPUNIT_TEST(testListen);
|
||||
CPPUNIT_TEST(testListenReadiness);
|
||||
CPPUNIT_TEST(testListenReadinessStopped);
|
||||
CPPUNIT_TEST(testListenReadinessInvalidKey);
|
||||
CPPUNIT_TEST(testListenReadinessFilteredSignedValues);
|
||||
CPPUNIT_TEST(testListenValueEdit);
|
||||
CPPUNIT_TEST(testListenLotOfBytes);
|
||||
CPPUNIT_TEST(testStoreEmptyValue);
|
||||
CPPUNIT_TEST(testIdOps);
|
||||
CPPUNIT_TEST(testImportValuesPreservesRemoteQuota);
|
||||
CPPUNIT_TEST(testExportValuesUsesSystemTime);
|
||||
@@ -51,11 +29,6 @@ class DhtRunnerTester : public CppUnit::TestFixture
|
||||
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 {};
|
||||
@@ -90,10 +63,6 @@ public:
|
||||
* Test listen method
|
||||
*/
|
||||
void testListen();
|
||||
void testListenReadiness();
|
||||
void testListenReadinessStopped();
|
||||
void testListenReadinessInvalidKey();
|
||||
void testListenReadinessFilteredSignedValues();
|
||||
/**
|
||||
* Test methods requiring a node identity
|
||||
*/
|
||||
@@ -107,23 +76,10 @@ public:
|
||||
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
|
||||
*/
|
||||
|
||||
@@ -0,0 +1,280 @@
|
||||
// Copyright (c) 2014-2026 Savoir-faire Linux Inc.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
#include "test_mdns.h"
|
||||
|
||||
#include "../src/peer_discovery/dns_sd.h"
|
||||
#include "../src/peer_discovery/mdns.h"
|
||||
#include "../src/peer_discovery/mdns_transport.h"
|
||||
|
||||
#include <array>
|
||||
#include <string>
|
||||
|
||||
namespace test {
|
||||
|
||||
using namespace dht::mdns;
|
||||
|
||||
CPPUNIT_TEST_SUITE_REGISTRATION(MdnsTester);
|
||||
|
||||
void
|
||||
MdnsTester::testName()
|
||||
{
|
||||
const auto name = Name::fromString("Node._opendht._udp.local.");
|
||||
CPPUNIT_ASSERT_EQUAL(std::string("Node._opendht._udp.local."), name.toString());
|
||||
CPPUNIT_ASSERT(name == Name::fromString("node._OpenDHT._UDP.LOCAL"));
|
||||
CPPUNIT_ASSERT_THROW(Name::fromString("bad..local."), ParseError);
|
||||
CPPUNIT_ASSERT_THROW(Name::fromString(std::string(64, 'a') + ".local."), ParseError);
|
||||
}
|
||||
|
||||
void
|
||||
MdnsTester::testRecordRoundTrip()
|
||||
{
|
||||
const auto service = Name::fromString("_opendht._udp.local.");
|
||||
const auto instance = Name::fromString("0123456789abcdef0123456789abcdef01234567._opendht._udp.local.");
|
||||
const auto host = Name::fromString("0123456789abcdef0123456789abcdef01234567.local.");
|
||||
|
||||
Message message;
|
||||
message.response = true;
|
||||
message.authoritative = true;
|
||||
message.answers.push_back({service, Type::PTR, CLASS_IN, false, 4500, PtrData {instance}});
|
||||
message.additionals.push_back({
|
||||
instance, Type::SRV, CLASS_IN, true, 120, SrvData {0, 0, 4222, host}
|
||||
});
|
||||
message.additionals.push_back(
|
||||
{instance,
|
||||
Type::TXT,
|
||||
CLASS_IN,
|
||||
true,
|
||||
4500,
|
||||
TxtData {{{'t', 'x', 't', 'v', 'e', 'r', 's', '=', '1'}, {'n', 'e', 't', '=', '0'}}}});
|
||||
message.additionals.push_back({host, Type::A, CLASS_IN, true, 120, AData {{192, 0, 2, 1}}});
|
||||
message.additionals.push_back(
|
||||
{host, Type::AAAA, CLASS_IN, true, 120, AaaaData {{0x20, 0x01, 0x0d, 0xb8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1}}});
|
||||
message.additionals.push_back({
|
||||
host, Type::NSEC, CLASS_IN, true, 120, NsecData {host, {Type::A, Type::AAAA}}
|
||||
});
|
||||
|
||||
const auto packet = encode(message);
|
||||
const auto decoded = decode(packet.data(), packet.size());
|
||||
|
||||
CPPUNIT_ASSERT(decoded.response);
|
||||
CPPUNIT_ASSERT(decoded.authoritative);
|
||||
CPPUNIT_ASSERT_EQUAL(size_t(1), decoded.answers.size());
|
||||
CPPUNIT_ASSERT_EQUAL(size_t(5), decoded.additionals.size());
|
||||
CPPUNIT_ASSERT(decoded == message);
|
||||
}
|
||||
|
||||
void
|
||||
MdnsTester::testCompression()
|
||||
{
|
||||
Message message;
|
||||
message.questions.push_back({Name::fromString("_opendht._udp.local."), Type::PTR, CLASS_IN, false});
|
||||
message.questions.push_back({Name::fromString("node._opendht._udp.local."), Type::SRV, CLASS_IN, false});
|
||||
|
||||
const auto packet = encode(message);
|
||||
CPPUNIT_ASSERT(std::find(packet.begin(), packet.end(), uint8_t {0xc0}) != packet.end());
|
||||
CPPUNIT_ASSERT(decode(packet.data(), packet.size()) == message);
|
||||
}
|
||||
|
||||
void
|
||||
MdnsTester::testMalformedPackets()
|
||||
{
|
||||
const std::array<uint8_t, 3> truncated {
|
||||
{0, 0, 0}
|
||||
};
|
||||
CPPUNIT_ASSERT_THROW(decode(truncated.data(), truncated.size()), ParseError);
|
||||
|
||||
const std::array<uint8_t, 18> compressionLoop {
|
||||
{
|
||||
0, 0,
|
||||
0, 0,
|
||||
0, 1,
|
||||
0, 0,
|
||||
0, 0,
|
||||
0, 0,
|
||||
0xc0, 0x0c,
|
||||
0, static_cast<uint8_t>(Type::PTR),
|
||||
0, CLASS_IN,
|
||||
}
|
||||
};
|
||||
CPPUNIT_ASSERT_THROW(decode(compressionLoop.data(), compressionLoop.size()), ParseError);
|
||||
}
|
||||
|
||||
void
|
||||
MdnsTester::testPacketSizeLimit()
|
||||
{
|
||||
std::array<uint8_t, MAX_PACKET_SIZE + 1> oversized {};
|
||||
CPPUNIT_ASSERT_THROW(decode(oversized.data(), oversized.size()), ParseError);
|
||||
}
|
||||
|
||||
void
|
||||
MdnsTester::testDnsSdAdvertisement()
|
||||
{
|
||||
dns_sd::Service service;
|
||||
service.nodeId = dht::InfoHash("0123456789abcdef0123456789abcdef01234567");
|
||||
service.network = 42;
|
||||
service.port = 4222;
|
||||
service.addresses.emplace_back(AData {
|
||||
{192, 0, 2, 1}
|
||||
});
|
||||
service.addresses.emplace_back(AaaaData {
|
||||
{0x20, 0x01, 0x0d, 0xb8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1}
|
||||
});
|
||||
|
||||
const auto message = dns_sd::announcement(service);
|
||||
CPPUNIT_ASSERT(message.response);
|
||||
CPPUNIT_ASSERT(message.authoritative);
|
||||
CPPUNIT_ASSERT_EQUAL(size_t(1), message.answers.size());
|
||||
CPPUNIT_ASSERT_EQUAL(size_t(4), message.additionals.size());
|
||||
CPPUNIT_ASSERT_EQUAL(std::string("_opendht._udp.local."), message.answers.front().name.toString());
|
||||
|
||||
const auto resolved = dns_sd::resolve(message);
|
||||
CPPUNIT_ASSERT(resolved);
|
||||
CPPUNIT_ASSERT_EQUAL(service.nodeId, resolved->nodeId);
|
||||
CPPUNIT_ASSERT_EQUAL(service.network, resolved->network);
|
||||
CPPUNIT_ASSERT_EQUAL(service.port, resolved->port);
|
||||
CPPUNIT_ASSERT_EQUAL(service.addresses.size(), resolved->addresses.size());
|
||||
}
|
||||
|
||||
void
|
||||
MdnsTester::testDnsSdGoodbye()
|
||||
{
|
||||
dns_sd::Service service;
|
||||
service.nodeId = dht::InfoHash("0123456789abcdef0123456789abcdef01234567");
|
||||
service.port = 4222;
|
||||
service.addresses.emplace_back(AData {
|
||||
{192, 0, 2, 1}
|
||||
});
|
||||
|
||||
const auto message = dns_sd::goodbye(service);
|
||||
CPPUNIT_ASSERT(std::all_of(message.answers.begin(), message.answers.end(), [](const auto& record) {
|
||||
return record.ttl == 0;
|
||||
}));
|
||||
CPPUNIT_ASSERT(std::all_of(message.additionals.begin(), message.additionals.end(), [](const auto& record) {
|
||||
return record.ttl == 0;
|
||||
}));
|
||||
}
|
||||
|
||||
void
|
||||
MdnsTester::testDnsSdValidation()
|
||||
{
|
||||
dns_sd::Service service;
|
||||
service.nodeId = dht::InfoHash("0123456789abcdef0123456789abcdef01234567");
|
||||
service.network = 42;
|
||||
service.port = 4222;
|
||||
service.addresses.emplace_back(AData {
|
||||
{192, 0, 2, 1}
|
||||
});
|
||||
|
||||
auto message = dns_sd::announcement(service);
|
||||
auto& txt = std::get<TxtData>(message.additionals[1].data);
|
||||
txt.strings.push_back({'n', 'e', 't', '=', '4', '3'});
|
||||
CPPUNIT_ASSERT(not dns_sd::resolve(message));
|
||||
|
||||
message = dns_sd::announcement(service);
|
||||
auto& srv = std::get<SrvData>(message.additionals[0].data);
|
||||
srv.target = Name::fromString("other.local.");
|
||||
CPPUNIT_ASSERT(not dns_sd::resolve(message));
|
||||
|
||||
CPPUNIT_ASSERT_THROW(dns_sd::announcement({}), ParseError);
|
||||
}
|
||||
|
||||
void
|
||||
MdnsTester::testDnsSdQueryResponse()
|
||||
{
|
||||
dns_sd::Service service;
|
||||
service.nodeId = dht::InfoHash("0123456789abcdef0123456789abcdef01234567");
|
||||
service.network = 42;
|
||||
service.port = 4222;
|
||||
service.addresses.emplace_back(AData {
|
||||
{192, 0, 2, 1}
|
||||
});
|
||||
|
||||
const auto query = dns_sd::browseQuery();
|
||||
CPPUNIT_ASSERT(not query.response);
|
||||
CPPUNIT_ASSERT_EQUAL(size_t(1), query.questions.size());
|
||||
CPPUNIT_ASSERT(query.questions.front().name == dns_sd::serviceName());
|
||||
CPPUNIT_ASSERT(query.questions.front().type == Type::PTR);
|
||||
|
||||
const auto response = dns_sd::respond(query, service);
|
||||
CPPUNIT_ASSERT(response);
|
||||
CPPUNIT_ASSERT(dns_sd::resolve(*response));
|
||||
|
||||
auto knownAnswerQuery = query;
|
||||
knownAnswerQuery.answers = response->answers;
|
||||
CPPUNIT_ASSERT(not dns_sd::respond(knownAnswerQuery, service));
|
||||
|
||||
knownAnswerQuery.answers.front().ttl = dns_sd::SERVICE_RECORD_TTL / 2;
|
||||
CPPUNIT_ASSERT(dns_sd::respond(knownAnswerQuery, service));
|
||||
|
||||
Message metaQuery;
|
||||
const auto metaName = Name::fromString("_services._dns-sd._udp.local.");
|
||||
metaQuery.questions.push_back({metaName, Type::PTR, CLASS_IN, false});
|
||||
const auto metaResponse = dns_sd::respond(metaQuery, service);
|
||||
CPPUNIT_ASSERT(metaResponse);
|
||||
CPPUNIT_ASSERT_EQUAL(size_t(1), metaResponse->answers.size());
|
||||
CPPUNIT_ASSERT(metaResponse->answers.front().name == metaName);
|
||||
CPPUNIT_ASSERT(std::get<PtrData>(metaResponse->answers.front().data).name == dns_sd::serviceName());
|
||||
CPPUNIT_ASSERT(metaResponse->additionals.empty());
|
||||
|
||||
metaQuery.answers = metaResponse->answers;
|
||||
CPPUNIT_ASSERT(not dns_sd::respond(metaQuery, service));
|
||||
}
|
||||
|
||||
void
|
||||
MdnsTester::testDnsSdCache()
|
||||
{
|
||||
dns_sd::Service service;
|
||||
service.nodeId = dht::InfoHash("0123456789abcdef0123456789abcdef01234567");
|
||||
service.network = 42;
|
||||
service.port = 4222;
|
||||
service.addresses.emplace_back(AData {
|
||||
{192, 0, 2, 1}
|
||||
});
|
||||
|
||||
auto now = dns_sd::Cache::Clock::now();
|
||||
dns_sd::Cache cache([&] { return now; });
|
||||
auto ptr = dns_sd::announcement(service);
|
||||
auto details = ptr;
|
||||
ptr.additionals.clear();
|
||||
details.answers.clear();
|
||||
|
||||
CPPUNIT_ASSERT(cache.update(ptr).empty());
|
||||
const auto discovered = cache.update(details);
|
||||
CPPUNIT_ASSERT_EQUAL(size_t(1), discovered.size());
|
||||
CPPUNIT_ASSERT_EQUAL(service.nodeId, discovered.front().nodeId);
|
||||
CPPUNIT_ASSERT_EQUAL(size_t(1), cache.services().size());
|
||||
|
||||
now += std::chrono::seconds(dns_sd::HOST_RECORD_TTL + 1);
|
||||
CPPUNIT_ASSERT(cache.services().empty());
|
||||
}
|
||||
|
||||
void
|
||||
MdnsTester::testInterfaceScoping()
|
||||
{
|
||||
const std::vector<InterfaceAddress> addresses {
|
||||
{1, 1, asio::ip::make_address("192.0.2.1"), true, false},
|
||||
{1, 1, asio::ip::make_address("fe80::1"), true, false},
|
||||
{2, 2, asio::ip::make_address("fe80::2"), true, false},
|
||||
{3, 3, asio::ip::make_address("fe80::3"), false, false},
|
||||
{4, 4, asio::ip::make_address("::1"), true, true },
|
||||
};
|
||||
|
||||
const auto interfaces = groupInterfaces(addresses);
|
||||
CPPUNIT_ASSERT_EQUAL(size_t(2), interfaces.size());
|
||||
CPPUNIT_ASSERT_EQUAL(InterfaceId {1}, interfaces[0].id);
|
||||
CPPUNIT_ASSERT_EQUAL(size_t(2), interfaces[0].addresses.size());
|
||||
CPPUNIT_ASSERT_EQUAL(InterfaceId {2}, interfaces[1].id);
|
||||
CPPUNIT_ASSERT_EQUAL(size_t(1), interfaces[1].addresses.size());
|
||||
|
||||
dns_sd::Service service;
|
||||
service.nodeId = dht::InfoHash("0123456789abcdef0123456789abcdef01234567");
|
||||
service.port = 4222;
|
||||
service.addresses = interfaces[0].addresses;
|
||||
const auto announcement = dns_sd::announcement(service);
|
||||
const auto resolved = dns_sd::resolve(announcement);
|
||||
CPPUNIT_ASSERT(resolved);
|
||||
CPPUNIT_ASSERT(resolved->addresses == interfaces[0].addresses);
|
||||
}
|
||||
|
||||
} // namespace test
|
||||
@@ -0,0 +1,40 @@
|
||||
// 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 MdnsTester : public CppUnit::TestFixture
|
||||
{
|
||||
CPPUNIT_TEST_SUITE(MdnsTester);
|
||||
CPPUNIT_TEST(testName);
|
||||
CPPUNIT_TEST(testRecordRoundTrip);
|
||||
CPPUNIT_TEST(testCompression);
|
||||
CPPUNIT_TEST(testMalformedPackets);
|
||||
CPPUNIT_TEST(testPacketSizeLimit);
|
||||
CPPUNIT_TEST(testDnsSdAdvertisement);
|
||||
CPPUNIT_TEST(testDnsSdGoodbye);
|
||||
CPPUNIT_TEST(testDnsSdValidation);
|
||||
CPPUNIT_TEST(testDnsSdQueryResponse);
|
||||
CPPUNIT_TEST(testDnsSdCache);
|
||||
CPPUNIT_TEST(testInterfaceScoping);
|
||||
CPPUNIT_TEST_SUITE_END();
|
||||
|
||||
public:
|
||||
void testName();
|
||||
void testRecordRoundTrip();
|
||||
void testCompression();
|
||||
void testMalformedPackets();
|
||||
void testPacketSizeLimit();
|
||||
void testDnsSdAdvertisement();
|
||||
void testDnsSdGoodbye();
|
||||
void testDnsSdValidation();
|
||||
void testDnsSdQueryResponse();
|
||||
void testDnsSdCache();
|
||||
void testInterfaceScoping();
|
||||
};
|
||||
|
||||
} // namespace test
|
||||
@@ -94,8 +94,8 @@ public:
|
||||
return 0;
|
||||
}
|
||||
|
||||
bool hasIPv4() const override { return ipv4; }
|
||||
bool hasIPv6() const override { return ipv6; }
|
||||
bool hasIPv4() const override { return true; }
|
||||
bool hasIPv6() const override { return false; }
|
||||
const SockAddr& getBoundRef(sa_family_t family = AF_UNSPEC) const override
|
||||
{
|
||||
(void) family;
|
||||
@@ -112,8 +112,6 @@ public:
|
||||
|
||||
SockAddr bound_;
|
||||
std::vector<SentPacket> sends {};
|
||||
bool ipv4 {true};
|
||||
bool ipv6 {false};
|
||||
};
|
||||
|
||||
static Blob
|
||||
@@ -232,9 +230,7 @@ makeEngine(std::unique_ptr<TestDatagramSocket>&& socket,
|
||||
[](Sp<Node>) { return net::RequestAnswer {}; },
|
||||
[](Sp<Node>, const InfoHash&, want_t) { return net::RequestAnswer {}; },
|
||||
[](Sp<Node>, const InfoHash&, want_t, const Query&) { return net::RequestAnswer {}; },
|
||||
[](Sp<Node>, const InfoHash&, const Blob&, Tid, const Query&, int, const std::optional<net::ListenSync>&) {
|
||||
return net::RequestAnswer {};
|
||||
},
|
||||
[](Sp<Node>, const InfoHash&, const Blob&, Tid, const Query&, int) { return net::RequestAnswer {}; },
|
||||
[](Sp<Node>, const InfoHash&, const Blob&, const std::vector<Sp<Value>>&, const time_point&) {
|
||||
return net::RequestAnswer {};
|
||||
},
|
||||
@@ -256,164 +252,6 @@ addPendingGetRequest(net::NetworkEngine& engine, const InfoHash& id, const SockA
|
||||
return node;
|
||||
}
|
||||
|
||||
struct ListenSnapshotTransport
|
||||
{
|
||||
InfoHash key {InfoHash::get("initial-listen-snapshot")};
|
||||
std::vector<Value::Id> received;
|
||||
std::vector<bool> results;
|
||||
std::vector<size_t> snapshots;
|
||||
std::vector<TestDatagramSocket::SentPacket> delayedUpdates;
|
||||
TestDatagramSocket* clientPackets;
|
||||
TestDatagramSocket* serverPackets;
|
||||
SockAddr clientAddr;
|
||||
SockAddr serverAddr;
|
||||
std::unique_ptr<Dht> client;
|
||||
std::unique_ptr<Dht> server;
|
||||
size_t token {0};
|
||||
|
||||
explicit ListenSnapshotTransport(uint64_t serverSeed = 31)
|
||||
{
|
||||
Config config {};
|
||||
config.max_req_per_sec = -1;
|
||||
config.max_peer_req_per_sec = -1;
|
||||
auto clientSocket = std::make_unique<TestDatagramSocket>();
|
||||
clientPackets = clientSocket.get();
|
||||
clientAddr = clientPackets->bound_;
|
||||
client = std::make_unique<Dht>(std::move(clientSocket),
|
||||
config,
|
||||
Sp<Logger> {},
|
||||
std::make_unique<std::mt19937_64>(30));
|
||||
auto serverSocket = std::make_unique<TestDatagramSocket>();
|
||||
serverPackets = serverSocket.get();
|
||||
serverPackets->bound_.setPort(5031);
|
||||
serverAddr = serverPackets->bound_;
|
||||
server = std::make_unique<Dht>(std::move(serverSocket),
|
||||
config,
|
||||
Sp<Logger> {},
|
||||
std::make_unique<std::mt19937_64>(serverSeed));
|
||||
}
|
||||
|
||||
~ListenSnapshotTransport()
|
||||
{
|
||||
if (token)
|
||||
client->cancelListen(key, token);
|
||||
}
|
||||
|
||||
static ParsedMessage parse(const TestDatagramSocket::SentPacket& packet)
|
||||
{
|
||||
ParsedMessage message;
|
||||
message.msgpack_unpack(msgpack::unpack((const char*) packet.data.data(), packet.data.size()).get());
|
||||
return message;
|
||||
}
|
||||
|
||||
void store(size_t count, size_t payloadSize = 20)
|
||||
{
|
||||
for (size_t index = 0; index < count; ++index) {
|
||||
auto value = std::make_shared<Value>(std::string(payloadSize, 'x'));
|
||||
value->id = index + 1;
|
||||
CPPUNIT_ASSERT(server->storageStore(key, value, server->scheduler.time()));
|
||||
}
|
||||
}
|
||||
|
||||
void listen()
|
||||
{
|
||||
token = client->listen(
|
||||
key,
|
||||
[this](const std::vector<Sp<Value>>& values, bool expired) {
|
||||
if (!expired)
|
||||
for (const auto& value : values)
|
||||
received.emplace_back(value->id);
|
||||
return true;
|
||||
},
|
||||
{},
|
||||
{},
|
||||
[this](bool ok) {
|
||||
results.push_back(ok);
|
||||
snapshots.emplace_back(received.size());
|
||||
});
|
||||
CPPUNIT_ASSERT(token != 0);
|
||||
}
|
||||
|
||||
void start(bool confirm = true)
|
||||
{
|
||||
listen();
|
||||
auto search = client->searches(AF_INET).at(key);
|
||||
auto node = client->network_engine.insertNode(server->getNodeId(), serverAddr);
|
||||
auto* searchNode = search->getNode(node);
|
||||
CPPUNIT_ASSERT(searchNode);
|
||||
searchNode->token = server->makeToken(clientAddr, false);
|
||||
searchNode->last_get_reply = client->scheduler.time();
|
||||
client->searchStep(search);
|
||||
if (confirm)
|
||||
acknowledge();
|
||||
}
|
||||
|
||||
void acknowledge(bool legacy = false, bool updatesFirst = false, bool includeToken = true)
|
||||
{
|
||||
auto requests = std::move(clientPackets->sends);
|
||||
clientPackets->sends.clear();
|
||||
unsigned registrations = 0;
|
||||
Blob legacyToken;
|
||||
for (const auto& packet : requests) {
|
||||
if (parse(packet).type == MessageType::Listen) {
|
||||
++registrations;
|
||||
if (legacy) {
|
||||
auto message = parse(packet);
|
||||
auto node = server->network_engine.insertNode(client->getNodeId(), clientAddr);
|
||||
auto answer = server->onListen(node, key, message.token, message.socket_id, message.query, 1, {});
|
||||
legacyToken = std::move(answer.ntoken);
|
||||
} else
|
||||
server->network_engine.processMessage(packet.data.data(), packet.data.size(), clientAddr);
|
||||
}
|
||||
}
|
||||
CPPUNIT_ASSERT_EQUAL(1u, registrations);
|
||||
auto replies = std::move(serverPackets->sends);
|
||||
serverPackets->sends.clear();
|
||||
std::vector<TestDatagramSocket::SentPacket> confirmations;
|
||||
for (const auto& packet : replies) {
|
||||
if (parse(packet).type == MessageType::Reply)
|
||||
confirmations.emplace_back(packet);
|
||||
else
|
||||
delayedUpdates.emplace_back(packet);
|
||||
}
|
||||
if (updatesFirst)
|
||||
deliverUpdates();
|
||||
if (legacy) {
|
||||
ParsedMessage acknowledgment;
|
||||
acknowledgment.type = MessageType::Reply;
|
||||
if (includeToken)
|
||||
acknowledgment.token = std::move(legacyToken);
|
||||
initialRequest()->setDone(std::move(acknowledgment));
|
||||
}
|
||||
for (const auto& packet : confirmations)
|
||||
client->network_engine.processMessage(packet.data.data(), packet.data.size(), serverAddr);
|
||||
}
|
||||
|
||||
void getRound(bool reversed = false)
|
||||
{
|
||||
for (const auto& packet : clientPackets->sends)
|
||||
CPPUNIT_ASSERT(parse(packet).type != MessageType::GetValues);
|
||||
if (reversed)
|
||||
std::reverse(delayedUpdates.begin(), delayedUpdates.end());
|
||||
deliverUpdates();
|
||||
}
|
||||
|
||||
void deliverUpdates()
|
||||
{
|
||||
for (const auto& packet : delayedUpdates)
|
||||
client->network_engine.processMessage(packet.data.data(), packet.data.size(), serverAddr);
|
||||
delayedUpdates.clear();
|
||||
}
|
||||
|
||||
Sp<net::Request> initialRequest() const
|
||||
{
|
||||
auto search = client->searches(AF_INET).at(key);
|
||||
auto& status = search->nodes.front()->listenStatus.begin()->second;
|
||||
CPPUNIT_ASSERT(status.req);
|
||||
return status.req;
|
||||
}
|
||||
};
|
||||
|
||||
void
|
||||
NetworkEngineTester::setUp()
|
||||
{}
|
||||
@@ -641,8 +479,7 @@ NetworkEngineTester::testListenConfirmationCarriesToken()
|
||||
[](Sp<Node>) { return net::RequestAnswer {}; },
|
||||
[](Sp<Node>, const InfoHash&, want_t) { return net::RequestAnswer {}; },
|
||||
[](Sp<Node>, const InfoHash&, want_t, const Query&) { return net::RequestAnswer {}; },
|
||||
[responseToken](
|
||||
Sp<Node>, const InfoHash&, const Blob&, Tid, const Query&, int, const std::optional<net::ListenSync>&) {
|
||||
[responseToken](Sp<Node>, const InfoHash&, const Blob&, Tid, const Query&, int) {
|
||||
net::RequestAnswer answer {};
|
||||
answer.ntoken = responseToken;
|
||||
return answer;
|
||||
@@ -718,682 +555,6 @@ NetworkEngineTester::testListenConfirmationUpdatesSearchNodeToken()
|
||||
CPPUNIT_ASSERT(!searchNodePtr->isSynced(localDht.scheduler.time()));
|
||||
}
|
||||
|
||||
void
|
||||
NetworkEngineTester::testListenReadinessWaitsForInitialUpdate()
|
||||
{
|
||||
Config config {};
|
||||
config.max_req_per_sec = -1;
|
||||
config.max_peer_req_per_sec = -1;
|
||||
auto clientSocket = std::make_unique<TestDatagramSocket>();
|
||||
auto* clientPackets = clientSocket.get();
|
||||
auto clientAddr = clientPackets->bound_;
|
||||
Dht client(std::move(clientSocket), config, {}, std::make_unique<std::mt19937_64>(20));
|
||||
auto serverSocket = std::make_unique<TestDatagramSocket>();
|
||||
auto* serverPackets = serverSocket.get();
|
||||
serverPackets->bound_.setPort(5020);
|
||||
auto serverAddr = serverPackets->bound_;
|
||||
Dht server(std::move(serverSocket), config, {}, std::make_unique<std::mt19937_64>(21));
|
||||
auto key = InfoHash::get("readiness-reordered-update");
|
||||
auto value = std::make_shared<Value>("initial-value");
|
||||
value->id = 42;
|
||||
CPPUNIT_ASSERT(server.storageStore(key, value, server.scheduler.time()));
|
||||
|
||||
std::vector<Value::Id> received;
|
||||
std::vector<size_t> snapshots;
|
||||
auto token = client.listen(
|
||||
key,
|
||||
[&](const std::vector<Sp<Value>>& values, bool expired) {
|
||||
if (!expired)
|
||||
for (const auto& current : values)
|
||||
received.emplace_back(current->id);
|
||||
return true;
|
||||
},
|
||||
{},
|
||||
{},
|
||||
[&](bool ok) {
|
||||
CPPUNIT_ASSERT(ok);
|
||||
snapshots.emplace_back(received.size());
|
||||
});
|
||||
CPPUNIT_ASSERT(token != 0);
|
||||
auto search = client.searches(AF_INET).at(key);
|
||||
auto node = client.network_engine.insertNode(server.getNodeId(), serverAddr);
|
||||
auto* searchNode = search->getNode(node);
|
||||
CPPUNIT_ASSERT(searchNode);
|
||||
searchNode->token = server.makeToken(clientAddr, false);
|
||||
searchNode->last_get_reply = client.scheduler.time();
|
||||
CPPUNIT_ASSERT(search->isSynced(client.scheduler.time()));
|
||||
client.searchStep(search);
|
||||
CPPUNIT_ASSERT(snapshots.empty());
|
||||
|
||||
auto listen = std::find_if(clientPackets->sends.begin(), clientPackets->sends.end(), [](const auto& packet) {
|
||||
ParsedMessage message;
|
||||
message.msgpack_unpack(msgpack::unpack((const char*) packet.data.data(), packet.data.size()).get());
|
||||
return message.type == MessageType::Listen;
|
||||
});
|
||||
CPPUNIT_ASSERT(listen != clientPackets->sends.end());
|
||||
server.network_engine.processMessage(listen->data.data(), listen->data.size(), clientAddr);
|
||||
CPPUNIT_ASSERT(serverPackets->sends.size() >= 2);
|
||||
|
||||
std::vector<TestDatagramSocket::SentPacket> updates;
|
||||
std::vector<TestDatagramSocket::SentPacket> confirmations;
|
||||
for (const auto& packet : serverPackets->sends) {
|
||||
ParsedMessage message;
|
||||
message.msgpack_unpack(msgpack::unpack((const char*) packet.data.data(), packet.data.size()).get());
|
||||
if (message.type == MessageType::Reply)
|
||||
confirmations.emplace_back(packet);
|
||||
else if (message.type == MessageType::UpdateValue)
|
||||
updates.emplace_back(packet);
|
||||
}
|
||||
CPPUNIT_ASSERT_EQUAL(size_t(1), confirmations.size());
|
||||
CPPUNIT_ASSERT(!updates.empty());
|
||||
for (const auto& packet : confirmations)
|
||||
client.network_engine.processMessage(packet.data.data(), packet.data.size(), serverAddr);
|
||||
CPPUNIT_ASSERT(snapshots.empty());
|
||||
serverPackets->sends.clear();
|
||||
for (const auto& packet : clientPackets->sends)
|
||||
CPPUNIT_ASSERT(ListenSnapshotTransport::parse(packet).type != MessageType::GetValues);
|
||||
for (const auto& packet : updates)
|
||||
client.network_engine.processMessage(packet.data.data(), packet.data.size(), serverAddr);
|
||||
CPPUNIT_ASSERT(client.cancelListen(key, token));
|
||||
CPPUNIT_ASSERT(received == std::vector<Value::Id>({42}));
|
||||
CPPUNIT_ASSERT_EQUAL(size_t(1), snapshots.size());
|
||||
CPPUNIT_ASSERT_EQUAL(size_t(1), snapshots.front());
|
||||
}
|
||||
|
||||
void
|
||||
NetworkEngineTester::testListenReadinessSnapshotVariants()
|
||||
{
|
||||
for (size_t count : {size_t(0), size_t(3)}) {
|
||||
ListenSnapshotTransport transport;
|
||||
transport.store(count, 6000);
|
||||
transport.start();
|
||||
if (count != 0)
|
||||
CPPUNIT_ASSERT(transport.results.empty());
|
||||
transport.getRound(true);
|
||||
CPPUNIT_ASSERT(transport.results == std::vector<bool>({true}));
|
||||
CPPUNIT_ASSERT(transport.snapshots == std::vector<size_t>({count}));
|
||||
transport.deliverUpdates();
|
||||
CPPUNIT_ASSERT_EQUAL(count, transport.received.size());
|
||||
CPPUNIT_ASSERT_EQUAL(size_t(1), transport.results.size());
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
NetworkEngineTester::testListenReadinessSnapshotLegacy()
|
||||
{
|
||||
for (size_t count : {size_t(0), size_t(3)})
|
||||
for (bool updatesFirst : {false, true})
|
||||
for (bool includeToken : {false, true}) {
|
||||
ListenSnapshotTransport transport;
|
||||
transport.store(count, 6000);
|
||||
transport.start(false);
|
||||
auto request = transport.initialRequest();
|
||||
auto search = transport.client->searches(AF_INET).at(transport.key);
|
||||
auto query = search->nodes.front()->listenStatus.begin()->first;
|
||||
auto socketId = search->nodes.front()->listenStatus.at(query).socketId;
|
||||
transport.acknowledge(true, updatesFirst, includeToken);
|
||||
CPPUNIT_ASSERT(transport.results == std::vector<bool>({true}));
|
||||
CPPUNIT_ASSERT(transport.snapshots == std::vector<size_t>({updatesFirst ? count : 0}));
|
||||
CPPUNIT_ASSERT(request->node->getSocket(socketId));
|
||||
transport.getRound(true);
|
||||
CPPUNIT_ASSERT_EQUAL(count, transport.received.size());
|
||||
CPPUNIT_ASSERT_EQUAL(size_t(1), transport.results.size());
|
||||
|
||||
std::vector<bool> reusedResults;
|
||||
size_t reusedValues = 0;
|
||||
auto reused = transport.client->listen(
|
||||
transport.key,
|
||||
[&](const std::vector<Sp<Value>>& values, bool expired) {
|
||||
if (!expired)
|
||||
reusedValues += values.size();
|
||||
return true;
|
||||
},
|
||||
{},
|
||||
{},
|
||||
[&](bool ok) { reusedResults.emplace_back(ok); });
|
||||
CPPUNIT_ASSERT(reused != 0);
|
||||
CPPUNIT_ASSERT_EQUAL(count, reusedValues);
|
||||
CPPUNIT_ASSERT(reusedResults == std::vector<bool>({true}));
|
||||
CPPUNIT_ASSERT(transport.client->cancelListen(transport.key, reused));
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
NetworkEngineTester::testListenLegacyLateValuesAndRenewal()
|
||||
{
|
||||
ListenSnapshotTransport transport;
|
||||
transport.start(false);
|
||||
transport.acknowledge(true, false, false);
|
||||
CPPUNIT_ASSERT(transport.results == std::vector<bool>({true}));
|
||||
auto search = transport.client->searches(AF_INET).at(transport.key);
|
||||
auto node = transport.initialRequest()->node;
|
||||
auto* source = search->getNode(node);
|
||||
auto query = source->listenStatus.begin()->first;
|
||||
auto socketId = source->listenStatus.at(query).socketId;
|
||||
auto token = source->token;
|
||||
CPPUNIT_ASSERT(!token.empty());
|
||||
|
||||
transport.client->scheduler.syncTime(transport.client->scheduler.time() + std::chrono::seconds(35));
|
||||
transport.client->scheduler.run();
|
||||
CPPUNIT_ASSERT(node->getSocket(socketId));
|
||||
source = search->getNode(node);
|
||||
CPPUNIT_ASSERT(source);
|
||||
CPPUNIT_ASSERT(source->token == token);
|
||||
auto first = std::make_shared<Value>("late-legacy-value");
|
||||
first->id = 42;
|
||||
CPPUNIT_ASSERT(transport.server->storageStore(transport.key, first, transport.server->scheduler.time()));
|
||||
transport.delayedUpdates = std::move(transport.serverPackets->sends);
|
||||
transport.serverPackets->sends.clear();
|
||||
transport.deliverUpdates();
|
||||
CPPUNIT_ASSERT(transport.received == std::vector<Value::Id>({42}));
|
||||
|
||||
if (!source->listenStatus.at(query).req->pending()) {
|
||||
transport.clientPackets->sends.clear();
|
||||
source->listenStatus.at(query).req->reply_time = transport.client->scheduler.time()
|
||||
- transport.client->getListenExpiration();
|
||||
transport.client->searchSynchedNodeListen(search, *source);
|
||||
}
|
||||
transport.acknowledge(true, false, false);
|
||||
CPPUNIT_ASSERT(transport.delayedUpdates.empty());
|
||||
CPPUNIT_ASSERT(node->getSocket(socketId));
|
||||
CPPUNIT_ASSERT_EQUAL(socketId, source->listenStatus.at(query).socketId);
|
||||
CPPUNIT_ASSERT(source->token == token);
|
||||
|
||||
auto second = std::make_shared<Value>("after-legacy-renewal");
|
||||
second->id = 43;
|
||||
CPPUNIT_ASSERT(transport.server->storageStore(transport.key, second, transport.server->scheduler.time()));
|
||||
transport.delayedUpdates = std::move(transport.serverPackets->sends);
|
||||
transport.serverPackets->sends.clear();
|
||||
transport.getRound();
|
||||
CPPUNIT_ASSERT(transport.received == std::vector<Value::Id>({42, 43}));
|
||||
CPPUNIT_ASSERT(transport.results == std::vector<bool>({true}));
|
||||
}
|
||||
|
||||
void
|
||||
NetworkEngineTester::testListenReadinessMixedPeers()
|
||||
{
|
||||
ListenSnapshotTransport transport;
|
||||
transport.store(1);
|
||||
transport.start(false);
|
||||
auto nativeRequests = std::move(transport.clientPackets->sends);
|
||||
transport.clientPackets->sends.clear();
|
||||
|
||||
ListenSnapshotTransport legacy(32);
|
||||
legacy.serverAddr.setPort(5032);
|
||||
legacy.serverPackets->bound_ = legacy.serverAddr;
|
||||
legacy.store(2);
|
||||
auto search = transport.client->searches(AF_INET).at(transport.key);
|
||||
auto node = transport.client->network_engine.insertNode(legacy.server->getNodeId(), legacy.serverAddr);
|
||||
auto* source = search->getNode(node);
|
||||
CPPUNIT_ASSERT(source);
|
||||
source->token = legacy.server->makeToken(transport.clientAddr, false);
|
||||
source->last_get_reply = transport.client->scheduler.time();
|
||||
transport.client->searchSynchedNodeListen(search, *source);
|
||||
CPPUNIT_ASSERT_EQUAL(size_t(1), source->listenStatus.size());
|
||||
auto& status = source->listenStatus.begin()->second;
|
||||
auto peer = legacy.server->network_engine.insertNode(transport.client->getNodeId(), transport.clientAddr);
|
||||
auto query = *source->listenStatus.begin()->first;
|
||||
legacy.server->storageAddListener(transport.key, peer, status.socketId, std::move(query), 1, {});
|
||||
ParsedMessage acknowledgment;
|
||||
acknowledgment.type = MessageType::Reply;
|
||||
status.req->setDone(std::move(acknowledgment));
|
||||
CPPUNIT_ASSERT(transport.results.empty());
|
||||
for (const auto& packet : legacy.serverPackets->sends)
|
||||
transport.client->network_engine.processMessage(packet.data.data(), packet.data.size(), legacy.serverAddr);
|
||||
CPPUNIT_ASSERT_EQUAL(size_t(2), transport.received.size());
|
||||
CPPUNIT_ASSERT(transport.results.empty());
|
||||
|
||||
transport.clientPackets->sends = std::move(nativeRequests);
|
||||
transport.acknowledge();
|
||||
CPPUNIT_ASSERT(transport.results.empty());
|
||||
transport.getRound();
|
||||
CPPUNIT_ASSERT(transport.results == std::vector<bool>({true}));
|
||||
CPPUNIT_ASSERT_EQUAL(size_t(2), transport.received.size());
|
||||
}
|
||||
|
||||
void
|
||||
NetworkEngineTester::testListenReadinessSnapshotFailure()
|
||||
{
|
||||
for (unsigned failure = 0; failure < 3; ++failure) {
|
||||
ListenSnapshotTransport transport;
|
||||
transport.store(1);
|
||||
transport.start(false);
|
||||
auto request = transport.initialRequest();
|
||||
if (failure == 0) {
|
||||
request->setExpired();
|
||||
} else {
|
||||
ParsedMessage answer;
|
||||
answer.type = MessageType::Reply;
|
||||
if (failure == 1) {
|
||||
auto search = transport.client->searches(AF_INET).at(transport.key);
|
||||
auto registration = search->nodes.front()->listenStatus.begin()->second.registration;
|
||||
answer.listenSync = net::ListenSync {registration, 1, 0, 0};
|
||||
}
|
||||
if (failure == 2) {
|
||||
answer.token = transport.server->makeToken(transport.clientAddr, false);
|
||||
answer.fields.emplace_back(std::make_shared<FieldValueIndex>());
|
||||
}
|
||||
request->setDone(std::move(answer));
|
||||
}
|
||||
CPPUNIT_ASSERT(transport.results == std::vector<bool>({false}));
|
||||
transport.deliverUpdates();
|
||||
CPPUNIT_ASSERT_EQUAL(size_t(1), transport.results.size());
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
NetworkEngineTester::testListenReadinessSnapshotCancellation()
|
||||
{
|
||||
ListenSnapshotTransport transport;
|
||||
transport.store(1);
|
||||
transport.start();
|
||||
CPPUNIT_ASSERT(transport.client->cancelListen(transport.key, transport.token));
|
||||
transport.token = 0;
|
||||
transport.getRound();
|
||||
transport.getRound();
|
||||
CPPUNIT_ASSERT(transport.results.empty());
|
||||
transport.listen();
|
||||
CPPUNIT_ASSERT(transport.results == std::vector<bool>({true}));
|
||||
CPPUNIT_ASSERT(transport.snapshots == std::vector<size_t>({1}));
|
||||
}
|
||||
|
||||
void
|
||||
NetworkEngineTester::testListenReadinessSnapshotReplacement()
|
||||
{
|
||||
ListenSnapshotTransport transport;
|
||||
transport.store(1);
|
||||
transport.start();
|
||||
auto previous = transport.initialRequest();
|
||||
auto search = transport.client->searches(AF_INET).at(transport.key);
|
||||
auto* node = search->nodes.front().get();
|
||||
auto oldUpdates = std::move(transport.delayedUpdates);
|
||||
node->listenStatus.clear();
|
||||
transport.clientPackets->sends.clear();
|
||||
transport.client->searchSynchedNodeListen(search, *node);
|
||||
transport.acknowledge();
|
||||
CPPUNIT_ASSERT(transport.results.empty());
|
||||
ParsedMessage answer;
|
||||
answer.token = transport.server->makeToken(transport.clientAddr, false);
|
||||
answer.values = transport.server->getLocal(transport.key);
|
||||
previous->setDone(std::move(answer));
|
||||
for (const auto& packet : oldUpdates)
|
||||
transport.client->network_engine.processMessage(packet.data.data(), packet.data.size(), transport.serverAddr);
|
||||
CPPUNIT_ASSERT(transport.results.empty());
|
||||
transport.getRound();
|
||||
transport.getRound();
|
||||
CPPUNIT_ASSERT(transport.results == std::vector<bool>({true}));
|
||||
CPPUNIT_ASSERT(transport.snapshots == std::vector<size_t>({1}));
|
||||
}
|
||||
|
||||
void
|
||||
NetworkEngineTester::testListenReadinessSnapshotRenewal()
|
||||
{
|
||||
ListenSnapshotTransport transport;
|
||||
transport.store(1);
|
||||
transport.start();
|
||||
transport.getRound();
|
||||
transport.getRound();
|
||||
CPPUNIT_ASSERT(transport.results == std::vector<bool>({true}));
|
||||
transport.deliverUpdates();
|
||||
transport.clientPackets->sends.clear();
|
||||
auto search = transport.client->searches(AF_INET).at(transport.key);
|
||||
auto* node = search->nodes.front().get();
|
||||
node->listenStatus.begin()->second.req->reply_time = transport.client->scheduler.time()
|
||||
- transport.client->getListenExpiration();
|
||||
transport.client->searchSynchedNodeListen(search, *node);
|
||||
transport.acknowledge();
|
||||
CPPUNIT_ASSERT(transport.delayedUpdates.empty());
|
||||
for (const auto& packet : transport.clientPackets->sends)
|
||||
CPPUNIT_ASSERT(ListenSnapshotTransport::parse(packet).type != MessageType::GetValues);
|
||||
CPPUNIT_ASSERT_EQUAL(size_t(1), transport.results.size());
|
||||
}
|
||||
|
||||
void
|
||||
NetworkEngineTester::testListenUpdatesBeforeConfirmation()
|
||||
{
|
||||
ListenSnapshotTransport transport;
|
||||
transport.store(40, 6000);
|
||||
transport.start(false);
|
||||
auto request = transport.initialRequest();
|
||||
auto listen = std::find_if(transport.clientPackets->sends.begin(),
|
||||
transport.clientPackets->sends.end(),
|
||||
[](const auto& packet) {
|
||||
return ListenSnapshotTransport::parse(packet).type == MessageType::Listen;
|
||||
});
|
||||
CPPUNIT_ASSERT(listen != transport.clientPackets->sends.end());
|
||||
transport.server->network_engine.processMessage(listen->data.data(), listen->data.size(), transport.clientAddr);
|
||||
auto replies = std::move(transport.serverPackets->sends);
|
||||
std::vector<TestDatagramSocket::SentPacket> confirmations;
|
||||
std::reverse(replies.begin(), replies.end());
|
||||
for (const auto& packet : replies) {
|
||||
if (ListenSnapshotTransport::parse(packet).type == MessageType::Reply)
|
||||
confirmations.emplace_back(packet);
|
||||
else
|
||||
transport.client->network_engine.processMessage(packet.data.data(),
|
||||
packet.data.size(),
|
||||
transport.serverAddr);
|
||||
}
|
||||
CPPUNIT_ASSERT(transport.results.empty());
|
||||
CPPUNIT_ASSERT(transport.received.empty());
|
||||
CPPUNIT_ASSERT_EQUAL(size_t(1), confirmations.size());
|
||||
auto& confirmation = confirmations.front();
|
||||
transport.client->network_engine.processMessage(confirmation.data.data(),
|
||||
confirmation.data.size(),
|
||||
transport.serverAddr);
|
||||
CPPUNIT_ASSERT(transport.results == std::vector<bool>({true}));
|
||||
CPPUNIT_ASSERT_EQUAL(size_t(40), transport.received.size());
|
||||
for (const auto& packet : replies)
|
||||
transport.client->network_engine.processMessage(packet.data.data(), packet.data.size(), transport.serverAddr);
|
||||
CPPUNIT_ASSERT_EQUAL(size_t(40), transport.received.size());
|
||||
}
|
||||
|
||||
void
|
||||
NetworkEngineTester::testListenLiveChangesBeforeSnapshot()
|
||||
{
|
||||
ListenSnapshotTransport transport;
|
||||
transport.store(1);
|
||||
transport.start();
|
||||
auto& storage = transport.server->store.at(transport.key);
|
||||
auto removed = storage.remove(transport.key, 1);
|
||||
CPPUNIT_ASSERT(removed);
|
||||
transport.server->storageRemoved(transport.key, storage, {removed}, removed->size());
|
||||
auto value = std::make_shared<Value>("new-live-value");
|
||||
value->id = 99;
|
||||
CPPUNIT_ASSERT(transport.server->storageStore(transport.key, value, transport.server->scheduler.time()));
|
||||
CPPUNIT_ASSERT_EQUAL(size_t(2), transport.serverPackets->sends.size());
|
||||
for (const auto& packet : transport.serverPackets->sends)
|
||||
transport.client->network_engine.processMessage(packet.data.data(), packet.data.size(), transport.serverAddr);
|
||||
CPPUNIT_ASSERT(transport.received.empty());
|
||||
CPPUNIT_ASSERT(transport.results.empty());
|
||||
transport.deliverUpdates();
|
||||
auto search = transport.client->searches(AF_INET).at(transport.key);
|
||||
CPPUNIT_ASSERT(!search->cache.get(1));
|
||||
CPPUNIT_ASSERT(search->cache.get(99));
|
||||
CPPUNIT_ASSERT(transport.results == std::vector<bool>({true}));
|
||||
for (const auto& packet : transport.serverPackets->sends)
|
||||
transport.client->network_engine.processMessage(packet.data.data(), packet.data.size(), transport.serverAddr);
|
||||
CPPUNIT_ASSERT(!search->cache.get(1));
|
||||
CPPUNIT_ASSERT_EQUAL(size_t(2), transport.received.size());
|
||||
}
|
||||
|
||||
void
|
||||
NetworkEngineTester::testListenDuplicateRegistration()
|
||||
{
|
||||
ListenSnapshotTransport transport;
|
||||
transport.store(40);
|
||||
transport.start(false);
|
||||
auto listen = std::find_if(transport.clientPackets->sends.begin(),
|
||||
transport.clientPackets->sends.end(),
|
||||
[](const auto& packet) {
|
||||
return ListenSnapshotTransport::parse(packet).type == MessageType::Listen;
|
||||
});
|
||||
CPPUNIT_ASSERT(listen != transport.clientPackets->sends.end());
|
||||
auto packet = *listen;
|
||||
transport.acknowledge();
|
||||
CPPUNIT_ASSERT_EQUAL(size_t(2),
|
||||
size_t(std::count_if(transport.delayedUpdates.begin(),
|
||||
transport.delayedUpdates.end(),
|
||||
[](const auto& update) {
|
||||
return ListenSnapshotTransport::parse(update).type
|
||||
== MessageType::UpdateValue;
|
||||
})));
|
||||
transport.server->network_engine.processMessage(packet.data.data(), packet.data.size(), transport.clientAddr);
|
||||
CPPUNIT_ASSERT_EQUAL(size_t(1), transport.serverPackets->sends.size());
|
||||
auto confirmation = ListenSnapshotTransport::parse(transport.serverPackets->sends.front());
|
||||
CPPUNIT_ASSERT(confirmation.type == MessageType::Reply);
|
||||
CPPUNIT_ASSERT(confirmation.listenSync);
|
||||
CPPUNIT_ASSERT_EQUAL(uint64_t(2), confirmation.listenSync->snapshotEnd);
|
||||
transport.deliverUpdates();
|
||||
CPPUNIT_ASSERT(transport.results == std::vector<bool>({true}));
|
||||
CPPUNIT_ASSERT_EQUAL(size_t(40), transport.received.size());
|
||||
}
|
||||
|
||||
void
|
||||
NetworkEngineTester::testListenLargeSnapshotDefaultLimits()
|
||||
{
|
||||
ListenSnapshotTransport transport;
|
||||
auto socket = std::make_unique<TestDatagramSocket>();
|
||||
socket->bound_.setPort(5031);
|
||||
transport.serverPackets = socket.get();
|
||||
transport.serverAddr = socket->bound_;
|
||||
transport.server = std::make_unique<Dht>(std::move(socket),
|
||||
Config {},
|
||||
Sp<Logger> {},
|
||||
std::make_unique<std::mt19937_64>(31));
|
||||
transport.store(4096);
|
||||
transport.start();
|
||||
CPPUNIT_ASSERT(transport.results.empty());
|
||||
size_t batches = 0;
|
||||
size_t payloads = 0;
|
||||
for (const auto& packet : transport.delayedUpdates) {
|
||||
auto message = ListenSnapshotTransport::parse(packet);
|
||||
if (message.type == MessageType::UpdateValue) {
|
||||
++batches;
|
||||
payloads += message.values.size() + message.value_parts.size();
|
||||
}
|
||||
}
|
||||
CPPUNIT_ASSERT_EQUAL(size_t(128), batches);
|
||||
CPPUNIT_ASSERT_EQUAL(size_t(4096), payloads);
|
||||
transport.getRound(true);
|
||||
CPPUNIT_ASSERT(transport.results == std::vector<bool>({true}));
|
||||
CPPUNIT_ASSERT_EQUAL(size_t(4096), transport.received.size());
|
||||
auto search = transport.client->searches(AF_INET).at(transport.key);
|
||||
CPPUNIT_ASSERT_EQUAL(size_t(4096), search->cache.get(Value::Filter {}).size());
|
||||
for (const auto& packet : transport.clientPackets->sends)
|
||||
CPPUNIT_ASSERT(ListenSnapshotTransport::parse(packet).type != MessageType::GetValues);
|
||||
}
|
||||
|
||||
void
|
||||
NetworkEngineTester::testListenLiveUpdateOrdering()
|
||||
{
|
||||
ListenSnapshotTransport transport;
|
||||
transport.store(1);
|
||||
transport.start();
|
||||
transport.deliverUpdates();
|
||||
for (Value::Id id : {2, 3}) {
|
||||
auto value = std::make_shared<Value>("live");
|
||||
value->id = id;
|
||||
CPPUNIT_ASSERT(transport.server->storageStore(transport.key, value, transport.server->scheduler.time()));
|
||||
}
|
||||
CPPUNIT_ASSERT_EQUAL(size_t(2), transport.serverPackets->sends.size());
|
||||
auto search = transport.client->searches(AF_INET).at(transport.key);
|
||||
auto& later = transport.serverPackets->sends.back();
|
||||
transport.client->network_engine.processMessage(later.data.data(), later.data.size(), transport.serverAddr);
|
||||
CPPUNIT_ASSERT(!search->cache.get(3));
|
||||
auto& earlier = transport.serverPackets->sends.front();
|
||||
transport.client->network_engine.processMessage(earlier.data.data(), earlier.data.size(), transport.serverAddr);
|
||||
CPPUNIT_ASSERT(search->cache.get(2));
|
||||
CPPUNIT_ASSERT(search->cache.get(3));
|
||||
CPPUNIT_ASSERT(transport.results == std::vector<bool>({true}));
|
||||
}
|
||||
|
||||
void
|
||||
NetworkEngineTester::testListenInitializationTimeout()
|
||||
{
|
||||
ListenSnapshotTransport transport;
|
||||
transport.store(1);
|
||||
transport.start();
|
||||
CPPUNIT_ASSERT(transport.results.empty());
|
||||
transport.client->scheduler.syncTime(transport.client->scheduler.time() + std::chrono::seconds(31));
|
||||
transport.client->scheduler.run();
|
||||
CPPUNIT_ASSERT(transport.results == std::vector<bool>({false}));
|
||||
transport.deliverUpdates();
|
||||
CPPUNIT_ASSERT_EQUAL(size_t(1), transport.results.size());
|
||||
}
|
||||
|
||||
void
|
||||
NetworkEngineTester::testListenRegistrationFailure()
|
||||
{
|
||||
ListenSnapshotTransport transport;
|
||||
transport.store(1);
|
||||
transport.start(false);
|
||||
auto search = transport.client->searches(AF_INET).at(transport.key);
|
||||
auto* node = search->nodes.front().get();
|
||||
auto request = node->listenStatus.begin()->second.req;
|
||||
CPPUNIT_ASSERT(request->pending());
|
||||
CPPUNIT_ASSERT(!node->node->isExpired());
|
||||
request->setExpired();
|
||||
CPPUNIT_ASSERT(transport.results == std::vector<bool>({false}));
|
||||
CPPUNIT_ASSERT(!node->node->isExpired());
|
||||
transport.clientPackets->sends.clear();
|
||||
transport.client->searchStep(search);
|
||||
transport.acknowledge();
|
||||
transport.getRound();
|
||||
transport.getRound();
|
||||
CPPUNIT_ASSERT(transport.results == std::vector<bool>({false}));
|
||||
}
|
||||
|
||||
void
|
||||
NetworkEngineTester::testListenRegistrationCapacity()
|
||||
{
|
||||
for (bool dualStack : {false, true}) {
|
||||
auto socket = std::make_unique<TestDatagramSocket>();
|
||||
socket->ipv6 = dualStack;
|
||||
Config config;
|
||||
config.max_searches = 1;
|
||||
Dht localDht(std::move(socket), config, {}, std::make_unique<std::mt19937_64>(40));
|
||||
auto occupiedKey = InfoHash::get("occupied-search");
|
||||
auto rejectedKey = InfoHash::get("rejected-search");
|
||||
auto values = [](const std::vector<Sp<Value>>&, bool) {
|
||||
return true;
|
||||
};
|
||||
if (dualStack)
|
||||
CPPUNIT_ASSERT(localDht.search(occupiedKey, AF_INET6));
|
||||
else
|
||||
CPPUNIT_ASSERT(localDht.listen(occupiedKey, values) != 0);
|
||||
std::vector<bool> results;
|
||||
CPPUNIT_ASSERT_EQUAL(size_t(0),
|
||||
localDht.listen(rejectedKey, values, {}, {}, [&](bool ok) { results.push_back(ok); }));
|
||||
CPPUNIT_ASSERT(results == std::vector<bool>({false}));
|
||||
CPPUNIT_ASSERT(localDht.store.at(rejectedKey).local_listeners.empty());
|
||||
if (dualStack) {
|
||||
auto partial = localDht.searches(AF_INET).at(rejectedKey);
|
||||
CPPUNIT_ASSERT(!partial->cache.cancelListen(1, localDht.scheduler.time()));
|
||||
partial->cache.onReady();
|
||||
CPPUNIT_ASSERT_EQUAL(size_t(1), results.size());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
NetworkEngineTester::testListenReadinessAddressFamilies()
|
||||
{
|
||||
auto key = InfoHash::get("readiness-address-families");
|
||||
for (unsigned families = 1; families <= 3; ++families) {
|
||||
auto socket = std::make_unique<TestDatagramSocket>();
|
||||
socket->ipv4 = families & 1;
|
||||
socket->ipv6 = families & 2;
|
||||
Dht localDht(std::move(socket), Config {}, {}, std::make_unique<std::mt19937_64>(families));
|
||||
std::vector<bool> results;
|
||||
auto token = localDht.listen(
|
||||
key,
|
||||
[](const std::vector<Sp<Value>>&, bool) { return true; },
|
||||
{},
|
||||
{},
|
||||
[&](bool ok) { results.push_back(ok); });
|
||||
CPPUNIT_ASSERT(token != 0);
|
||||
CPPUNIT_ASSERT(results.empty());
|
||||
if (families & 1) {
|
||||
localDht.searchStep(localDht.searches(AF_INET).at(key));
|
||||
if (families & 2)
|
||||
CPPUNIT_ASSERT(results.empty());
|
||||
}
|
||||
if (families & 2)
|
||||
localDht.searchStep(localDht.searches(AF_INET6).at(key));
|
||||
CPPUNIT_ASSERT(results == std::vector<bool>({true}));
|
||||
CPPUNIT_ASSERT(localDht.cancelListen(key, token));
|
||||
|
||||
results.clear();
|
||||
CPPUNIT_ASSERT_EQUAL(size_t(0),
|
||||
localDht.listen(
|
||||
InfoHash {},
|
||||
[](const std::vector<Sp<Value>>&, bool) { return true; },
|
||||
{},
|
||||
{},
|
||||
[&](bool ok) { results.push_back(ok); }));
|
||||
CPPUNIT_ASSERT(results == std::vector<bool>({false}));
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
NetworkEngineTester::testListenReadinessFailedSearch()
|
||||
{
|
||||
Dht localDht(std::make_unique<TestDatagramSocket>(), Config {}, {}, std::make_unique<std::mt19937_64>(22));
|
||||
auto key = InfoHash::get("readiness-failed-search");
|
||||
std::vector<bool> results;
|
||||
auto token = localDht.listen(
|
||||
key, [](const std::vector<Sp<Value>>&, bool) { return true; }, {}, {}, [&](bool ok) { results.push_back(ok); });
|
||||
auto search = localDht.searches(AF_INET).at(key);
|
||||
auto node = std::make_shared<Node>(InfoHash::getRandom(localDht.rd),
|
||||
makeIPv4("127.0.0.2", 5022),
|
||||
localDht.rd,
|
||||
false);
|
||||
node->setExpired();
|
||||
search->nodes.emplace_back(std::make_unique<Dht::SearchNode>(node));
|
||||
localDht.searchStep(search);
|
||||
CPPUNIT_ASSERT(search->expired);
|
||||
CPPUNIT_ASSERT(results == std::vector<bool>({false}));
|
||||
search->cache.onReady();
|
||||
CPPUNIT_ASSERT_EQUAL(size_t(1), results.size());
|
||||
CPPUNIT_ASSERT(localDht.cancelListen(key, token));
|
||||
}
|
||||
|
||||
void
|
||||
NetworkEngineTester::testListenReadinessReusesExpiredSearch()
|
||||
{
|
||||
Dht localDht(std::make_unique<TestDatagramSocket>(), Config {}, {}, std::make_unique<std::mt19937_64>(23));
|
||||
auto key = InfoHash::get("readiness-reused-search");
|
||||
auto values = [](const std::vector<Sp<Value>>&, bool) {
|
||||
return true;
|
||||
};
|
||||
std::vector<bool> results;
|
||||
auto ready = [&](bool ok) {
|
||||
results.push_back(ok);
|
||||
};
|
||||
auto first = localDht.listen(key, values, {}, {}, ready);
|
||||
auto search = localDht.searches(AF_INET).at(key);
|
||||
search->expire();
|
||||
CPPUNIT_ASSERT(results == std::vector<bool>({false}));
|
||||
CPPUNIT_ASSERT(localDht.cancelListen(key, first));
|
||||
auto second = localDht.listen(key, values, {}, {}, ready);
|
||||
CPPUNIT_ASSERT(second != 0);
|
||||
CPPUNIT_ASSERT(search == localDht.searches(AF_INET).at(key));
|
||||
CPPUNIT_ASSERT(!search->expired);
|
||||
CPPUNIT_ASSERT(!search->done);
|
||||
CPPUNIT_ASSERT(localDht.scheduler.time() == search->nextSearchStep->t_);
|
||||
localDht.searchStep(search);
|
||||
CPPUNIT_ASSERT(results == std::vector<bool>({false, true}));
|
||||
CPPUNIT_ASSERT(localDht.cancelListen(key, second));
|
||||
}
|
||||
|
||||
void
|
||||
NetworkEngineTester::testListenReadinessShutdown()
|
||||
{
|
||||
for (bool stop : {false, true}) {
|
||||
std::vector<bool> results;
|
||||
auto localDht = std::make_unique<Dht>(std::make_unique<TestDatagramSocket>(),
|
||||
Config {},
|
||||
Sp<Logger> {},
|
||||
std::make_unique<std::mt19937_64>(24));
|
||||
CPPUNIT_ASSERT(localDht->listen(
|
||||
InfoHash::get("readiness-shutdown"),
|
||||
[](const std::vector<Sp<Value>>&, bool) { return true; },
|
||||
{},
|
||||
{},
|
||||
[&](bool ok) { results.push_back(ok); })
|
||||
!= 0);
|
||||
CPPUNIT_ASSERT(results.empty());
|
||||
if (stop) {
|
||||
localDht->shutdown({}, true);
|
||||
CPPUNIT_ASSERT_EQUAL(size_t(1), results.size());
|
||||
CPPUNIT_ASSERT(!results.front());
|
||||
}
|
||||
localDht.reset();
|
||||
CPPUNIT_ASSERT_EQUAL(size_t(1), results.size());
|
||||
CPPUNIT_ASSERT(!results.front());
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
NetworkEngineTester::testListenReopensSocketAfterNodeExpiration()
|
||||
{
|
||||
|
||||
@@ -22,27 +22,6 @@ class NetworkEngineTester : public CppUnit::TestFixture
|
||||
CPPUNIT_TEST(testSeparatesPartialSessionsBySource);
|
||||
CPPUNIT_TEST(testListenConfirmationCarriesToken);
|
||||
CPPUNIT_TEST(testListenConfirmationUpdatesSearchNodeToken);
|
||||
CPPUNIT_TEST(testListenReadinessWaitsForInitialUpdate);
|
||||
CPPUNIT_TEST(testListenReadinessSnapshotVariants);
|
||||
CPPUNIT_TEST(testListenReadinessSnapshotLegacy);
|
||||
CPPUNIT_TEST(testListenLegacyLateValuesAndRenewal);
|
||||
CPPUNIT_TEST(testListenReadinessMixedPeers);
|
||||
CPPUNIT_TEST(testListenReadinessSnapshotFailure);
|
||||
CPPUNIT_TEST(testListenReadinessSnapshotCancellation);
|
||||
CPPUNIT_TEST(testListenReadinessSnapshotReplacement);
|
||||
CPPUNIT_TEST(testListenReadinessSnapshotRenewal);
|
||||
CPPUNIT_TEST(testListenUpdatesBeforeConfirmation);
|
||||
CPPUNIT_TEST(testListenLiveChangesBeforeSnapshot);
|
||||
CPPUNIT_TEST(testListenDuplicateRegistration);
|
||||
CPPUNIT_TEST(testListenLargeSnapshotDefaultLimits);
|
||||
CPPUNIT_TEST(testListenLiveUpdateOrdering);
|
||||
CPPUNIT_TEST(testListenInitializationTimeout);
|
||||
CPPUNIT_TEST(testListenRegistrationFailure);
|
||||
CPPUNIT_TEST(testListenRegistrationCapacity);
|
||||
CPPUNIT_TEST(testListenReadinessAddressFamilies);
|
||||
CPPUNIT_TEST(testListenReadinessFailedSearch);
|
||||
CPPUNIT_TEST(testListenReadinessReusesExpiredSearch);
|
||||
CPPUNIT_TEST(testListenReadinessShutdown);
|
||||
CPPUNIT_TEST(testListenReopensSocketAfterNodeExpiration);
|
||||
CPPUNIT_TEST(testUnauthorizedListenFlushClearsListenState);
|
||||
#endif
|
||||
@@ -64,27 +43,6 @@ public:
|
||||
void testSeparatesPartialSessionsBySource();
|
||||
void testListenConfirmationCarriesToken();
|
||||
void testListenConfirmationUpdatesSearchNodeToken();
|
||||
void testListenReadinessWaitsForInitialUpdate();
|
||||
void testListenReadinessSnapshotVariants();
|
||||
void testListenReadinessSnapshotLegacy();
|
||||
void testListenLegacyLateValuesAndRenewal();
|
||||
void testListenReadinessMixedPeers();
|
||||
void testListenReadinessSnapshotFailure();
|
||||
void testListenReadinessSnapshotCancellation();
|
||||
void testListenReadinessSnapshotReplacement();
|
||||
void testListenReadinessSnapshotRenewal();
|
||||
void testListenUpdatesBeforeConfirmation();
|
||||
void testListenLiveChangesBeforeSnapshot();
|
||||
void testListenDuplicateRegistration();
|
||||
void testListenLargeSnapshotDefaultLimits();
|
||||
void testListenLiveUpdateOrdering();
|
||||
void testListenInitializationTimeout();
|
||||
void testListenRegistrationFailure();
|
||||
void testListenRegistrationCapacity();
|
||||
void testListenReadinessAddressFamilies();
|
||||
void testListenReadinessFailedSearch();
|
||||
void testListenReadinessReusesExpiredSearch();
|
||||
void testListenReadinessShutdown();
|
||||
void testListenReopensSocketAfterNodeExpiration();
|
||||
void testUnauthorizedListenFlushClearsListenState();
|
||||
#endif
|
||||
|
||||
@@ -1,951 +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::testInitialListenerReadiness()
|
||||
{
|
||||
dht::OpCache cache;
|
||||
auto query = std::make_shared<dht::Query>();
|
||||
unsigned calls = 0;
|
||||
auto callback = [&](bool ok) {
|
||||
CPPUNIT_ASSERT(ok);
|
||||
++calls;
|
||||
};
|
||||
CPPUNIT_ASSERT(cache.addListener(1, [](const Values&, bool) { return true; }, query, {}, callback));
|
||||
CPPUNIT_ASSERT_EQUAL(0u, calls);
|
||||
cache.onNodeChanged(dht::ListenSyncStatus::ADDED);
|
||||
cache.onNodeChanged(dht::ListenSyncStatus::SYNCED);
|
||||
CPPUNIT_ASSERT_EQUAL(1u, calls);
|
||||
cache.onNodeChanged(dht::ListenSyncStatus::UNSYNCED);
|
||||
CPPUNIT_ASSERT(cache.addListener(4, [](const Values&, bool) { return true; }, query, {}, callback));
|
||||
CPPUNIT_ASSERT_EQUAL(1u, calls);
|
||||
cache.onNodeChanged(dht::ListenSyncStatus::SYNCED);
|
||||
CPPUNIT_ASSERT_EQUAL(2u, calls);
|
||||
CPPUNIT_ASSERT(cache.addListener(2, [](const Values&, bool) { return true; }, query, {}, callback));
|
||||
CPPUNIT_ASSERT_EQUAL(3u, calls);
|
||||
dht::OpCache isolated;
|
||||
CPPUNIT_ASSERT(isolated.addListener(3, [](const Values&, bool) { return true; }, query, {}, callback));
|
||||
isolated.onReady();
|
||||
CPPUNIT_ASSERT_EQUAL(4u, calls);
|
||||
dht::OpCache searching(true, true);
|
||||
CPPUNIT_ASSERT(searching.addListener(5, [](const Values&, bool) { return true; }, query, {}, callback));
|
||||
searching.onNodeChanged(dht::ListenSyncStatus::ADDED);
|
||||
searching.onNodeChanged(dht::ListenSyncStatus::SYNCED);
|
||||
CPPUNIT_ASSERT_EQUAL(4u, calls);
|
||||
searching.onSearchReady();
|
||||
CPPUNIT_ASSERT_EQUAL(5u, calls);
|
||||
}
|
||||
|
||||
void
|
||||
OpCacheTester::testInitialReadinessCancellation()
|
||||
{
|
||||
dht::OpCache cache(true, true);
|
||||
auto query = std::make_shared<dht::Query>();
|
||||
auto values = [](const Values&, bool) {
|
||||
return true;
|
||||
};
|
||||
unsigned calls = 0;
|
||||
auto ready = [&](bool) {
|
||||
++calls;
|
||||
};
|
||||
CPPUNIT_ASSERT(cache.addListener(1, values, query, {}, ready));
|
||||
CPPUNIT_ASSERT(cache.removeListener(1, dht::clock::now()));
|
||||
cache.onReady();
|
||||
CPPUNIT_ASSERT_EQUAL(0u, calls);
|
||||
|
||||
dht::OpCache cleared(true, true);
|
||||
CPPUNIT_ASSERT(cleared.addListener(1, values, query, {}, ready));
|
||||
cleared.removeAll();
|
||||
cleared.onReady(false);
|
||||
CPPUNIT_ASSERT_EQUAL(0u, calls);
|
||||
|
||||
dht::OpCache reentrant(true, true);
|
||||
CPPUNIT_ASSERT(reentrant.addListener(1, values, query, {}, [&](bool ok) {
|
||||
CPPUNIT_ASSERT(ok);
|
||||
CPPUNIT_ASSERT(reentrant.removeListener(2, dht::clock::now()));
|
||||
}));
|
||||
CPPUNIT_ASSERT(reentrant.addListener(2, values, query, {}, ready));
|
||||
reentrant.onReady();
|
||||
CPPUNIT_ASSERT_EQUAL(0u, calls);
|
||||
|
||||
dht::OpCache clearing(true, true);
|
||||
CPPUNIT_ASSERT(clearing.addListener(1, values, query, {}, [&](bool) { clearing.removeAll(); }));
|
||||
CPPUNIT_ASSERT(clearing.addListener(2, values, query, {}, ready));
|
||||
clearing.onReady();
|
||||
CPPUNIT_ASSERT_EQUAL(0u, calls);
|
||||
}
|
||||
|
||||
void
|
||||
OpCacheTester::testInitialReadinessFailure()
|
||||
{
|
||||
dht::OpCache cache(true, true);
|
||||
auto query = std::make_shared<dht::Query>();
|
||||
auto values = [](const Values&, bool) {
|
||||
return true;
|
||||
};
|
||||
std::vector<bool> results;
|
||||
auto ready = [&](bool ok) {
|
||||
results.push_back(ok);
|
||||
};
|
||||
CPPUNIT_ASSERT(cache.addListener(1, values, query, {}, ready));
|
||||
CPPUNIT_ASSERT(cache.addListener(2, values, query, {}, ready));
|
||||
cache.onReady(false);
|
||||
CPPUNIT_ASSERT(results == std::vector<bool>({false, false}));
|
||||
cache.onReady();
|
||||
CPPUNIT_ASSERT_EQUAL(size_t(2), results.size());
|
||||
|
||||
cache.onReady(false);
|
||||
CPPUNIT_ASSERT(cache.addListener(3, values, query, {}, ready));
|
||||
CPPUNIT_ASSERT_EQUAL(size_t(2), results.size());
|
||||
cache.onNodeChanged(dht::ListenSyncStatus::ADDED);
|
||||
cache.onNodeChanged(dht::ListenSyncStatus::SYNCED);
|
||||
CPPUNIT_ASSERT_EQUAL(size_t(2), results.size());
|
||||
cache.onSearchReady();
|
||||
CPPUNIT_ASSERT(results == std::vector<bool>({false, false, true}));
|
||||
|
||||
dht::SearchCache searches(true, true);
|
||||
auto first = std::make_shared<dht::Query>(dht::Select {}, dht::Where {}.id(42));
|
||||
auto second = std::make_shared<dht::Query>(dht::Select {}, dht::Where {}.id(43));
|
||||
dht::SyncCallback secondSource;
|
||||
auto onListen = [&](dht::Sp<dht::Query> query, dht::ValueCallback, dht::SyncCallback source) {
|
||||
source(dht::ListenSyncStatus::ADDED);
|
||||
if (query == second)
|
||||
secondSource = std::move(source);
|
||||
return size_t(1);
|
||||
};
|
||||
std::vector<bool> firstResults;
|
||||
std::vector<bool> secondResults;
|
||||
CPPUNIT_ASSERT(searches.listen(values, first, {}, onListen, [&](bool ok) { firstResults.push_back(ok); }) != 0);
|
||||
CPPUNIT_ASSERT(searches.listen(values, second, {}, onListen, [&](bool ok) { secondResults.push_back(ok); }) != 0);
|
||||
searches.onReady(first, false);
|
||||
CPPUNIT_ASSERT(firstResults == std::vector<bool>({false}));
|
||||
CPPUNIT_ASSERT(secondResults.empty());
|
||||
secondSource(dht::ListenSyncStatus::SYNCED);
|
||||
searches.onSearchReady();
|
||||
CPPUNIT_ASSERT(secondResults == std::vector<bool>({true}));
|
||||
}
|
||||
|
||||
void
|
||||
OpCacheTester::testInitialReadinessMultipleSources()
|
||||
{
|
||||
dht::OpCache cache(true, true);
|
||||
auto query = std::make_shared<dht::Query>();
|
||||
unsigned calls = 0;
|
||||
CPPUNIT_ASSERT(cache.addListener(
|
||||
1,
|
||||
[](const Values&, bool) { return true; },
|
||||
query,
|
||||
{},
|
||||
[&](bool ok) {
|
||||
CPPUNIT_ASSERT(ok);
|
||||
++calls;
|
||||
}));
|
||||
cache.onSearchReady();
|
||||
CPPUNIT_ASSERT_EQUAL(0u, calls);
|
||||
cache.onNodeChanged(dht::ListenSyncStatus::ADDED);
|
||||
cache.onNodeChanged(dht::ListenSyncStatus::ADDED);
|
||||
cache.onNodeChanged(dht::ListenSyncStatus::SYNCED);
|
||||
CPPUNIT_ASSERT_EQUAL(0u, calls);
|
||||
cache.onNodeChanged(dht::ListenSyncStatus::SYNCED);
|
||||
CPPUNIT_ASSERT_EQUAL(1u, calls);
|
||||
cache.onSearchReady();
|
||||
cache.onNodeChanged(dht::ListenSyncStatus::UNSYNCED);
|
||||
cache.onNodeChanged(dht::ListenSyncStatus::SYNCED);
|
||||
CPPUNIT_ASSERT_EQUAL(1u, calls);
|
||||
}
|
||||
|
||||
void
|
||||
OpCacheTester::testInitialReadinessCachedValues()
|
||||
{
|
||||
dht::OpCache cache(true, true);
|
||||
auto query = std::make_shared<dht::Query>();
|
||||
auto value = makeValue(42, 0, "initial");
|
||||
cache.onNodeChanged(dht::ListenSyncStatus::ADDED);
|
||||
cache.onValue({value, makeValue(43, 0, "filtered")}, false);
|
||||
cache.onNodeChanged(dht::ListenSyncStatus::SYNCED);
|
||||
cache.onSearchReady();
|
||||
std::vector<std::string> events;
|
||||
CPPUNIT_ASSERT(cache.addListener(
|
||||
1,
|
||||
[&](const Values& values, bool expired) {
|
||||
CPPUNIT_ASSERT(!expired);
|
||||
CPPUNIT_ASSERT_EQUAL(size_t(1), values.size());
|
||||
CPPUNIT_ASSERT_EQUAL(value->id, values.front()->id);
|
||||
events.emplace_back("value");
|
||||
return true;
|
||||
},
|
||||
query,
|
||||
[](const dht::Value& current) { return current.id == 42; },
|
||||
[&](bool ok) {
|
||||
CPPUNIT_ASSERT(ok);
|
||||
events.emplace_back("ready");
|
||||
}));
|
||||
CPPUNIT_ASSERT(events == std::vector<std::string>({"value", "ready"}));
|
||||
|
||||
unsigned calls = 0;
|
||||
CPPUNIT_ASSERT(!cache.addListener(2, [](const Values&, bool) { return false; }, query, {}, [&](bool) { ++calls; }));
|
||||
cache.onReady();
|
||||
CPPUNIT_ASSERT_EQUAL(0u, calls);
|
||||
}
|
||||
|
||||
void
|
||||
OpCacheTester::testInitialReadinessReusedOperation()
|
||||
{
|
||||
dht::SearchCache cache(true, true);
|
||||
auto query = std::make_shared<dht::Query>();
|
||||
dht::SyncCallback source;
|
||||
unsigned operations = 0;
|
||||
unsigned calls = 0;
|
||||
auto onListen = [&](dht::Sp<dht::Query>, dht::ValueCallback values, dht::SyncCallback sync) {
|
||||
++operations;
|
||||
source = std::move(sync);
|
||||
source(dht::ListenSyncStatus::ADDED);
|
||||
values({makeValue(42, 0, "cached")}, false);
|
||||
source(dht::ListenSyncStatus::SYNCED);
|
||||
return size_t(1);
|
||||
};
|
||||
auto values = [](const Values&, bool) {
|
||||
return true;
|
||||
};
|
||||
auto ready = [&](bool ok) {
|
||||
CPPUNIT_ASSERT(ok);
|
||||
++calls;
|
||||
};
|
||||
auto first = cache.listen(values, query, {}, onListen, ready);
|
||||
CPPUNIT_ASSERT(first != 0);
|
||||
CPPUNIT_ASSERT_EQUAL(0u, calls);
|
||||
cache.onSearchReady();
|
||||
CPPUNIT_ASSERT_EQUAL(1u, calls);
|
||||
CPPUNIT_ASSERT(cache.cancelListen(first, dht::clock::now()));
|
||||
|
||||
auto second = cache.listen(values, std::make_shared<dht::Query>(), {}, onListen, ready);
|
||||
CPPUNIT_ASSERT(second != 0);
|
||||
CPPUNIT_ASSERT_EQUAL(1u, operations);
|
||||
CPPUNIT_ASSERT_EQUAL(2u, calls);
|
||||
source(dht::ListenSyncStatus::UNSYNCED);
|
||||
CPPUNIT_ASSERT(cache.listen(values, query, {}, onListen, ready) != 0);
|
||||
CPPUNIT_ASSERT_EQUAL(2u, calls);
|
||||
source(dht::ListenSyncStatus::SYNCED);
|
||||
CPPUNIT_ASSERT_EQUAL(3u, calls);
|
||||
}
|
||||
|
||||
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::testSameSeqConflictExpireIgnored()
|
||||
{
|
||||
// A different payload with the same seq (e.g. the same signed content
|
||||
// re-signed with a randomized signature) is ignored on add and not
|
||||
// counted: its expiration must be ignored too.
|
||||
std::vector<Event> events;
|
||||
dht::OpValueCache cache(recordEvents(events));
|
||||
|
||||
auto x = makeValue(60, 0, "x");
|
||||
auto y = makeValue(60, 0, "y");
|
||||
|
||||
cache.onValuesAdded({x}); // A
|
||||
cache.onValuesAdded({y}); // B: same seq, different content
|
||||
CPPUNIT_ASSERT_EQUAL(1u, count(events, false));
|
||||
CPPUNIT_ASSERT(cache.get(60)->data == x->data);
|
||||
|
||||
cache.onValuesExpired({y}); // B
|
||||
CPPUNIT_ASSERT_EQUAL_MESSAGE("An uncounted version must not expire the value", (size_t) 1, cache.size());
|
||||
CPPUNIT_ASSERT_EQUAL(0u, count(events, true));
|
||||
|
||||
cache.onValuesExpired({x}); // A
|
||||
CPPUNIT_ASSERT_EQUAL((size_t) 0, cache.size());
|
||||
CPPUNIT_ASSERT_EQUAL(1u, count(events, true));
|
||||
}
|
||||
|
||||
void
|
||||
OpCacheTester::testCallbacks()
|
||||
{
|
||||
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::testSeqOnlyRefreshNotCounted()
|
||||
{
|
||||
// A node re-announcing the same content with a bumped seq must not be
|
||||
// counted as an additional source by the OpValueCache above it, or the
|
||||
// value would survive the node's departure.
|
||||
std::vector<Event> events;
|
||||
dht::OpValueCache parent(recordEvents(events));
|
||||
dht::TypeStore types;
|
||||
auto now = dht::clock::now();
|
||||
|
||||
auto v = makeValue(42, 0, "same");
|
||||
auto vBumped = makeValue(42, 1, "same");
|
||||
{
|
||||
dht::ValueCache node([&](const Values& vals, bool expired) { parent.onValue(vals, expired); });
|
||||
node.onValues({v}, {}, {}, types, now);
|
||||
node.onValues({vBumped}, {}, {}, types, now);
|
||||
CPPUNIT_ASSERT_EQUAL(1u, count(events, false));
|
||||
}
|
||||
CPPUNIT_ASSERT_EQUAL_MESSAGE("Node gone: its value must be expired once", 1u, count(events, true));
|
||||
CPPUNIT_ASSERT_EQUAL((size_t) 0, parent.size());
|
||||
}
|
||||
|
||||
void
|
||||
OpCacheTester::testHighChurnRefCountConsistency()
|
||||
{
|
||||
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
|
||||
@@ -1,79 +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(testSameSeqConflictExpireIgnored);
|
||||
CPPUNIT_TEST(testCallbacks);
|
||||
CPPUNIT_TEST(testFilters);
|
||||
CPPUNIT_TEST(testSyncStatus);
|
||||
CPPUNIT_TEST(testInitialListenerReadiness);
|
||||
CPPUNIT_TEST(testInitialReadinessCancellation);
|
||||
CPPUNIT_TEST(testInitialReadinessFailure);
|
||||
CPPUNIT_TEST(testInitialReadinessMultipleSources);
|
||||
CPPUNIT_TEST(testInitialReadinessCachedValues);
|
||||
CPPUNIT_TEST(testInitialReadinessReusedOperation);
|
||||
CPPUNIT_TEST(testGetWhileSynced);
|
||||
CPPUNIT_TEST(testGetWhileNotSynced);
|
||||
CPPUNIT_TEST(testGetEmptySynced);
|
||||
CPPUNIT_TEST(testGetWithoutCallback);
|
||||
CPPUNIT_TEST(testGetCallbackReturnsFalse);
|
||||
CPPUNIT_TEST(testTimestampOrdering);
|
||||
CPPUNIT_TEST(testSourceDropKeepsValue);
|
||||
CPPUNIT_TEST(testSeqOnlyRefreshNotCounted);
|
||||
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 testSameSeqConflictExpireIgnored();
|
||||
void testCallbacks();
|
||||
void testFilters();
|
||||
void testSyncStatus();
|
||||
void testInitialListenerReadiness();
|
||||
void testInitialReadinessCancellation();
|
||||
void testInitialReadinessFailure();
|
||||
void testInitialReadinessMultipleSources();
|
||||
void testInitialReadinessCachedValues();
|
||||
void testInitialReadinessReusedOperation();
|
||||
void testGetWhileSynced();
|
||||
void testGetWhileNotSynced();
|
||||
void testGetEmptySynced();
|
||||
void testGetWithoutCallback();
|
||||
void testGetCallbackReturnsFalse();
|
||||
void testTimestampOrdering();
|
||||
void testSourceDropKeepsValue();
|
||||
void testSeqOnlyRefreshNotCounted();
|
||||
void testHighChurnRefCountConsistency();
|
||||
void testValueUpdateSingleSourcePhantom();
|
||||
void testExpireAfterUpdate();
|
||||
void testSingleSourceMode();
|
||||
void testSingleSourceModeSearchCache();
|
||||
};
|
||||
|
||||
} // namespace test
|
||||
@@ -105,69 +105,6 @@ void
|
||||
ParsedMessageTester::setUp()
|
||||
{}
|
||||
|
||||
void
|
||||
ParsedMessageTester::testParseListenSync()
|
||||
{
|
||||
for (const auto& fields : std::vector<std::vector<int64_t>> {
|
||||
{1, 2, 3, 1},
|
||||
{0, 2, 3, 1},
|
||||
{1, 2, -1, 1},
|
||||
{1, 2, 3},
|
||||
{1, 2, 3, 1, 0}
|
||||
}) {
|
||||
msgpack::sbuffer buffer;
|
||||
msgpack::packer<msgpack::sbuffer> packer(buffer);
|
||||
packer.pack_map(3);
|
||||
packer.pack(KEY_Y);
|
||||
packer.pack(KEY_R);
|
||||
packer.pack(KEY_TID);
|
||||
packer.pack(1);
|
||||
packer.pack(KEY_R);
|
||||
packer.pack_map(2);
|
||||
packer.pack(KEY_VERSION);
|
||||
packer.pack(2);
|
||||
packer.pack(KEY_LISTEN_SYNC);
|
||||
packer.pack(fields);
|
||||
if (fields == std::vector<int64_t>({1, 2, 3, 1})) {
|
||||
auto message = parsePackedMessage(buffer);
|
||||
CPPUNIT_ASSERT(message.listenSync);
|
||||
CPPUNIT_ASSERT_EQUAL(uint64_t(1), message.listenSync->registration);
|
||||
CPPUNIT_ASSERT_EQUAL(uint64_t(2), message.listenSync->epoch);
|
||||
CPPUNIT_ASSERT_EQUAL(uint64_t(3), message.listenSync->sequence);
|
||||
CPPUNIT_ASSERT_EQUAL(uint64_t(1), message.listenSync->snapshotEnd);
|
||||
} else {
|
||||
CPPUNIT_ASSERT_THROW(parsePackedMessage(buffer), msgpack::type_error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
ParsedMessageTester::testParseRejectsMalformedSyncedValues()
|
||||
{
|
||||
for (unsigned failure = 0; failure < 3; ++failure) {
|
||||
msgpack::sbuffer buffer;
|
||||
msgpack::packer<msgpack::sbuffer> packer(buffer);
|
||||
packer.pack_map(3);
|
||||
packer.pack(KEY_Y);
|
||||
packer.pack(KEY_R);
|
||||
packer.pack(KEY_TID);
|
||||
packer.pack(1);
|
||||
packer.pack(KEY_R);
|
||||
packer.pack_map(3);
|
||||
packer.pack(KEY_VERSION);
|
||||
packer.pack(2);
|
||||
packer.pack(KEY_LISTEN_SYNC);
|
||||
packer.pack(ListenSync {1, 2, 1, 1});
|
||||
packer.pack(KEY_REQ_VALUES);
|
||||
packer.pack_array(1);
|
||||
if (failure == 0)
|
||||
packer.pack_nil();
|
||||
else
|
||||
packer.pack(failure == 1 ? MAX_VALUE_SIZE + 33 : 0);
|
||||
CPPUNIT_ASSERT_THROW(parsePackedMessage(buffer), msgpack::type_error);
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
ParsedMessageTester::tearDown()
|
||||
{}
|
||||
|
||||
@@ -27,8 +27,6 @@ class ParsedMessageTester : public CppUnit::TestFixture
|
||||
CPPUNIT_TEST(testParseRejectsEmptyFieldSelection);
|
||||
CPPUNIT_TEST(testParseRejectsEmptyErrorArray);
|
||||
CPPUNIT_TEST(testParseRejectsNonMapRequest);
|
||||
CPPUNIT_TEST(testParseListenSync);
|
||||
CPPUNIT_TEST(testParseRejectsMalformedSyncedValues);
|
||||
CPPUNIT_TEST(testParseErrorOnlyMessageKeepsRequestFieldsEmpty);
|
||||
CPPUNIT_TEST(testParseIgnoresIncompleteValueDataEntry);
|
||||
CPPUNIT_TEST(testCompleteEmpty);
|
||||
@@ -62,8 +60,6 @@ public:
|
||||
void testParseRejectsEmptyFieldSelection();
|
||||
void testParseRejectsEmptyErrorArray();
|
||||
void testParseRejectsNonMapRequest();
|
||||
void testParseListenSync();
|
||||
void testParseRejectsMalformedSyncedValues();
|
||||
void testParseErrorOnlyMessageKeepsRequestFieldsEmpty();
|
||||
void testParseIgnoresIncompleteValueDataEntry();
|
||||
void testCompleteEmpty();
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
#include "test_peerdiscovery.h"
|
||||
#include "opendht/value.h"
|
||||
|
||||
#include <mutex>
|
||||
#include <condition_variable>
|
||||
@@ -11,24 +10,8 @@ namespace test {
|
||||
|
||||
using namespace std::literals;
|
||||
|
||||
constexpr unsigned MULTICAST_PORT = 2222;
|
||||
constexpr auto DHT_NODE_NAME {"dht"sv};
|
||||
constexpr auto JAMI_NODE_NAME {"jami"sv};
|
||||
|
||||
struct DhtNode
|
||||
{
|
||||
dht::InfoHash nodeid;
|
||||
in_port_t node_port;
|
||||
dht::NetId nid;
|
||||
MSGPACK_DEFINE(nodeid, node_port, nid)
|
||||
};
|
||||
struct JamiNode
|
||||
{
|
||||
int num;
|
||||
char cha;
|
||||
std::string str;
|
||||
MSGPACK_DEFINE(num, cha, str)
|
||||
};
|
||||
constexpr dht::NetId NETWORK_ID = 10;
|
||||
constexpr in_port_t NODE_PORT = 50000;
|
||||
|
||||
CPPUNIT_TEST_SUITE_REGISTRATION(PeerDiscoveryTester);
|
||||
|
||||
@@ -39,60 +22,40 @@ PeerDiscoveryTester::setUp()
|
||||
void
|
||||
PeerDiscoveryTester::testMulticastToTwoNodes()
|
||||
{
|
||||
DhtNode dhtNode;
|
||||
dhtNode.nid = 10;
|
||||
dhtNode.node_port = 50000;
|
||||
dhtNode.nodeid = dht::InfoHash::get("opendht01");
|
||||
|
||||
JamiNode jamiNode;
|
||||
jamiNode.num = 100;
|
||||
jamiNode.cha = 'a';
|
||||
jamiNode.str = "jami01";
|
||||
const auto nodeId = dht::InfoHash::get("opendht01");
|
||||
|
||||
std::mutex lock;
|
||||
std::condition_variable cv;
|
||||
unsigned countDht {0};
|
||||
unsigned countJami {0};
|
||||
unsigned discovered {0};
|
||||
unsigned wrongNetwork {0};
|
||||
{
|
||||
std::unique_lock l(lock);
|
||||
dht::PeerDiscovery testDht(MULTICAST_PORT);
|
||||
dht::PeerDiscovery testJami(MULTICAST_PORT);
|
||||
dht::PeerDiscovery publisher;
|
||||
dht::PeerDiscovery browser;
|
||||
dht::PeerDiscovery otherNetworkBrowser;
|
||||
|
||||
testJami.startDiscovery<DhtNode>(DHT_NODE_NAME, [&](DhtNode&& v, dht::SockAddr&&) {
|
||||
CPPUNIT_ASSERT_EQUAL(dhtNode.node_port, v.node_port);
|
||||
CPPUNIT_ASSERT_EQUAL(dhtNode.nodeid, v.nodeid);
|
||||
CPPUNIT_ASSERT_EQUAL(dhtNode.nid, v.nid);
|
||||
browser.startDiscovery(NETWORK_ID, [&](const dht::InfoHash& peerId, dht::SockAddr&& address) {
|
||||
CPPUNIT_ASSERT_EQUAL(nodeId, peerId);
|
||||
CPPUNIT_ASSERT_EQUAL(NODE_PORT, address.getPort());
|
||||
{
|
||||
std::lock_guard l(lock);
|
||||
countDht++;
|
||||
discovered++;
|
||||
}
|
||||
cv.notify_all();
|
||||
});
|
||||
|
||||
testJami.startDiscovery(JAMI_NODE_NAME, [&](msgpack::object&& obj, dht::SockAddr&&) {
|
||||
auto v = obj.as<JamiNode>();
|
||||
CPPUNIT_ASSERT_EQUAL(jamiNode.num, v.num);
|
||||
CPPUNIT_ASSERT_EQUAL(jamiNode.cha, v.cha);
|
||||
CPPUNIT_ASSERT_EQUAL(jamiNode.str, v.str);
|
||||
{
|
||||
std::lock_guard l(lock);
|
||||
countJami++;
|
||||
}
|
||||
cv.notify_all();
|
||||
otherNetworkBrowser.startDiscovery(NETWORK_ID + 1, [&](const dht::InfoHash&, dht::SockAddr&&) {
|
||||
std::lock_guard l(lock);
|
||||
wrongNetwork++;
|
||||
});
|
||||
|
||||
testDht.startPublish(DHT_NODE_NAME, dhtNode);
|
||||
CPPUNIT_ASSERT(cv.wait_for(l, std::chrono::seconds(5), [&] { return countDht > 0; }));
|
||||
|
||||
testDht.startPublish(JAMI_NODE_NAME, jamiNode);
|
||||
CPPUNIT_ASSERT(cv.wait_for(l, std::chrono::seconds(5), [&] { return countDht > 1 and countJami > 0; }));
|
||||
// we don't verify count values since its a continious multicasting
|
||||
publisher.startPublish(nodeId, NETWORK_ID, NODE_PORT);
|
||||
CPPUNIT_ASSERT(cv.wait_for(l, std::chrono::seconds(5), [&] { return discovered > 0; }));
|
||||
CPPUNIT_ASSERT_EQUAL(0u, wrongNetwork);
|
||||
|
||||
l.unlock();
|
||||
testDht.stopPublish(DHT_NODE_NAME);
|
||||
testDht.stopPublish(JAMI_NODE_NAME);
|
||||
testJami.stopDiscovery(DHT_NODE_NAME);
|
||||
testJami.stopDiscovery(JAMI_NODE_NAME);
|
||||
publisher.stopPublish();
|
||||
browser.stopDiscovery();
|
||||
otherNetworkBrowser.stopDiscovery();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,601 +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::testSameContentHigherSeqIsRefresh()
|
||||
{
|
||||
// Value::operator== ignores seq: identical content with a bumped seq is
|
||||
// the same value re-announced, not an edition. Re-emitting it would make
|
||||
// a source-counting OpValueCache count this node twice.
|
||||
int addCount = 0;
|
||||
dht::ValueCache cache([&](const std::vector<std::shared_ptr<dht::Value>>& vals, bool expired) {
|
||||
if (!expired)
|
||||
addCount += vals.size();
|
||||
});
|
||||
dht::TypeStore types;
|
||||
auto now = std::chrono::steady_clock::now();
|
||||
|
||||
auto v1 = std::make_shared<dht::Value>();
|
||||
v1->id = 1;
|
||||
v1->seq = 0;
|
||||
v1->data = {'A'};
|
||||
cache.onValues({v1}, {}, {}, types, now);
|
||||
CPPUNIT_ASSERT_EQUAL(1, addCount);
|
||||
|
||||
auto sameContent = std::make_shared<dht::Value>();
|
||||
sameContent->id = 1;
|
||||
sameContent->seq = 1;
|
||||
sameContent->data = {'A'};
|
||||
auto later = now + std::chrono::minutes(5);
|
||||
cache.onValues({sameContent}, {}, {}, types, later);
|
||||
CPPUNIT_ASSERT_EQUAL_MESSAGE("Same content with a higher seq is a refresh, not an edit", 1, addCount);
|
||||
|
||||
// The refresh still extends the expiration (default type: 10 minutes)
|
||||
cache.expireValues(now + std::chrono::minutes(11));
|
||||
CPPUNIT_ASSERT_EQUAL((size_t) 1, cache.size());
|
||||
|
||||
auto edited = std::make_shared<dht::Value>();
|
||||
edited->id = 1;
|
||||
edited->seq = 2;
|
||||
edited->data = {'B'};
|
||||
cache.onValues({edited}, {}, {}, types, later);
|
||||
CPPUNIT_ASSERT_EQUAL(2, addCount);
|
||||
}
|
||||
|
||||
void
|
||||
ValueCacheTester::testShortExpirationRefreshLost()
|
||||
{
|
||||
// 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
|
||||
@@ -1,51 +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(testSameContentHigherSeqIsRefresh);
|
||||
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 testSameContentHigherSeqIsRefresh();
|
||||
void testShortExpirationRefreshLost();
|
||||
void testShortExpirationHighChurn();
|
||||
void testGracePeriodWhileSynced();
|
||||
void testUnsyncExpiresImmediately();
|
||||
};
|
||||
|
||||
} // namespace test
|
||||
+1
-2
@@ -405,8 +405,7 @@ cmd_loop(std::shared_ptr<DhtRunner>& node,
|
||||
return true;
|
||||
},
|
||||
{},
|
||||
dht::Where {rem},
|
||||
[](bool synced) { std::cout << "Listen: " << (synced ? "synced" : "not synced") << std::endl; });
|
||||
dht::Where {rem});
|
||||
auto t = token.get();
|
||||
std::cout << "Listening, token: " << t << std::endl;
|
||||
}
|
||||
|
||||
@@ -1,12 +1,9 @@
|
||||
{
|
||||
"default-registry": {
|
||||
"kind": "git",
|
||||
"baseline": "319504a5326aa870edde46438c5455fa76305a56",
|
||||
"baseline": "f3e10653cc27d62a37a3763cd84b38bca07c6075",
|
||||
"repository": "https://github.com/microsoft/vcpkg"
|
||||
},
|
||||
"overlay-ports": [
|
||||
"ports"
|
||||
],
|
||||
"registries": [
|
||||
{
|
||||
"kind": "artifact",
|
||||
|
||||
+2
-9
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "opendht",
|
||||
"version-string": "4.4.1",
|
||||
"version-string": "4.3.0",
|
||||
"port-version": 1,
|
||||
"description": "OpenDHT: A lightweight C++17 Distributed Hash Table library.",
|
||||
"homepage": "https://opendht.net",
|
||||
@@ -16,14 +16,7 @@
|
||||
},
|
||||
"fmt",
|
||||
"nettle",
|
||||
{
|
||||
"name": "shiftmedia-libgnutls",
|
||||
"platform": "windows & !mingw"
|
||||
},
|
||||
{
|
||||
"name": "libgnutls",
|
||||
"platform": "!windows | mingw"
|
||||
},
|
||||
"shiftmedia-libgnutls",
|
||||
"msgpack",
|
||||
"jsoncpp",
|
||||
"asio",
|
||||
|
||||
Reference in new issue
Block a user