// Copyright (c) 2014-2026 Savoir-faire Linux Inc. // SPDX-License-Identifier: MIT #include "dhtproxytester.h" // std #include #include #include #include #include #include #include using namespace std::chrono_literals; namespace test { CPPUNIT_TEST_SUITE_REGISTRATION(DhtProxyTester); void DhtProxyTester::setUp() { clientConfig.dht_config.node_config.max_peer_req_per_sec = -1; clientConfig.dht_config.node_config.max_req_per_sec = -1; nodePeer.run(0, clientConfig); nodeProxy = std::make_shared(); nodeProxy->run(0, clientConfig); auto bound = nodePeer.getBound(); if (bound.isUnspecified()) bound.setLoopback(); nodeProxy->bootstrap(bound); // auto serverCAIdentity = dht::crypto::generateEcIdentity("DHT Node CA"); uint16_t port = 1024 + (std::rand() % (65535 - 1024)); dht::ProxyServerConfig serverConfig; // serverConfig.identity = dht::crypto::generateIdentity("DHT Node", serverCAIdentity); serverConfig.port = port; // serverConfig.pushServer = "127.0.0.1:8090"; serverProxy = std::make_unique(nodeProxy, serverConfig); /*clientConfig.server_ca = serverCAIdentity.second; clientConfig.client_identity = dht::crypto::generateIdentity("DhtProxyTester"); clientConfig.push_node_id = "dhtnode";*/ clientConfig.proxy_server = "http://127.0.0.1:" + std::to_string(port); //"https://127.0.0.1:8080"; } void DhtProxyTester::tearDown() { nodePeer.join(); nodeClient.join(); bool done = false; std::condition_variable cv; std::mutex cv_m; nodeProxy->shutdown([&] { std::lock_guard lk(cv_m); done = true; cv.notify_all(); }); std::unique_lock lk(cv_m); CPPUNIT_ASSERT(cv.wait_for(lk, 15s, [&] { return done; })); serverProxy.reset(); nodeProxy.reset(); } void DhtProxyTester::testGetPut() { nodeClient.run(0, clientConfig); bool done = false; std::condition_variable cv; std::mutex cv_m; auto key = dht::InfoHash::get("GLaDOs"); dht::Value val {"Hey! It's been a long time. How have you been?"}; auto val_data = val.data; { nodePeer.put(key, std::move(val), [&](bool) { std::lock_guard lk(cv_m); done = true; cv.notify_all(); }); std::unique_lock lk(cv_m); CPPUNIT_ASSERT(cv.wait_for(lk, 10s, [&] { return done; })); } auto vals = nodeClient.get(key).get(); CPPUNIT_ASSERT(not vals.empty()); CPPUNIT_ASSERT(vals.front()->data == val_data); } void DhtProxyTester::testListen() { nodeClient.run(0, clientConfig); std::condition_variable cv; std::mutex cv_m; std::unique_lock lk(cv_m); auto key = dht::InfoHash::get("GLaDOs"); bool done = false; // If a peer send a value, the listen operation from the client // should retrieve this value dht::Value firstVal {"Hey! It's been a long time. How have you been?"}; auto firstVal_data = firstVal.data; nodePeer.put(key, std::move(firstVal), [&](bool ok) { CPPUNIT_ASSERT(ok); std::lock_guard lk(cv_m); done = true; cv.notify_all(); }); CPPUNIT_ASSERT(cv.wait_for(lk, 10s, [&] { return done; })); done = false; std::vector values; auto token = nodeClient.listen(key, [&](const std::vector>& v, bool expired) { if (not expired) { std::lock_guard lk(cv_m); for (const auto& value : v) values.emplace_back(value->data); done = true; cv.notify_all(); } return true; }); CPPUNIT_ASSERT(cv.wait_for(lk, 10s, [&] { return done; })); done = false; // Here values should contains 1 values CPPUNIT_ASSERT_EQUAL(static_cast(values.size()), 1); CPPUNIT_ASSERT(values.front() == firstVal_data); // And the listen should retrieve futures values // All values dht::Value secondVal {"You're a monster"}; auto secondVal_data = secondVal.data; nodePeer.put(key, std::move(secondVal)); CPPUNIT_ASSERT(cv.wait_for(lk, 10s, [&] { return done; })); nodeClient.cancelListen(key, std::move(token)); // Here values should contains 2 values CPPUNIT_ASSERT_EQUAL(static_cast(values.size()), 2); CPPUNIT_ASSERT(values.back() == secondVal_data); } void DhtProxyTester::testResubscribeGetValues() { clientConfig.push_token = "atlas"; nodeClient.run(0, clientConfig); bool done = false; std::condition_variable cv; std::mutex cv_m; std::unique_lock lk(cv_m); auto key = dht::InfoHash::get("GLaDOs"); // If a peer sent a value, the listen operation from the client // should retrieve this value dht::Value firstVal {"Hey! It's been a long time. How have you been?"}; auto firstVal_data = firstVal.data; nodePeer.put(key, std::move(firstVal), [&](bool ok) { std::lock_guard lk(cv_m); CPPUNIT_ASSERT(ok); done = true; cv.notify_all(); }); CPPUNIT_ASSERT(cv.wait_for(lk, 10s, [&] { return done; })); done = false; // Send a first subscribe, the value is sent via a push notification // So ignore values here. nodeClient.listen(key, [&](const std::vector>&, bool) { return true; }); cv.wait_for(lk, std::chrono::seconds(1)); // Reboot node (to avoid cache) nodeClient.join(); clientConfig.push_token = ""; nodeClient.run(0, clientConfig); // For the second subscribe, the proxy will return the value in the body std::vector> values; auto ftoken = nodeClient.listen(key, [&](const std::vector>& v, bool expired) { if (not expired) { std::lock_guard lk(cv_m); values.insert(values.end(), v.begin(), v.end()); done = true; cv.notify_all(); } return true; }); CPPUNIT_ASSERT(cv.wait_for(lk, 10s, [&] { return done; })); auto token = ftoken.get(); CPPUNIT_ASSERT(token); nodeClient.cancelListen(key, token); // Here values should still contains 1 values CPPUNIT_ASSERT_EQUAL((size_t) 1u, values.size()); CPPUNIT_ASSERT(firstVal_data == values.front()->data); } void DhtProxyTester::testPushNotification() { #ifndef OPENDHT_PUSH_NOTIFICATIONS fmt::print(stderr, "Push notifications are not supported in this build, skipping test\n"); return; #else static constexpr dht::ValueType::Id PUSH_EDITABLE_TYPE_ID = 4242; const dht::ValueType pushEditableType {PUSH_EDITABLE_TYPE_ID, "push-editable", std::chrono::seconds(1), dht::ValueType::DEFAULT_STORE_POLICY, [](dht::InfoHash, const std::shared_ptr&, std::shared_ptr&, const dht::InfoHash&, const dht::SockAddr&) { return true; }}; nodePeer.registerType(pushEditableType); nodeProxy->registerType(pushEditableType); clientConfig.push_token = "atlas"; nodeClient.run(0, clientConfig); nodeClient.registerType(pushEditableType); bool gotValues = false; bool gotExpiredValues = false; std::condition_variable cv; std::mutex cv_m; std::unique_lock lk(cv_m); auto key = dht::InfoHash::get("GLaDOs"); auto valueAuthor = dht::crypto::generateIdentity("PushNotificationAuthor"); std::vector> values; std::vector> expiredValues; auto ftoken = nodeClient.listen(key, [&](const std::vector>& v, bool expired) { std::lock_guard lk(cv_m); if (expired) { expiredValues.insert(expiredValues.end(), v.begin(), v.end()); gotExpiredValues = true; } else { values.insert(values.end(), v.begin(), v.end()); gotValues = true; } cv.notify_all(); return true; }); // Wait for listen to be sent to proxy cv.wait_for(lk, std::chrono::seconds(1)); // Peer puts a value dht::Value firstVal {"Hey! It's been a long time. How have you been?"}; firstVal.type = PUSH_EDITABLE_TYPE_ID; firstVal.id = 1337; firstVal.seq = 1; firstVal.sign(*valueAuthor.first); auto firstVal_data = firstVal.data; nodePeer.put(key, std::move(firstVal), [&](bool ok) { std::lock_guard lk(cv_m); CPPUNIT_ASSERT(ok); gotValues = true; cv.notify_all(); }); CPPUNIT_ASSERT(cv.wait_for(lk, 10s, [&] { return gotValues; })); gotValues = false; // Simulate push notification std::map push_data; push_data["key"] = key.toString(); push_data["t"] = std::to_string( std::chrono::duration_cast(std::chrono::system_clock::now().time_since_epoch()) .count()); CPPUNIT_ASSERT(nodeClient.pushNotificationReceived(push_data).get() == dht::PushNotificationResult::Values); // Wait for client to receive the value CPPUNIT_ASSERT(cv.wait_for(lk, 10s, [&] { return gotValues; })); gotValues = false; CPPUNIT_ASSERT_EQUAL((size_t) 1u, values.size()); CPPUNIT_ASSERT(firstVal_data == values.front()->data); // Peer edits the existing value (same id, higher seq) dht::Value secondVal {"You're a monster"}; secondVal.type = PUSH_EDITABLE_TYPE_ID; secondVal.id = 1337; secondVal.seq = 2; secondVal.sign(*valueAuthor.first); auto secondVal_data = secondVal.data; nodePeer.put(key, std::move(secondVal), [&](bool ok) { std::lock_guard lk(cv_m); CPPUNIT_ASSERT(ok); gotValues = true; cv.notify_all(); }); CPPUNIT_ASSERT(cv.wait_for(lk, 10s, [&] { return gotValues; })); gotValues = false; // Simulate push notification again push_data["t"] = std::to_string( std::chrono::duration_cast(std::chrono::system_clock::now().time_since_epoch()) .count()); CPPUNIT_ASSERT(nodeClient.pushNotificationReceived(push_data).get() == dht::PushNotificationResult::Values); // Wait for client to receive the edited value CPPUNIT_ASSERT(cv.wait_for(lk, 10s, [&] { return gotValues; })); gotValues = false; // OpValueCache should update the existing id with the new sequence/data CPPUNIT_ASSERT_EQUAL((size_t) 2u, values.size()); CPPUNIT_ASSERT_EQUAL((dht::Value::Id) 1337, values.back()->id); CPPUNIT_ASSERT_EQUAL((uint16_t) 2, values.back()->seq); CPPUNIT_ASSERT(secondVal_data == values.back()->data); // Simulate expiration push notification for the edited value push_data["exp"] = std::to_string((dht::Value::Id) 1337); push_data["t"] = std::to_string( std::chrono::duration_cast(std::chrono::system_clock::now().time_since_epoch()) .count()); CPPUNIT_ASSERT(nodeClient.pushNotificationReceived(push_data).get() == dht::PushNotificationResult::ValuesExpired); cv.wait_for(lk, 500ms, [&] { return gotExpiredValues; }); auto token = ftoken.get(); CPPUNIT_ASSERT(token); nodeClient.cancelListen(key, token); #endif } void DhtProxyTester::testProxyServerClientFullChain() { #ifndef OPENDHT_PUSH_NOTIFICATIONS fmt::print(stderr, "Push notifications are not supported in this build, skipping test\n"); return; #else static constexpr dht::ValueType::Id CHAIN_EDITABLE_TYPE_ID = 4343; const dht::ValueType chainEditableType {CHAIN_EDITABLE_TYPE_ID, "chain-editable", std::chrono::seconds(30), dht::ValueType::DEFAULT_STORE_POLICY, [](dht::InfoHash, const std::shared_ptr&, std::shared_ptr&, const dht::InfoHash&, const dht::SockAddr&) { return true; }}; uint16_t pushPort = 1024 + (std::rand() % (65535 - 1024)); uint16_t proxyPort = 1024 + (std::rand() % (65535 - 1024)); std::atomic_bool relayStop {false}; std::atomic_size_t relayedPushCount {0}; std::thread pushRelay([&] { try { asio::io_context io; asio::ip::tcp::acceptor acceptor(io, asio::ip::tcp::endpoint(asio::ip::address_v4::loopback(), pushPort)); acceptor.non_blocking(true); while (not relayStop.load()) { asio::ip::tcp::socket socket(io); std::error_code ec; acceptor.accept(socket, ec); if (ec == asio::error::would_block || ec == asio::error::try_again) { std::this_thread::sleep_for(5ms); continue; } if (ec) continue; socket.non_blocking(true); std::string raw; raw.reserve(4096); auto deadline = std::chrono::steady_clock::now() + 2s; while (raw.find("\r\n\r\n") == std::string::npos && std::chrono::steady_clock::now() < deadline && not relayStop.load()) { std::array chunk {}; size_t n = socket.read_some(asio::buffer(chunk), ec); if (!ec) { raw.append(chunk.data(), n); } else if (ec == asio::error::would_block || ec == asio::error::try_again) { std::this_thread::sleep_for(2ms); } else { break; } } if (raw.find("\r\n\r\n") == std::string::npos) continue; auto sep = raw.find("\r\n\r\n"); std::string head = raw.substr(0, sep + 4); auto clPos = head.find("Content-Length:"); size_t contentLength = 0; if (clPos != std::string::npos) { clPos += std::string("Content-Length:").size(); while (clPos < head.size() && (head[clPos] == ' ' || head[clPos] == '\t')) ++clPos; size_t clEnd = head.find("\r\n", clPos); contentLength = std::stoul(head.substr(clPos, clEnd - clPos)); } std::string body = raw.substr(sep + 4); while (body.size() < contentLength && std::chrono::steady_clock::now() < deadline && not relayStop.load()) { std::array chunk {}; size_t n = socket.read_some(asio::buffer(chunk), ec); if (!ec) { body.append(chunk.data(), n); } else if (ec == asio::error::would_block || ec == asio::error::try_again) { std::this_thread::sleep_for(2ms); } else { break; } } if (body.size() > contentLength) body.resize(contentLength); Json::Value payload; std::string err; auto reader = std::unique_ptr(Json::CharReaderBuilder().newCharReader()); if (reader->parse(body.data(), body.data() + body.size(), &payload, &err) && payload.isMember("notifications") && payload["notifications"].isArray() && !payload["notifications"].empty() && payload["notifications"][0].isMember("data")) { const auto& data = payload["notifications"][0]["data"]; std::map notif; for (const auto& name : data.getMemberNames()) { const auto& value = data[name]; if (value.isString()) notif[name] = value.asString(); else if (value.isBool()) notif[name] = value.asBool() ? "true" : "false"; else if (value.isUInt64()) notif[name] = std::to_string(value.asUInt64()); else if (value.isInt64()) notif[name] = std::to_string(value.asInt64()); } nodeClient.pushNotificationReceived(notif); relayedPushCount++; } static const std::string okResp = "HTTP/1.1 200 OK\r\nContent-Length: 2\r\n\r\nOK"; asio::write(socket, asio::buffer(okResp), ec); } } catch (...) { } }); struct RelayGuard { std::atomic_bool& stop; std::thread& th; ~RelayGuard() { stop = true; if (th.joinable()) th.join(); } } relayGuard {relayStop, pushRelay}; serverProxy.reset(); dht::ProxyServerConfig serverConfig; serverConfig.port = proxyPort; serverConfig.pushServer = "127.0.0.1:" + std::to_string(pushPort); serverProxy = std::make_unique(nodeProxy, serverConfig); clientConfig.proxy_server = "http://127.0.0.1:" + std::to_string(proxyPort); clientConfig.push_token = "relay-token"; clientConfig.push_platform = "android"; nodePeer.registerType(chainEditableType); nodeProxy->registerType(chainEditableType); nodeClient.run(0, clientConfig); nodeClient.registerType(chainEditableType); bool done = false; std::condition_variable cv; std::mutex cv_m; std::unique_lock lk(cv_m); auto key = dht::InfoHash::get("proxy-full-chain"); auto valueAuthor = dht::crypto::generateIdentity("ProxyChainAuthor"); std::vector> values; auto token = nodeClient.listen(key, [&](const std::vector>& incoming, bool expired) { if (expired) return true; std::lock_guard lk(cv_m); values.insert(values.end(), incoming.begin(), incoming.end()); done = true; cv.notify_all(); return true; }); dht::Value firstVal {"first-version"}; firstVal.type = CHAIN_EDITABLE_TYPE_ID; firstVal.id = 424242; firstVal.seq = 1; firstVal.sign(*valueAuthor.first); auto firstData = firstVal.data; nodePeer.put(key, std::move(firstVal), [&](bool ok) { std::lock_guard lk(cv_m); CPPUNIT_ASSERT(ok); cv.notify_all(); }); CPPUNIT_ASSERT(cv.wait_for(lk, 10s, [&] { return done; })); done = false; CPPUNIT_ASSERT_EQUAL((size_t) 1u, values.size()); CPPUNIT_ASSERT_EQUAL((dht::Value::Id) 424242, values.back()->id); CPPUNIT_ASSERT_EQUAL((uint16_t) 1, values.back()->seq); CPPUNIT_ASSERT(firstData == values.back()->data); dht::Value secondVal {"second-version"}; secondVal.type = CHAIN_EDITABLE_TYPE_ID; secondVal.id = 424242; secondVal.seq = 2; secondVal.sign(*valueAuthor.first); auto secondData = secondVal.data; nodePeer.put(key, std::move(secondVal), [&](bool ok) { std::lock_guard lk(cv_m); CPPUNIT_ASSERT(ok); cv.notify_all(); }); CPPUNIT_ASSERT(cv.wait_for(lk, 10s, [&] { return done; })); done = false; CPPUNIT_ASSERT_EQUAL((size_t) 2u, values.size()); CPPUNIT_ASSERT_EQUAL((dht::Value::Id) 424242, values.back()->id); CPPUNIT_ASSERT_EQUAL((uint16_t) 2, values.back()->seq); CPPUNIT_ASSERT(secondData == values.back()->data); CPPUNIT_ASSERT(relayedPushCount.load() >= 2); // Older sequence should not update current cached value dht::Value staleVal {"stale-version"}; staleVal.type = CHAIN_EDITABLE_TYPE_ID; staleVal.id = 424242; staleVal.seq = 1; staleVal.sign(*valueAuthor.first); nodePeer.put(key, std::move(staleVal), [&](bool ok) { std::lock_guard lk(cv_m); CPPUNIT_ASSERT(ok); cv.notify_all(); }); CPPUNIT_ASSERT(not cv.wait_for(lk, 1s, [&] { return done; })); CPPUNIT_ASSERT_EQUAL((size_t) 2u, values.size()); CPPUNIT_ASSERT_EQUAL((uint16_t) 2, values.back()->seq); CPPUNIT_ASSERT(secondData == values.back()->data); // New id should be delivered as an additional value dht::Value thirdVal {"third-value"}; thirdVal.type = CHAIN_EDITABLE_TYPE_ID; thirdVal.id = 424243; thirdVal.seq = 1; thirdVal.sign(*valueAuthor.first); auto thirdData = thirdVal.data; nodePeer.put(key, std::move(thirdVal), [&](bool ok) { std::lock_guard lk(cv_m); CPPUNIT_ASSERT(ok); cv.notify_all(); }); CPPUNIT_ASSERT(cv.wait_for(lk, 10s, [&] { return done; })); done = false; CPPUNIT_ASSERT_EQUAL((size_t) 3u, values.size()); CPPUNIT_ASSERT_EQUAL((dht::Value::Id) 424243, values.back()->id); CPPUNIT_ASSERT_EQUAL((uint16_t) 1, values.back()->seq); CPPUNIT_ASSERT(thirdData == values.back()->data); CPPUNIT_ASSERT(relayedPushCount.load() >= 3); nodeClient.cancelListen(key, std::move(token)); #endif } void DhtProxyTester::testPutGet40KChars() { nodeClient.run(0, clientConfig); constexpr size_t N = 40000; // Arrange auto key = dht::InfoHash::get("testPutGet40KChars"); std::vector> values; std::vector mtu; mtu.reserve(N); for (size_t i = 0; i < N; i++) mtu.emplace_back((i % 2) ? 'T' : 'M'); std::condition_variable cv; std::mutex cv_m; std::unique_lock lk(cv_m); bool done_put = false; bool done_get = false; // Act dht::Value val {mtu}; nodePeer.put(key, std::move(val), [&](bool ok) { std::lock_guard lk(cv_m); done_put = ok; cv.notify_all(); }); CPPUNIT_ASSERT(cv.wait_for(lk, 10s, [&] { return done_put; })); nodeClient.get( key, [&](const std::vector>& vals) { values.insert(values.end(), vals.begin(), vals.end()); return true; }, [&](bool ok) { std::lock_guard lk(cv_m); done_get = ok; cv.notify_all(); }); CPPUNIT_ASSERT(cv.wait_for(lk, 10s, [&] { return done_get; })); // Assert CPPUNIT_ASSERT_EQUAL((size_t) 1u, values.size()); for (const auto& value : values) CPPUNIT_ASSERT(value->data == mtu); } void DhtProxyTester::testFuzzy() { constexpr size_t N = 40000; // Arrange auto key = dht::InfoHash::get("testFuzzy"); std::vector> values; std::vector mtu; mtu.reserve(N); for (size_t i = 0; i < N; i++) mtu.emplace_back((i % 2) ? 'T' : 'M'); // Act for (size_t i = 0; i < 100; i++) { auto nodeTest = std::make_shared(); nodeTest->run(0, clientConfig); nodeTest->put(key, dht::Value(mtu), [&](bool ok) { CPPUNIT_ASSERT(ok); }); nodeTest->get( key, [&](const std::vector>& vals) { values.insert(values.end(), vals.begin(), vals.end()); return true; }, [&](bool ok) { CPPUNIT_ASSERT(ok); }); std::this_thread::sleep_for(5ms); } // Assert for (const auto& value : values) CPPUNIT_ASSERT(value->data == mtu); } void DhtProxyTester::testShutdownStop() { constexpr size_t N = 40000; constexpr unsigned C = 100; // Arrange auto key = dht::InfoHash::get("testShutdownStop"); std::vector> values; std::vector mtu; mtu.reserve(N); for (size_t i = 0; i < N; i++) mtu.emplace_back((i % 2) ? 'T' : 'M'); std::atomic_uint callback_count {0}; // Act for (size_t i = 0; i < C; i++) { auto nodeTest = std::make_shared(); nodeTest->run(0, clientConfig); nodeTest->put(key, dht::Value(mtu), [&](bool /*ok*/) { callback_count++; }); nodeTest->get( key, [&](const std::vector>& vals) { values.insert(values.end(), vals.begin(), vals.end()); return true; }, [&](bool /*ok*/) { callback_count++; }); bool done = false; std::condition_variable cv; std::mutex cv_m; nodeTest->shutdown( [&] { std::lock_guard lk(cv_m); done = true; cv.notify_all(); }, true); std::unique_lock lk(cv_m); CPPUNIT_ASSERT(cv.wait_for(lk, 10s, [&] { return done; })); } CPPUNIT_ASSERT_EQUAL(2 * C, callback_count.load()); } } // namespace test