Files
opendht/src/dht.cpp
T
Cyrille BéraudandCopilot 9b9056c0c8 dht: do not report unfiltered answers to filtered queries
Query::isSatisfiedBy() considers an unfiltered answer sufficient for a
filtered query, which is true for a vanilla get: onGetValuesDone() applies
the operation filter to the values before calling back.

A projected query is different. It receives the fields another query selected
and can no longer evaluate its own filters on them, all the more so as those
filters may target fields that were not selected at all. Two concurrent
queries such as "SELECT id" and "SELECT id WHERE value_type=1" therefore made
the filtered callback report identifiers whose type does not match, and
searchNodeGetDone() additionally cancelled its own request so it never got a
correctly filtered answer.

Add Query::isProjectionSatisfiedBy(), which additionally requires both queries
to apply the same filters, and use it for operations with a query callback.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-10-26 09:21:10 -04:00

2965 lines
106 KiB
C++

// Copyright (c) 2014-2026 Savoir-faire Linux Inc.
// SPDX-License-Identifier: MIT
#include "dht.h"
#include "rng.h"
#include "search.h"
#include "storage.h"
#include "request.h"
#include <msgpack.hpp>
#include <algorithm>
#include <random>
#include <sstream>
#include <fstream>
#include <inttypes.h>
namespace dht {
using namespace std::placeholders;
constexpr std::chrono::minutes Dht::MAX_STORAGE_MAINTENANCE_EXPIRE_TIME;
constexpr std::chrono::minutes Dht::SEARCH_EXPIRE_TIME;
constexpr std::chrono::seconds Dht::BOOTSTRAP_PERIOD;
constexpr duration Dht::LISTEN_EXPIRE_TIME;
constexpr duration Dht::LISTEN_EXPIRE_TIME_PUBLIC;
constexpr duration Dht::REANNOUNCE_MARGIN;
static constexpr size_t MAX_REQUESTS_PER_SEC {8 * 1024};
static constexpr duration BOOTSTRAP_PERIOD_MAX {std::chrono::hours(24)};
NodeStatus
Dht::updateStatus(sa_family_t af)
{
auto& d = dht(af);
auto old = d.status;
d.status = d.getStatus(scheduler.time());
if (d.status == NodeStatus::Disconnected && bootstrap_pending && bootstrapJob && !bootstrap_nodes.empty())
d.status = NodeStatus::Connecting;
if (d.status != old) {
auto& other = dht(af == AF_INET ? AF_INET6 : AF_INET);
if (other.status == NodeStatus::Disconnected && d.status == NodeStatus::Disconnected) {
onDisconnected();
} else if (other.status == NodeStatus::Connected || d.status == NodeStatus::Connected) {
onConnected();
}
}
return d.status;
}
NodeStatus
Dht::Kad::getStatus(time_point now) const
{
unsigned dubious = 0;
for (const auto& b : buckets) {
for (auto& n : b.nodes) {
if (n->isGood(now)) {
return NodeStatus::Connected;
} else if (not n->isExpired())
dubious++;
}
}
auto& ping = pending_pings;
if (dubious or ping)
return NodeStatus::Connecting;
return NodeStatus::Disconnected;
}
void
Dht::shutdown(ShutdownCallback cb, bool stop)
{
if (not persistPath.empty())
saveState(persistPath);
if (stop) {
for (auto dht : {&dht4, &dht6}) {
for (auto& sr : dht->searches) {
for (const auto& r : sr.second->callbacks)
r.second.done_cb(false, {});
sr.second->callbacks.clear();
for (const auto& a : sr.second->announce) {
for (auto& cb : a.callbacks)
cb(false, {});
}
sr.second->announce.clear();
sr.second->listeners.clear();
}
}
network_engine.clear();
}
if (not maintain_storage) {
if (cb)
cb();
return;
}
// Last store maintenance
scheduler.syncTime();
auto remaining = std::make_shared<int>(0);
auto str_donecb = [=](bool, const std::vector<Sp<Node>>&) {
--*remaining;
if (logger_)
logger_->warn("Shutting down node: {} ops remaining", *remaining);
if (!*remaining && cb) {
cb();
}
};
for (auto& str : store)
*remaining += maintainStorage(str, true, str_donecb);
if (logger_)
logger_->warn("Shutting down node: after storage, {} ops", *remaining);
if (!*remaining) {
if (cb)
cb();
}
}
bool
Dht::isRunning(sa_family_t af) const
{
return network_engine.isRunning(af);
}
/* Every bucket contains an unordered list of nodes. */
const Sp<Node>
Dht::findNode(const InfoHash& id, sa_family_t af) const
{
if (const Bucket* b = findBucket(id, af))
for (const auto& n : b->nodes)
if (n->id == id)
return n;
return {};
}
/* Every bucket caches the address of a likely node. Ping it. */
void
Dht::sendCachedPing(Bucket& b)
{
if (b.cached)
if (logger_)
logger_->debug("[node {}] Sending ping to cached node", b.cached->toString());
b.sendCachedPing(network_engine);
}
std::vector<SockAddr>
Dht::getPublicAddress(sa_family_t family)
{
std::vector<SockAddr> ret;
if (family == AF_UNSPEC) {
auto& d4 = dht(AF_INET).reported_addr;
auto& d6 = dht(AF_INET6).reported_addr;
ret.reserve(d4.size() + d6.size());
for (const auto& a : d4)
ret.emplace_back(a.second);
for (const auto& a : d6)
ret.emplace_back(a.second);
return ret;
}
auto& reported_addr = dht(family).reported_addr;
ret.reserve(!family ? reported_addr.size() : reported_addr.size() / 2);
for (const auto& addr : reported_addr)
if (!family || family == addr.second.getFamily())
ret.emplace_back(addr.second);
return ret;
}
std::pair<size_t, size_t>
Dht::getLocalStoreSize() const
{
return {local_store_quota->size(), local_store_quota->valueCount()};
}
bool
Dht::trySearchInsert(const Sp<Node>& node)
{
const auto& now = scheduler.time();
if (not node)
return false;
auto& srs = searches(node->getFamily());
auto closest = srs.lower_bound(node->id);
bool inserted {false};
// insert forward
for (auto it = closest; it != srs.end(); it++) {
auto& s = *it->second;
if (s.insertNode(node, now)) {
inserted = true;
scheduler.edit(s.nextSearchStep, now);
} else if (not s.expired and not s.done)
break;
}
// insert backward
for (auto it = closest; it != srs.begin();) {
--it;
auto& s = *it->second;
if (s.insertNode(node, now)) {
inserted = true;
scheduler.edit(s.nextSearchStep, now);
} else if (not s.expired and not s.done)
break;
}
return inserted;
}
void
Dht::reportedAddr(const SockAddr& addr)
{
auto& reported_addr = dht(addr.getFamily()).reported_addr;
auto firstBefore = reported_addr.empty() ? nullptr : reported_addr.begin()->second.get();
auto it = std::find_if(reported_addr.begin(), reported_addr.end(), [&](const ReportedAddr& a) {
return a.second == addr;
});
if (it == reported_addr.end()) {
if (reported_addr.size() < 32)
reported_addr.emplace_back(1, addr);
} else
it->first++;
std::sort(reported_addr.begin(), reported_addr.end(), [](const ReportedAddr& a, const ReportedAddr& b) {
return a.first > b.first;
});
if (publicAddressChangedCb_) {
auto firstAfter = reported_addr.begin()->second.get();
if (firstBefore != firstAfter) {
auto& otherDht = dht(addr.getFamily() == AF_INET ? AF_INET6 : AF_INET).reported_addr;
std::vector<SockAddr> v;
v.reserve(otherDht.empty() ? 1 : 2);
v.emplace_back(reported_addr.begin()->second);
if (not otherDht.empty())
v.emplace_back(otherDht.begin()->second);
publicAddressChangedCb_(std::move(v));
}
}
}
/* We just learnt about a node, not necessarily a new one. Confirm is 1 if
the node sent a message, 2 if it sent us a reply. */
void
Dht::onNewNode(const Sp<Node>& node, int confirm)
{
const auto& now = scheduler.time();
auto& b = buckets(node->getFamily());
auto wasEmpty = confirm < 2 && b.grow_time < now - std::chrono::minutes(5);
if (b.onNewNode(node, confirm, now, myid, network_engine) or confirm) {
trySearchInsert(node);
if (wasEmpty) {
scheduler.edit(nextNodesConfirmation, now + std::chrono::seconds(1));
}
}
}
/* Called periodically to purge known-bad nodes. Note that we're very
conservative here: broken nodes in the table don't do much harm, we'll
recover as soon as we find better ones. */
void
Dht::expireBuckets(RoutingTable& list)
{
for (auto& b : list) {
bool changed = false;
b.nodes.remove_if([&changed](const Sp<Node>& n) {
if (n->isExpired()) {
changed = true;
return true;
}
return false;
});
if (changed)
sendCachedPing(b);
}
}
void
Dht::expireSearches()
{
auto t = scheduler.time() - SEARCH_EXPIRE_TIME;
auto expired = [&](std::pair<const InfoHash, Sp<Search>>& srp) {
auto& sr = *srp.second;
auto b = sr.callbacks.empty() && sr.announce.empty() && sr.listeners.empty() && sr.step_time < t;
if (b) {
if (logger_)
logger_->debug("[search {}] Removing search", srp.first.to_view());
sr.clear();
return b;
} else {
return false;
}
};
erase_if(dht4.searches, expired);
erase_if(dht6.searches, expired);
}
void
Dht::searchNodeGetDone(const net::Request& req, net::RequestAnswer&& answer, std::weak_ptr<Search> ws, Sp<Query> query)
{
const auto& now = scheduler.time();
if (auto sr = ws.lock()) {
sr->insertNode(req.node, now, answer.ntoken);
if (auto srn = sr->getNode(req.node)) {
/* all other get requests which are satisfied by this answer
should not be sent anymore */
for (auto& g : sr->callbacks) {
auto& q = g.second.query;
if (q != query and g.second.isSatisfiedBy(*query)) {
auto dummy_req = std::make_shared<net::Request>();
dummy_req->cancel();
srn->getStatus[q] = std::move(dummy_req);
}
}
auto syncTime = srn->getSyncTime(scheduler.time());
if (srn->syncJob)
scheduler.edit(srn->syncJob, syncTime);
else
srn->syncJob = scheduler.add(syncTime, std::bind(&Dht::searchStep, this, ws));
}
onGetValuesDone(req.node, answer, sr, query);
}
}
void
Dht::searchNodeGetExpired(const net::Request& status, bool over, std::weak_ptr<Search> ws, Sp<Query> query)
{
if (auto sr = ws.lock()) {
if (auto srn = sr->getNode(status.node)) {
srn->candidate = not over;
if (over)
srn->getStatus.erase(query);
}
scheduler.edit(sr->nextSearchStep, scheduler.time());
}
}
void
Dht::paginate(std::weak_ptr<Search> ws, Sp<Query> query, SearchNode* n)
{
auto sr = ws.lock();
if (not sr)
return;
auto select_q = std::make_shared<Query>(Select {}.field(Value::Field::Id), query ? query->where : Where {});
auto onSelectDone = [this, ws, query](const net::Request& status, net::RequestAnswer&& answer) mutable {
// Retrieve search
auto sr = ws.lock();
if (not sr)
return;
const auto& id = sr->id;
// Retrieve search node
auto sn = sr->getNode(status.node);
if (not sn)
return;
// backward compatibility
if (answer.fields.empty()) {
searchNodeGetDone(status, std::move(answer), ws, query);
return;
}
for (const auto& fvi : answer.fields) {
try {
auto vid = fvi->index.at(Value::Field::Id).getInt();
if (vid == Value::INVALID_ID)
continue;
auto query_for_vid = std::make_shared<Query>(Select {}, Where {}.id(vid));
sn->pagination_queries[query].push_back(query_for_vid);
if (logger_)
logger_->debug("[search {}] [node {}] Sending {}",
id.toString(),
sn->node->toString(),
query_for_vid->toString());
sn->getStatus[query_for_vid] = network_engine.sendGetValues(
status.node,
id,
*query_for_vid,
-1,
std::bind(&Dht::searchNodeGetDone, this, _1, _2, ws, query),
std::bind(&Dht::searchNodeGetExpired, this, _1, _2, ws, query_for_vid));
} catch (const std::out_of_range&) {
if (logger_)
logger_->error("[search {}] [node {}] Received non-id field in response to "
"'SELECT id' request…",
id.toString(),
sn->node->toString());
}
}
};
/* add pagination query key for tracking ongoing requests. */
n->pagination_queries[query].push_back(select_q);
if (logger_)
logger_->debug("[search {}] [node {}] Sending {}", sr->id.toString(), n->node->toString(), select_q->toString());
n->getStatus[select_q] = network_engine.sendGetValues(
n->node, sr->id, *select_q, -1, onSelectDone, std::bind(&Dht::searchNodeGetExpired, this, _1, _2, ws, select_q));
}
Dht::SearchNode*
Dht::searchSendGetValues(Sp<Search> sr, SearchNode* pn, bool update)
{
if (sr->done or sr->currentlySolicitedNodeCount() >= MAX_REQUESTED_SEARCH_NODES)
return nullptr;
const auto& now = scheduler.time();
std::weak_ptr<Search> ws = sr;
auto cb = sr->callbacks.begin();
static const auto ANY_QUERY = std::make_shared<Query>(Select {}, Where {}, true);
do { /* for all requests to send */
SearchNode* n = nullptr;
auto& query = sr->callbacks.empty() ? ANY_QUERY : cb->second.query;
const time_point up = (not sr->callbacks.empty() and update) ? sr->getLastGetTime(*query) : time_point::min();
if (pn and pn->canGet(now, up, query)) {
n = pn;
} else {
for (auto& sn : sr->nodes) {
if (sn->canGet(now, up, query)) {
n = sn.get();
break;
}
}
}
if (sr->callbacks.empty()) { /* 'find_node' request */
if (not n)
return nullptr;
/* if (logger_)
logger_->d(sr->id, n->node->id, "[search %s] [node %s] Sending 'find_node'",
sr->id.toString().c_str(), n->node->toString().c_str());*/
n->getStatus[query] = network_engine
.sendFindNode(n->node,
sr->id,
-1,
std::bind(&Dht::searchNodeGetDone, this, _1, _2, ws, query),
std::bind(&Dht::searchNodeGetExpired, this, _1, _2, ws, query));
} else { /* 'get' request */
if (not n)
continue;
if (query and not query->select.empty()) {
/* The request contains a select. No need to paginate… */
/* if (logger_)
logger_->d(sr->id, n->node->id, "[search %s] [node %s] Sending 'get'",
sr->id.toString().c_str(), n->node->toString().c_str());*/
n->getStatus[query]
= network_engine.sendGetValues(n->node,
sr->id,
*query,
-1,
std::bind(&Dht::searchNodeGetDone, this, _1, _2, ws, query),
std::bind(&Dht::searchNodeGetExpired, this, _1, _2, ws, query));
} else
paginate(ws, query, n);
}
/* We only attempt to send one request. Return. */
return n;
} while (++cb != sr->callbacks.end());
/* no request were sent */
return nullptr;
}
void
Dht::searchSendAnnounceValue(const Sp<Search>& sr, unsigned syncLevel)
{
if (sr->announce.empty())
return;
std::weak_ptr<Search> ws = sr;
auto onDone = [this, ws](const net::Request& req,
net::RequestAnswer&& answer) { /* when put done */
if (auto sr = ws.lock()) {
onAnnounceDone(req.node, answer, sr);
scheduler.edit(sr->nextSearchStep, scheduler.time());
}
};
auto onExpired = [this, ws](const net::Request&, bool over) { /* when put expired */
if (over)
if (auto sr = ws.lock())
scheduler.edit(sr->nextSearchStep,
scheduler.time());
};
auto onSelectDone = [this, ws, onDone, onExpired](const net::Request& req, net::RequestAnswer&& answer) mutable {
/* on probing done */
auto sr = ws.lock();
if (not sr)
return;
const auto& now = scheduler.time();
sr->insertNode(req.node, now, answer.ntoken);
auto sn = sr->getNode(req.node);
if (not sn)
return;
if (not sn->isSynced(now)) {
/* Search is now unsynced. Let's call searchStep to sync again. */
scheduler.edit(sr->nextSearchStep, now);
return;
}
for (auto& a : sr->announce) {
if (sn->getAnnounceTime(a.value->id) > now)
continue;
bool hasValue {false};
uint16_t seq_no = 0;
try {
const auto& f = std::find_if(answer.fields.cbegin(),
answer.fields.cend(),
[&a](const Sp<FieldValueIndex>& i) {
return i->index.at(Value::Field::Id).getInt() == a.value->id;
});
if (f != answer.fields.cend() and *f) {
hasValue = true;
seq_no = static_cast<uint16_t>((*f)->index.at(Value::Field::SeqNum).getInt());
}
} catch (std::out_of_range&) {
}
auto next_refresh_time = now + getType(a.value->type).expiration;
auto& acked = sn->acked[a.value->id];
scheduler.cancel(acked.refresh);
/* only put the value if the node doesn't already have it */
if (not hasValue or seq_no < a.value->seq) {
if (logger_)
logger_->debug("[search {}] [node {}] Sending 'put' (vid: {:016x})",
sr->id.toString(),
sn->node->toString(),
a.value->id);
auto created = a.permanent ? time_point::max() : a.created;
acked = {network_engine
.sendAnnounceValue(sn->node, sr->id, a.value, created, sn->token, onDone, onExpired),
next_refresh_time};
} else if (hasValue and a.permanent) {
if (logger_)
logger_->warn("[search {}] [node {}] Sending 'refresh' (vid: {:016x})",
sr->id.toString(),
sn->node->toString(),
a.value->id);
acked = {network_engine.sendRefreshValue(
sn->node,
sr->id,
a.value->id,
sn->token,
onDone,
[this,
ws,
node = sn->node,
v = a.value,
onDone,
onExpired,
created = a.permanent ? time_point::max() : a.created,
next_refresh_time](const net::Request& /*req*/, net::DhtProtocolException&& e) {
if (e.getCode() == net::DhtProtocolException::NOT_FOUND) {
if (logger_)
logger_->error("[node {}] Returned error 404: storage not found",
node->toString());
if (auto sr = ws.lock()) {
if (auto sn = sr->getNode(node)) {
sn->acked[v->id] = {network_engine.sendAnnounceValue(sn->node,
sr->id,
v,
created,
sn->token,
onDone,
onExpired),
next_refresh_time};
scheduler.edit(sr->nextSearchStep, scheduler.time());
return true;
}
}
}
return false;
},
onExpired),
next_refresh_time};
} else {
if (logger_)
logger_->warn("[search {}] [node {}] Already has value (vid: {:016x}). Aborting.",
sr->id.toString(),
sn->node->toString(),
a.value->id);
auto ack_req = std::make_shared<net::Request>(net::Request::State::COMPLETED);
ack_req->reply_time = now;
acked = {std::move(ack_req), next_refresh_time};
/* step to clear announces */
scheduler.edit(sr->nextSearchStep, now);
}
if (a.permanent) {
acked.refresh = scheduler.add(next_refresh_time - REANNOUNCE_MARGIN,
std::bind(&Dht::searchStep, this, ws));
}
}
};
static const auto PROBE_QUERY = std::make_shared<Query>(
Select {}.field(Value::Field::Id).field(Value::Field::SeqNum));
const auto& now = scheduler.time();
unsigned i = 0;
for (auto& np : sr->nodes) {
auto& n = *np;
if (not n.isSynced(now))
continue;
auto gs = n.probe_query ? n.getStatus.find(n.probe_query) : n.getStatus.end();
if (gs != n.getStatus.end() and gs->second and gs->second->pending()) {
continue;
}
bool sendQuery = false;
for (auto& a : sr->announce) {
if (n.getAnnounceTime(a.value->id) <= now) {
if (a.permanent) {
sendQuery = true;
} else {
if (logger_)
logger_->warn("[search {}] [node {}] Sending 'put' (vid: {:016x})",
sr->id.toString(),
n.node->toString(),
a.value->id);
n.acked[a.value->id] = {network_engine.sendAnnounceValue(
n.node, sr->id, a.value, a.created, n.token, onDone, onExpired),
now + getType(a.value->type).expiration};
}
}
}
if (sendQuery) {
n.probe_query = PROBE_QUERY;
if (logger_)
logger_->debug("[search {}] [node {}] Sending {}",
sr->id.toString(),
n.node->toString(),
n.probe_query->toString());
auto req = network_engine.sendGetValues(n.node,
sr->id,
*PROBE_QUERY,
-1,
onSelectDone,
std::bind(&Dht::searchNodeGetExpired, this, _1, _2, ws, PROBE_QUERY));
n.getStatus[PROBE_QUERY] = std::move(req);
}
if (not n.candidate and ++i == syncLevel)
break;
}
}
void
Dht::searchSynchedNodeListen(const Sp<Search>& sr, SearchNode& n)
{
const auto& listenExp = getListenExpiration();
std::weak_ptr<Search> ws = sr;
for (const auto& l : sr->listeners) {
const auto& query = l.second.query;
auto openListenSocket = [this, ws, query, node = n.node] {
return node->openSocket([this, ws, query](const Sp<Node>& node, net::RequestAnswer&& answer) mutable {
/* on new values */
if (auto sr = ws.lock()) {
scheduler.edit(sr->nextSearchStep, scheduler.time());
if (auto sn = sr->getNode(node)) {
if (not answer.ntoken.empty())
sn->refreshToken(answer.ntoken);
sn->onValues(query, std::move(answer), types, scheduler);
}
}
});
};
auto r = n.listenStatus.find(query);
if (n.getListenTime(r, listenExp) > scheduler.time())
continue;
// if (logger_)
// logger_->d(sr->id, n.node->id, "[search %s] [node %s] Sending 'listen'",
// sr->id.toString().c_str(), n.node->toString().c_str());
if (r == n.listenStatus.end()) {
r = n.listenStatus
.emplace(std::piecewise_construct,
std::forward_as_tuple(query),
std::forward_as_tuple(l.second.get_cb, l.second.sync_cb, openListenSocket()))
.first;
r->second.cacheExpirationJob = scheduler.add(time_point::max(), [this, ws, query, node = n.node] {
if (auto sr = ws.lock()) {
if (auto sn = sr->getNode(node)) {
sn->expireValues(query, scheduler);
}
}
});
} else if (not n.node->getSocket(r->second.socketId)) {
r->second.socketId = openListenSocket();
}
auto new_req = network_engine.sendListen(
n.node,
sr->id,
*query,
n.token,
r->second.socketId,
[this, ws, query](const net::Request& req, net::RequestAnswer&& answer) mutable {
/* on done */
if (auto sr = ws.lock()) {
scheduler.edit(sr->nextSearchStep, scheduler.time());
if (auto sn = sr->getNode(req.node)) {
auto job = scheduler.add(sn->getListenTime(query, getListenExpiration()),
std::bind(&Dht::searchStep, this, ws));
sn->onListenSynced(query, true, std::move(job));
}
onListenDone(req.node, answer, sr);
}
},
[this, ws, query](const net::Request& req, bool over) mutable {
/* on request expired */
if (auto sr = ws.lock()) {
scheduler.edit(sr->nextSearchStep, scheduler.time());
if (over)
if (auto sn = sr->getNode(req.node))
sn->listenStatus.erase(query);
}
});
// Here the request may have failed and the CachedListenStatus removed
r = n.listenStatus.find(query);
if (r != n.listenStatus.end()) {
r->second.req = new_req;
}
}
}
/* When a search is in progress, we periodically call search_step to send
further requests. */
void
Dht::searchStep(std::weak_ptr<Search> ws)
{
auto sr = ws.lock();
if (not sr or sr->expired or sr->done)
return;
const auto& now = scheduler.time();
auto level = sr->syncLevel(now);
constexpr auto MARGIN = 1;
bool preSynced = level > MARGIN;
auto syncLevel = preSynced ? level - MARGIN : 0;
/*if (auto req_count = sr->currentlySolicitedNodeCount())
if (logger_)
logger_->d(sr->id, "[search %s IPv%c] Step (%d requests)",
sr->id.toString().c_str(), sr->af == AF_INET ? '4' : '6', req_count);*/
sr->step_time = now;
if (sr->refill_time + Node::NODE_EXPIRE_TIME < now and sr->nodes.size() - sr->getNumberOfBadNodes() < SEARCH_NODES)
refill(*sr);
/* Check if the first TARGET_NODES (8) live nodes have replied. */
if (sr->isSynced(now)) {
if (not(sr->callbacks.empty() and sr->announce.empty())) {
// search is synced but some (newer) get operations are not complete
// Call callbacks when done
std::vector<Get> completed_gets;
for (auto b = sr->callbacks.begin(); b != sr->callbacks.end();) {
if (sr->isDone(b->second)) {
sr->setDone(b->second);
completed_gets.emplace_back(std::move(b->second));
b = sr->callbacks.erase(b);
} else
++b;
}
// clear corresponding queries
for (const auto& get : completed_gets)
for (auto& sn : sr->nodes) {
sn->getStatus.erase(get.query);
sn->pagination_queries.erase(get.query);
}
/* clearing callbacks for announced values */
sr->checkAnnounced();
if (sr->callbacks.empty() && sr->announce.empty() && sr->listeners.empty())
sr->setDone();
}
// true if this node is part of the target nodes cluter.
/*bool in = sr->id.xorCmp(myid, sr->nodes.back().node->id) < 0;
logger__DBG("[search %s IPv%c] synced%s",
sr->id.toString().c_str(), sr->af == AF_INET ? '4' : '6', in ? ", in" : "");*/
if (not sr->listeners.empty()) {
unsigned i = 0;
for (auto& n : sr->nodes) {
if (not n->isSynced(now))
continue;
searchSynchedNodeListen(sr, *n);
if (not n->candidate and ++i == LISTEN_NODES)
break;
}
}
// Announce requests
searchSendAnnounceValue(sr);
if (sr->callbacks.empty() && sr->announce.empty() && sr->listeners.empty())
sr->setDone();
} else if (preSynced) {
if (not sr->listeners.empty()) {
auto nl = std::min(syncLevel, LISTEN_NODES);
unsigned i = 0;
for (auto& n : sr->nodes) {
if (not n->isSynced(now))
break;
searchSynchedNodeListen(sr, *n);
if (++i == nl)
break;
}
}
// Announce requests
searchSendAnnounceValue(sr, syncLevel);
}
while (sr->currentlySolicitedNodeCount() < MAX_REQUESTED_SEARCH_NODES and searchSendGetValues(sr))
;
if (sr->getNumberOfConsecutiveBadNodes() >= std::min<size_t>(sr->nodes.size(), SEARCH_MAX_BAD_NODES)) {
if (logger_)
logger_->warn("[search {} IPv{}] Expired", sr->id.to_view(), sr->af == AF_INET ? '4' : '6');
sr->expire();
if (not public_stable)
connectivityChanged(sr->af);
}
/* dumpSearch(*sr, std::cout); */
}
unsigned
Dht::refill(Dht::Search& sr)
{
const auto& now = scheduler.time();
sr.refill_time = now;
/* we search for up to SEARCH_NODES good nodes. */
auto cached_nodes = network_engine.getCachedNodes(sr.id, sr.af, SEARCH_NODES);
if (cached_nodes.empty()) {
if (logger_)
logger_->error("[search {} IPv{}] No nodes from cache while refilling search",
sr.id.toString(),
(sr.af == AF_INET) ? '4' : '6');
return 0;
}
unsigned inserted = 0;
for (auto& i : cached_nodes) {
/* try to insert the nodes. Search::insertNode will know how many to insert. */
if (sr.insertNode(i, now))
++inserted;
}
if (logger_)
logger_->debug("[search {} IPv{}] Refilled search with {} nodes from node cache",
sr.id.toString(),
(sr.af == AF_INET) ? '4' : '6',
inserted);
return inserted;
}
/* Start a search. */
Sp<Dht::Search>
Dht::search(const InfoHash& id,
sa_family_t af,
GetCallback gcb,
QueryCallback qcb,
DoneCallback dcb,
Value::Filter f,
const Sp<Query>& q)
{
if (!isRunning(af)) {
if (logger_)
logger_->error("[search {} IPv{}] Unsupported protocol", id.to_view(), (af == AF_INET) ? '4' : '6');
if (dcb)
dcb(false, {});
return {};
}
auto& srs = searches(af);
const auto& srp = srs.find(id);
Sp<Search> sr {};
if (srp != srs.end()) {
sr = srp->second;
sr->done = false;
sr->expired = false;
} else {
if (srs.size() < max_searches) {
sr = std::make_shared<Search>();
srs.emplace(id, sr);
} else {
for (auto it = srs.begin(); it != srs.end(); ++it) {
auto& s = *it->second;
if ((s.done or s.expired) and s.announce.empty() and s.listeners.empty()) {
sr = it->second;
if (sr->nextSearchStep)
scheduler.cancel(sr->nextSearchStep);
if (sr->opExpirationJob)
scheduler.cancel(sr->opExpirationJob);
sr->clear();
srs.erase(it);
break;
}
}
if (not sr) {
if (logger_)
logger_->error("[search {} IPv{}] Maximum number of searches reached!",
id.toString(),
(af == AF_INET) ? '4' : '6');
if (dcb)
dcb(false, {});
return {};
}
srs.emplace(id, sr);
}
sr->af = af;
sr->tid = search_id++;
sr->step_time = time_point::min();
sr->id = id;
sr->done = false;
sr->expired = false;
sr->nodes.clear();
sr->nodes.reserve(SEARCH_NODES + 1);
sr->nextSearchStep = scheduler.add(time_point::max(),
std::bind(&Dht::searchStep, this, std::weak_ptr<Search>(sr)));
if (logger_)
logger_->warn("[search {} IPv{}] New search", id.to_view(), (af == AF_INET) ? '4' : '6');
if (search_id == 0)
search_id++;
}
sr->get(f, q, qcb, gcb, dcb, scheduler);
refill(*sr);
return sr;
}
void
Dht::announce(
const InfoHash& id, sa_family_t af, Sp<Value> value, DoneCallback callback, time_point created, bool permanent)
{
auto& srs = searches(af);
auto srp = srs.find(id);
if (auto sr = srp == srs.end() ? search(id, af) : srp->second) {
sr->put(value, callback, created, permanent);
scheduler.edit(sr->nextSearchStep, scheduler.time());
} else if (callback) {
callback(false, {});
}
}
size_t
Dht::listenTo(const InfoHash& id, sa_family_t af, ValueCallback cb, Value::Filter f, const Sp<Query>& q)
{
if (!isRunning(af))
return 0;
// logger__ERR("[search %s IPv%c] Search_time is now in %lfs", sr->id.toString().c_str(), (sr->af == AF_INET) ? '4'
// : '6', print_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()) ? 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');
return sr->listen(cb, std::move(f), q, scheduler);
}
size_t
Dht::listen(const InfoHash& id, ValueCallback cb, Value::Filter f, Where where)
{
if (not id) {
if (logger_)
logger_->warn("Listen called with invalid key");
return 0;
}
scheduler.syncTime();
auto token = ++listener_token;
auto gcb = OpValueCache::cacheCallback(std::move(cb), [this, id, token] { cancelListen(id, token); });
auto query = std::make_shared<Query>(Select {}, std::move(where));
auto filter = Value::Filter::chain(std::move(f), query->where.getFilter());
auto st = store.find(id);
if (st == store.end() && store.size() < max_store_keys)
st = store.emplace(id, scheduler.time() + MAX_STORAGE_MAINTENANCE_EXPIRE_TIME).first;
size_t tokenlocal = 0;
if (st != store.end()) {
tokenlocal = st->second.listen(gcb, filter, query);
if (tokenlocal == 0)
return 0;
}
auto token4 = Dht::listenTo(id, AF_INET, gcb, filter, query);
auto token6 = token4 == 0 ? 0 : Dht::listenTo(id, AF_INET6, gcb, filter, query);
if (token6 == 0 && st != store.end()) {
st->second.cancelListen(tokenlocal);
return 0;
}
listeners.emplace(token, std::make_tuple(tokenlocal, token4, token6));
return token;
}
bool
Dht::cancelListen(const InfoHash& id, size_t token)
{
scheduler.syncTime();
auto it = listeners.find(token);
if (it == listeners.end()) {
if (logger_)
logger_->warn("Listen token not found: {}", token);
return false;
}
if (logger_)
logger_->debug("cancelListen {} with token {}", id.to_view(), token);
if (auto tokenlocal = std::get<0>(it->second)) {
auto st = store.find(id);
if (st != store.end())
st->second.cancelListen(tokenlocal);
}
auto searches_cancel_listen = [this, &id](std::map<InfoHash, Sp<Search>>& srs, size_t token) {
if (token) {
auto srp = srs.find(id);
if (srp != srs.end())
srp->second->cancelListen(token, scheduler);
}
};
searches_cancel_listen(dht4.searches, std::get<1>(it->second));
searches_cancel_listen(dht6.searches, std::get<2>(it->second));
listeners.erase(it);
return true;
}
struct OpStatus
{
struct Status
{
bool done {false};
bool ok {false};
Status(bool done = false, bool ok = false)
: done(done)
, ok(ok)
{}
};
Status status;
Status status4;
Status status6;
};
template<typename T>
struct GetStatus : public OpStatus
{
T values;
std::vector<Sp<Node>> nodes;
};
void
Dht::put(const InfoHash& id, Sp<Value> val, DoneCallback callback, time_point created, bool permanent)
{
if (not id or not val) {
if (logger_)
logger_->warn("Put called with invalid key or value");
if (callback)
callback(false, {});
return;
}
if (val->id == Value::INVALID_ID)
val->id = std::uniform_int_distribution<Value::Id> {1}(rd);
scheduler.syncTime();
const auto& now = scheduler.time();
created = std::min(now, created);
storageStore(id, val, created, {}, permanent);
if (logger_)
logger_->debug("put: adding {} → {}", id.to_view(), val->toString());
auto op = std::make_shared<OpStatus>();
auto donecb = [callback](const std::vector<Sp<Node>>& nodes, OpStatus& op) {
// Callback as soon as the value is announced on one of the available networks
if (callback and not op.status.done and (op.status4.done && op.status6.done)) {
callback(op.status4.ok or op.status6.ok, nodes);
op.status.done = true;
}
};
announce(
id,
AF_INET,
val,
[=](bool ok4, const std::vector<Sp<Node>>& nodes) {
if (logger_)
logger_->debug("Announce done IPv4 {}", ok4);
auto& o = *op;
o.status4 = {true, ok4};
donecb(nodes, o);
},
created,
permanent);
announce(
id,
AF_INET6,
val,
[=](bool ok6, const std::vector<Sp<Node>>& nodes) {
if (logger_)
logger_->debug("Announce done IPv6 {}", ok6);
auto& o = *op;
o.status6 = {true, ok6};
donecb(nodes, o);
},
created,
permanent);
}
template<typename T>
void
doneCallbackWrapper(DoneCallback dcb, const std::vector<Sp<Node>>& nodes, GetStatus<T>& op)
{
if (op.status.done)
return;
op.nodes.insert(op.nodes.end(), nodes.begin(), nodes.end());
if (op.status.ok or (op.status4.done and op.status6.done)) {
bool ok = op.status.ok or op.status4.ok or op.status6.ok;
op.status.done = true;
if (dcb)
dcb(ok, op.nodes);
}
}
template<typename T, typename St, typename Cb, typename Av, typename Cv>
bool
callbackWrapper(
Cb get_cb, DoneCallback done_cb, const std::vector<Sp<T>>& values, Av add_values, Cv cache_values, GetStatus<St>& op)
{
if (op.status.done)
return false;
auto newvals = add_values(values);
if (not newvals.empty()) {
op.status.ok = !get_cb(newvals);
cache_values(newvals);
}
doneCallbackWrapper(done_cb, {}, op);
return !op.status.ok;
}
void
Dht::get(const InfoHash& id, GetCallback getcb, DoneCallback donecb, Value::Filter&& filter, Where&& where)
{
if (not id) {
if (logger_)
logger_->warn("Get called with invalid key");
if (donecb)
donecb(false, {});
return;
}
scheduler.syncTime();
auto op = std::make_shared<GetStatus<std::map<Value::Id, Sp<Value>>>>();
auto gcb = [getcb, donecb, op](const std::vector<Sp<Value>>& vals) {
auto& o = *op;
return callbackWrapper(
getcb,
donecb,
vals,
[&o](const std::vector<Sp<Value>>& values) {
std::vector<Sp<Value>> newvals {};
for (const auto& v : values) {
auto it = o.values.find(v->id);
if (it == o.values.cend() or (it->second != v && !(*it->second == *v))) {
newvals.push_back(v);
}
}
return newvals;
},
[&o](const std::vector<Sp<Value>>& newvals) {
for (const auto& v : newvals)
o.values[v->id] = v;
},
o);
};
auto q = std::make_shared<Query>(Select {}, std::move(where));
auto f = filter.chain(q->where.getFilter());
/* Try to answer this search locally. */
gcb(getLocal(id, f));
Dht::search(
id,
AF_INET,
gcb,
{},
[=](bool ok, const std::vector<Sp<Node>>& nodes) {
// logger__WARN("DHT done IPv4");
op->status4 = {true, ok};
doneCallbackWrapper(donecb, nodes, *op);
},
f,
q);
Dht::search(
id,
AF_INET6,
gcb,
{},
[=](bool ok, const std::vector<Sp<Node>>& nodes) {
// logger__WARN("DHT done IPv6");
op->status6 = {true, ok};
doneCallbackWrapper(donecb, nodes, *op);
},
f,
q);
}
void
Dht::query(const InfoHash& id, QueryCallback cb, DoneCallback done_cb, Query&& q)
{
if (not id) {
if (logger_)
logger_->warn("Query called with invalid key");
if (done_cb)
done_cb(false, {});
return;
}
scheduler.syncTime();
auto op = std::make_shared<GetStatus<std::vector<Sp<FieldValueIndex>>>>();
auto f = q.where.getFilter();
auto qcb = [cb, done_cb, op](const std::vector<Sp<FieldValueIndex>>& fields) {
auto& o = *op;
return callbackWrapper(
cb,
done_cb,
fields,
[&](const std::vector<Sp<FieldValueIndex>>& fields) {
std::vector<Sp<FieldValueIndex>> newvals {};
for (const auto& f : fields) {
auto it = std::find_if(o.values.cbegin(), o.values.cend(), [&](const Sp<FieldValueIndex>& sf) {
return sf == f or f->containedIn(*sf);
});
if (it == o.values.cend()) {
auto lesser = std::find_if(o.values.begin(),
o.values.end(),
[&](const Sp<FieldValueIndex>& sf) { return sf->containedIn(*f); });
if (lesser != o.values.end())
o.values.erase(lesser);
newvals.push_back(f);
}
}
return newvals;
},
[&](const std::vector<Sp<FieldValueIndex>>& fields) {
o.values.insert(o.values.end(), fields.begin(), fields.end());
},
o);
};
/* Try to answer this search locally. */
auto values = getLocal(id, f);
std::vector<Sp<FieldValueIndex>> local_fields(values.size());
std::transform(values.begin(), values.end(), local_fields.begin(), [&q](const Sp<Value>& v) {
return std::make_shared<FieldValueIndex>(*v, q.select);
});
qcb(local_fields);
auto sq = std::make_shared<Query>(std::move(q));
Dht::search(
id,
AF_INET,
{},
qcb,
[=](bool ok, const std::vector<Sp<Node>>& nodes) {
// logger__WARN("DHT done IPv4");
op->status4 = {true, ok};
doneCallbackWrapper(done_cb, nodes, *op);
},
f,
sq);
Dht::search(
id,
AF_INET6,
{},
qcb,
[=](bool ok, const std::vector<Sp<Node>>& nodes) {
// logger__WARN("DHT done IPv6");
op->status6 = {true, ok};
doneCallbackWrapper(done_cb, nodes, *op);
},
f,
sq);
}
std::vector<Sp<Value>>
Dht::getLocal(const InfoHash& id, const Value::Filter& f) const
{
auto s = store.find(id);
if (s == store.end())
return {};
return s->second.get(f);
}
Sp<Value>
Dht::getLocalById(const InfoHash& id, Value::Id vid) const
{
auto s = store.find(id);
if (s != store.end())
return s->second.getById(vid);
return {};
}
std::vector<Sp<Value>>
Dht::getPut(const InfoHash& id) const
{
std::vector<Sp<Value>> ret;
auto find_values = [&](const std::map<InfoHash, Sp<Search>>& srs) {
auto srp = srs.find(id);
if (srp == srs.end())
return;
auto vals = srp->second->getPut();
ret.insert(ret.end(), vals.begin(), vals.end());
};
find_values(dht4.searches);
find_values(dht6.searches);
return ret;
}
Sp<Value>
Dht::getPut(const InfoHash& id, const Value::Id& vid) const
{
auto find_value = [&](const std::map<InfoHash, Sp<Search>>& srs) {
auto srp = srs.find(id);
return (srp != srs.end()) ? srp->second->getPut(vid) : Sp<Value> {};
};
if (auto v4 = find_value(dht4.searches))
return v4;
if (auto v6 = find_value(dht6.searches))
return v6;
return {};
}
bool
Dht::cancelPut(const InfoHash& id, const Value::Id& vid)
{
bool canceled {false};
auto sr_cancel_put = [&](std::map<InfoHash, Sp<Search>>& srs) {
auto srp = srs.find(id);
return (srp != srs.end()) ? srp->second->cancelPut(vid) : false;
};
canceled |= sr_cancel_put(dht4.searches);
canceled |= sr_cancel_put(dht6.searches);
if (canceled) {
auto st = store.find(id);
if (st != store.end()) {
if (auto value = st->second.remove(id, vid))
storageRemoved(id, st->second, {value}, value->size());
}
}
return canceled;
}
// Storage
void
Dht::storageChanged(const InfoHash& id, Storage& st, const Sp<Value>& v, bool newValue)
{
if (newValue) {
if (not st.local_listeners.empty()) {
if (logger_)
logger_->debug("[store {}] {} local listeners", id.to_view(), st.local_listeners.size());
std::vector<std::pair<ValueCallback, std::vector<Sp<Value>>>> cbs;
cbs.reserve(st.local_listeners.size());
for (const auto& l : st.local_listeners) {
std::vector<Sp<Value>> vals;
if (not l.second.filter or l.second.filter(*v))
vals.push_back(v);
if (not vals.empty()) {
if (logger_)
logger_->debug("[store {}] Sending update local listener with token {}", id.to_view(), l.first);
cbs.emplace_back(l.second.get_cb, std::move(vals));
}
}
// listeners are copied: they may be deleted by the callback
for (auto& cb : cbs)
cb.first(cb.second, false);
}
}
if (not st.listeners.empty()) {
if (logger_)
logger_->debug("[store {}] {} remote listeners", id.to_view(), st.listeners.size());
for (const auto& node_listeners : st.listeners) {
for (const auto& l : node_listeners.second) {
auto f = l.second.query.where.getFilter();
if (f and not f(*v))
continue;
if (logger_)
logger_->debug("[store {}] [node {}] Sending update",
id.to_view(),
node_listeners.first->toString());
std::vector<Sp<Value>> vals;
vals.push_back(v);
Blob ntoken = makeToken(node_listeners.first->getAddr(), false);
network_engine.tellListener(node_listeners.first,
l.first,
id,
0,
ntoken,
{},
{},
std::move(vals),
l.second.query,
l.second.version);
}
}
}
}
bool
Dht::storageStore(const InfoHash& id,
const Sp<Value>& value,
time_point created,
const SockAddr& sa,
bool permanent,
time_point expiration)
{
const auto& now = scheduler.time();
created = std::min(created, now);
if (expiration == time_point::min())
expiration = permanent ? time_point::max() : created + getType(value->type).expiration;
else if (not permanent)
expiration = std::min(expiration, created + getType(value->type).expiration);
if (expiration < now)
return false;
auto st = store.find(id);
if (st == store.end()) {
if (store.size() >= max_store_keys)
return false;
auto st_i = store.emplace(id, now);
st = st_i.first;
if (maintain_storage and st_i.second)
scheduler.add(st->second.maintenance_time, std::bind(&Dht::dataPersistence, this, id));
}
StorageBucket* store_bucket {nullptr};
bool is_local = !sa;
if (sa)
store_bucket = &store_quota.try_emplace(sa, sa).first->second;
else
store_bucket = local_store_quota.get();
// Reject new local values if local storage limit is exceeded
if (is_local && max_local_store_size != STORAGE_LIMIT_UNLIMITED
&& local_store_quota->size() + value->size() > max_local_store_size) {
if (logger_)
logger_->warn("[store {}] Rejecting local put: local storage limit exceeded ({}/{})",
id.to_view(),
local_store_quota->size(),
max_local_store_size);
return false;
}
auto store = st->second.store(id, value, created, expiration, store_bucket);
if (auto vs = store.first) {
total_store_size += store.second.size_diff;
total_values += store.second.values_diff;
scheduler.cancel(vs->expiration_job);
if (not permanent) {
vs->expiration_job = scheduler.add(expiration, std::bind(&Dht::expireStorage, this, id));
}
if (total_store_size - local_store_quota->size() > max_store_size) {
auto value = vs->data;
auto value_diff = store.second.values_diff;
auto value_edit = store.second.edited_values;
expireStore();
storageChanged(id, st->second, value, value_diff > 0 || value_edit > 0);
} else {
storageChanged(id, st->second, vs->data, store.second.values_diff > 0 || store.second.edited_values > 0);
}
}
return std::get<0>(store);
}
void
Dht::storageAddListener(const InfoHash& id, const Sp<Node>& node, size_t socket_id, Query&& query, int version)
{
const auto& now = scheduler.time();
auto st = store.find(id);
if (st == store.end()) {
if (store.size() >= max_store_keys)
return;
st = store.emplace(id, now).first;
}
auto& node_listeners = st->second.listeners[node];
auto l = node_listeners.find(socket_id);
if (l == node_listeners.end()) {
auto vals = st->second.get(query.where.getFilter());
if (not vals.empty()) {
network_engine.tellListener(node,
socket_id,
id,
WANT4 | WANT6,
makeToken(node->getAddr(), false),
dht4.buckets.findClosestNodes(id, now, TARGET_NODES),
dht6.buckets.findClosestNodes(id, now, TARGET_NODES),
std::move(vals),
query,
version);
}
node_listeners.emplace(socket_id, Listener {now, std::forward<Query>(query), version});
} else
l->second.refresh(now, std::forward<Query>(query));
}
void
Dht::expireStore(decltype(store)::iterator i)
{
const auto& id = i->first;
auto& st = i->second;
auto stats = st.expire(id, scheduler.time());
if (not stats.expired_values.empty()) {
storageRemoved(id, st, stats.expired_values, -stats.size_diff);
}
}
void
Dht::expireStorage(InfoHash h)
{
auto i = store.find(h);
if (i != store.end())
expireStore(i);
}
void
Dht::storageRemoved(const InfoHash& id, Storage& st, const std::vector<Sp<Value>>& values, size_t totalSize)
{
if (logger_)
logger_->debug("[store {}] Discarded {} values ({} bytes)", id.to_view(), values.size(), totalSize);
total_store_size -= totalSize;
total_values -= values.size();
if (not st.listeners.empty()) {
if (logger_)
logger_->debug("[store {}] {} remote listeners", id.to_view(), st.listeners.size());
std::vector<Value::Id> ids;
ids.reserve(values.size());
for (const auto& v : values)
ids.emplace_back(v->id);
for (const auto& node_listeners : st.listeners) {
for (const auto& l : node_listeners.second) {
if (logger_)
logger_->warn("[store {}] [node {}] Sending expired",
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);
}
}
}
for (const auto& local_listeners : st.local_listeners) {
local_listeners.second.get_cb(values, true);
}
}
void
Dht::expireStore()
{
// removing expired values
for (auto i = store.begin(); i != store.end();) {
expireStore(i);
if (i->second.empty() && i->second.listeners.empty() && i->second.local_listeners.empty()) {
if (logger_)
logger_->debug("[store {}] Discarding empty storage", i->first.to_view());
i = store.erase(i);
} else
++i;
}
// remove more values if remote storage limit is exceeded
while (total_store_size - local_store_quota->size() > max_store_size) {
// find IP using the most storage
if (store_quota.empty()) {
if (logger_)
logger_->warn("No space left: local data consumes all the quota!");
break;
}
auto largest = store_quota.begin();
for (auto it = std::next(largest); it != store_quota.end(); ++it) {
if (it->second.size() > largest->second.size())
largest = it;
}
if (largest != store_quota.end() && !largest->second.empty()) {
while (true) {
auto exp_value = largest->second.getOldest();
auto storage = store.find(exp_value.first);
if (storage != store.end()) {
if (logger_)
logger_->warn("Storage quota full: discarding value from {} at {} {:016x}",
largest->first.toString(),
exp_value.first.to_view(),
exp_value.second);
if (auto value = storage->second.remove(exp_value.first, exp_value.second)) {
storageRemoved(storage->first, storage->second, {value}, value->size());
break;
}
}
}
} else {
if (logger_)
logger_->warn("No space left: remote data consumes all the quota!");
break;
}
}
// remove unused quota entires
for (auto i = store_quota.begin(); i != store_quota.end();) {
if (i->second.empty())
i = store_quota.erase(i);
else
++i;
}
}
void
Dht::connectivityChanged(sa_family_t af)
{
const auto& now = scheduler.time();
scheduler.edit(nextNodesConfirmation, now);
auto& dht = this->dht(af);
dht.buckets.connectivityChanged(now);
dht.reported_addr.clear();
network_engine.connectivityChanged(af);
startBootstrap(); // will only happen if disconnected
}
void
Dht::rotateSecrets()
{
oldsecret = secret;
secret = std::uniform_int_distribution<uint64_t> {}(rd);
uniform_duration_distribution<> time_dist(std::chrono::minutes(15), std::chrono::minutes(45));
auto rotate_secrets_time = scheduler.time() + time_dist(rd);
scheduler.add(rotate_secrets_time, std::bind(&Dht::rotateSecrets, this));
}
Blob
Dht::makeToken(const SockAddr& addr, bool old) const
{
const void* ip;
size_t iplen;
in_port_t port;
auto family = addr.getFamily();
if (family == AF_INET) {
const auto& sin = addr.getIPv4();
ip = &sin.sin_addr;
iplen = 4;
port = sin.sin_port;
} else if (family == AF_INET6) {
const auto& sin6 = addr.getIPv6();
ip = &sin6.sin6_addr;
iplen = 16;
port = sin6.sin6_port;
} else {
return {};
}
const auto& c1 = old ? oldsecret : secret;
Blob data;
data.reserve(sizeof(secret) + sizeof(in_port_t) + iplen);
data.insert(data.end(), (uint8_t*) &c1, ((uint8_t*) &c1) + sizeof(c1));
data.insert(data.end(), (uint8_t*) ip, (uint8_t*) ip + iplen);
data.insert(data.end(), (uint8_t*) &port, ((uint8_t*) &port) + sizeof(in_port_t));
return crypto::hash(data, TOKEN_SIZE);
}
bool
Dht::tokenMatch(const Blob& token, const SockAddr& addr) const
{
if (not addr or token.size() != TOKEN_SIZE)
return false;
if (token == makeToken(addr, false))
return true;
if (token == makeToken(addr, true))
return true;
return false;
}
NodeStats
Dht::getNodesStats(sa_family_t af) const
{
NodeStats stats = dht(af).getNodesStats(scheduler.time(), myid);
stats.node_cache_size = network_engine.getNodeCacheSize(af);
return stats;
}
NodeStats
Dht::Kad::getNodesStats(time_point now, const InfoHash& myid) const
{
NodeStats stats {};
for (const auto& b : buckets) {
for (auto& n : b.nodes) {
if (n->isGood(now)) {
stats.good_nodes++;
if (n->isIncoming())
stats.incoming_nodes++;
} else if (not n->isExpired())
stats.dubious_nodes++;
}
if (b.cached)
stats.cached_nodes++;
}
stats.table_depth = buckets.depth(buckets.findBucket(myid));
stats.searches = searches.size();
return stats;
}
void
Dht::dumpBucket(const Bucket& b, std::ostream& out) const
{
const auto& now = scheduler.time();
using namespace std::chrono;
out << b.first << " count: " << b.nodes.size() << " updated: " << print_time_relative(now, b.time);
if (b.cached)
out << " (cached)";
out << std::endl;
for (auto& n : b.nodes) {
out << " Node " << n->toString();
const auto& t = n->getTime();
const auto& r = n->getReplyTime();
if (t != r)
out << " updated: " << print_time_relative(now, t) << ", replied: " << print_time_relative(now, r);
else
out << " updated: " << print_time_relative(now, t);
if (n->isExpired())
out << " [expired]";
else if (n->isGood(now))
out << " [good]";
out << std::endl;
}
}
void
Dht::dumpSearch(const Search& sr, std::ostream& out) const
{
const auto& now = scheduler.time();
const auto& listen_expire = getListenExpiration();
using namespace std::chrono;
fmt::print(out,
"Search IPv{} {} gets: {} last step: {}{}{}{}{}\n",
(sr.af == AF_INET6 ? '6' : '4'),
sr.id,
sr.callbacks.size(),
print_time_relative(now, sr.step_time),
(sr.done ? " [done]"sv : ""sv),
(sr.expired ? " [expired]"sv : ""sv),
(sr.isSynced(now) ? " [synced]"sv : " [not synced]"sv),
(sr.isListening(now, listen_expire) ? " [listening]"sv : ""sv));
// printing the queries
if (sr.callbacks.size() + sr.listeners.size() > 0)
fmt::print(out, "Queries:\n");
for (const auto& cb : sr.callbacks) {
out << *cb.second.query << std::endl;
}
for (const auto& l : sr.listeners) {
out << *l.second.query << std::endl;
}
for (const auto& a : sr.announce) {
bool announced = sr.isAnnounced(a.value->id);
out << "Announcement: " << *a.value << (announced ? " [announced]" : "") << std::endl;
}
fmt::print(out, " Common bits InfoHash Conn. Get Ops IP\n");
auto last_get = sr.getLastGetTime();
for (const auto& np : sr.nodes) {
auto& n = *np;
auto listen = (not sr.listeners.empty()
? (n.listenStatus.empty() ? " "
: fmt::format("[{}] ",
(n.isListening(now, listen_expire)
? 'l'
: (n.pending(n.listenStatus) ? 'f' : ' '))))
: "");
std::string announce;
if (not sr.announce.empty()) {
if (n.acked.empty()) {
announce = std::string(sr.announce.size() + 3, ' ');
} else {
announce = "[";
for (const auto& a : sr.announce) {
auto ack = n.acked.find(a.value->id);
if (ack == n.acked.end() or not ack->second.req) {
announce += ' ';
} else {
announce += ack->second.req->getStateChar();
}
}
announce += "] ";
}
}
fmt::print(out,
"{:3} {} {} [{}{}] {}{}{}\n",
InfoHash::commonBits(sr.id, n.node->id),
n.node->id,
(findNode(n.node->id, sr.af) ? '*' : ' '),
(n.pending(n.getStatus) ? (n.candidate ? 'c' : 'f') : ' '),
(n.isSynced(now) ? (n.last_get_reply > last_get ? 'u' : 's') : '-'),
listen,
announce,
n.node->getAddrStr());
}
}
void
Dht::dumpTables() const
{
std::ostringstream out;
out << "My id " << myid << std::endl;
out << "Buckets IPv4 :" << std::endl;
for (const auto& b : dht4.buckets)
dumpBucket(b, out);
out << "Buckets IPv6 :" << std::endl;
for (const auto& b : dht6.buckets)
dumpBucket(b, out);
auto dump_searches = [&](std::map<InfoHash, Sp<Search>> srs) {
for (auto& srp : srs)
dumpSearch(*srp.second, out);
};
dump_searches(dht4.searches);
dump_searches(dht6.searches);
out << std::endl;
out << getStorageLog() << std::endl;
if (logger_)
logger_->debug("{}", out.str());
}
std::string
Dht::getStorageLog() const
{
std::ostringstream out;
for (const auto& s : store) {
const auto& st = s.second;
out << "Storage " << s.first << " " << st.listeners.size() << " list., " << st.valueCount() << " values, "
<< printByteCount(st.totalSize()) << "\n";
}
out << std::endl << std::endl;
std::multimap<size_t, const SockAddr*> q_map;
for (const auto& ip : store_quota)
if (ip.second.size())
q_map.emplace(ip.second.size(), &ip.first);
for (auto ip = q_map.rbegin(); ip != q_map.rend(); ++ip)
out << "IP " << ip->second->toString() << " uses " << printByteCount(ip->first) << std::endl;
out << std::endl;
out << "Total " << store.size() << " storages, " << total_values << " values (";
if (total_store_size < 1024)
out << total_store_size << " bytes)";
else {
auto remote_store_size = total_store_size - local_store_quota->size();
out << (remote_store_size / 1024) << " / " << (max_store_size / 1024) << " KB remote, "
<< (local_store_quota->size() / 1024) << " / ";
if (max_local_store_size == STORAGE_LIMIT_UNLIMITED)
out << "unlimited";
else
out << (max_local_store_size / 1024) << " KB";
out << " local)";
}
out << std::endl;
return out.str();
}
std::string
Dht::getStorageLog(const InfoHash& h) const
{
auto s = store.find(h);
if (s == store.end())
return fmt::format("Storage {} empty\n", h);
return printStorageLog(*s);
}
std::string
Dht::printStorageLog(const decltype(store)::value_type& s) const
{
std::ostringstream out;
using namespace std::chrono;
const auto& st = s.second;
const auto& now = scheduler.time();
out << "Storage " << s.first << " " << st.listeners.size() << " list., " << st.valueCount() << " values, "
<< printByteCount(st.totalSize()) << std::endl;
for (const auto& v : st.getValues()) {
out << " Value " << v.data->id << " size: " << printByteCount(v.data->size())
<< " created: " << print_time_relative(now, v.created)
<< " expires: " << print_time_relative(now, v.expiration) << std::endl;
}
if (not st.local_listeners.empty())
out << " " << st.local_listeners.size() << " local listeners" << std::endl;
for (const auto& node_listeners : st.listeners) {
const auto& node = node_listeners.first;
out << " " << "Listener " << node->toString() << " : " << node_listeners.second.size() << " entries"
<< std::endl;
}
return out.str();
}
std::string
Dht::getRoutingTablesLog(sa_family_t af) const
{
std::ostringstream out;
for (const auto& b : buckets(af))
dumpBucket(b, out);
return out.str();
}
std::string
Dht::getSearchesLog(sa_family_t af) const
{
std::ostringstream out;
auto num_searches = dht4.searches.size() + dht6.searches.size();
if (num_searches > 8) {
if (not af or af == AF_INET)
for (const auto& sr : dht4.searches)
out << "[search " << sr.first << " IPv4]" << std::endl;
if (not af or af == AF_INET6)
for (const auto& sr : dht6.searches)
out << "[search " << sr.first << " IPv6]" << std::endl;
} else {
out << "s:synched, u:updated, a:announced, c:candidate, f:cur req, x:expired, *:known" << std::endl;
if (not af or af == AF_INET)
for (const auto& sr : dht4.searches)
dumpSearch(*sr.second, out);
if (not af or af == AF_INET6)
for (const auto& sr : dht6.searches)
dumpSearch(*sr.second, out);
}
out << "Total: " << num_searches << " searches (" << dht4.searches.size() << " IPv4, " << dht6.searches.size()
<< " IPv6)." << std::endl;
return out.str();
}
std::string
Dht::getSearchLog(const InfoHash& id, sa_family_t af) const
{
std::ostringstream out;
if (af == AF_UNSPEC) {
out << getSearchLog(id, AF_INET) << getSearchLog(id, AF_INET6);
} else {
auto& srs = searches(af);
auto sr = srs.find(id);
if (sr != srs.end())
dumpSearch(*sr->second, out);
}
return out.str();
}
Dht::~Dht()
{
for (auto& s : dht4.searches)
s.second->clear();
for (auto& s : dht6.searches)
s.second->clear();
}
net::NetworkConfig
fromDhtConfig(const Config& config)
{
net::NetworkConfig netConf;
netConf.network = config.network;
netConf.max_req_per_sec = config.max_req_per_sec ? config.max_req_per_sec : MAX_REQUESTS_PER_SEC;
netConf.max_peer_req_per_sec = config.max_peer_req_per_sec ? config.max_peer_req_per_sec
: netConf.max_req_per_sec / 8;
netConf.is_client = config.client_mode;
return netConf;
}
Dht::Dht(std::unique_ptr<net::DatagramSocket>&& sock,
const Config& config,
const Sp<Logger>& l,
std::unique_ptr<std::mt19937_64>&& rda)
: DhtInterface(l)
, rd(rda ? std::move(*rda) : crypto::getSeededRandomEngine<std::mt19937_64>())
, myid(config.node_id ? config.node_id : InfoHash::getRandom(rd))
, store()
, store_quota()
, local_store_quota(std::make_unique<StorageBucket>())
, max_store_keys(config.max_store_keys ? (int) config.max_store_keys : MAX_HASHES)
, max_store_size(config.max_store_size ? (size_t) config.max_store_size : STORAGE_LIMIT_DEFAULT)
, max_local_store_size(config.max_local_store_size ? (size_t) config.max_local_store_size : STORAGE_LIMIT_UNLIMITED)
, max_searches(config.max_searches ? (int) config.max_searches : MAX_SEARCHES)
, network_engine(myid,
fromDhtConfig(config),
std::move(sock),
logger_,
rd,
scheduler,
std::bind(&Dht::onError, this, _1, _2),
std::bind(&Dht::onNewNode, this, _1, _2),
std::bind(&Dht::onReportedAddr, this, _1, _2),
std::bind(&Dht::onPing, this, _1),
std::bind(&Dht::onFindNode, this, _1, _2, _3),
std::bind(&Dht::onGetValues, this, _1, _2, _3, _4),
std::bind(&Dht::onListen, this, _1, _2, _3, _4, _5, _6),
std::bind(&Dht::onAnnounce, this, _1, _2, _3, _4, _5),
std::bind(&Dht::onRefresh, this, _1, _2, _3, _4))
, persistPath(config.persist_path)
, is_bootstrap(config.is_bootstrap)
, maintain_storage(config.maintain_storage)
, public_stable(config.public_stable)
{
scheduler.syncTime();
auto s = network_engine.getSocket();
if (not s or (not s->hasIPv4() and not s->hasIPv6()))
throw DhtException("Opened socket required");
if (s->hasIPv4()) {
dht4.buckets = {Bucket {AF_INET}};
dht4.buckets.is_client = config.is_bootstrap;
}
if (s->hasIPv6()) {
dht6.buckets = {Bucket {AF_INET6}};
dht6.buckets.is_client = config.is_bootstrap;
}
search_id = std::uniform_int_distribution<decltype(search_id)> {}(rd);
uniform_duration_distribution<> time_dis {std::chrono::seconds(3), std::chrono::seconds(5)};
nextNodesConfirmation = scheduler.add(scheduler.time() + time_dis(rd), std::bind(&Dht::confirmNodes, this));
// Fill old secret
secret = std::uniform_int_distribution<uint64_t> {}(rd);
rotateSecrets();
if (not persistPath.empty())
loadState(persistPath);
expire();
if (logger_)
logger_->debug("DHT node initialised with ID {:s}", myid);
}
bool
Dht::neighbourhoodMaintenance(RoutingTable& list)
{
// logger__DBG("neighbourhoodMaintenance");
auto b = list.findBucket(myid);
if (b == list.end())
return false;
InfoHash id = myid;
#ifdef _WIN32
std::uniform_int_distribution<int> rand_byte {0, std::numeric_limits<uint8_t>::max()};
#else
std::uniform_int_distribution<uint8_t> rand_byte;
#endif
id[HASH_LEN - 1] = rand_byte(rd);
std::bernoulli_distribution rand_trial(1. / 8.);
auto q = b;
if (std::next(q) != list.end() && (q->nodes.empty() || rand_trial(rd)))
q = std::next(q);
if (b != list.begin() && (q->nodes.empty() || rand_trial(rd))) {
auto r = std::prev(b);
if (!r->nodes.empty())
q = r;
}
auto n = q->randomNode(rd);
if (n) {
if (logger_)
logger_->debug("[node {}] Sending [find {}] for neighborhood maintenance", n->toString(), id.to_view());
/* Since our node-id is the same in both DHTs, it's probably
profitable to query both families. */
network_engine.sendFindNode(n, id, network_engine.want());
}
return true;
}
bool
Dht::bucketMaintenance(RoutingTable& list)
{
std::bernoulli_distribution rand_trial(1. / 8.);
std::bernoulli_distribution rand_trial_38(1. / 38.);
bool sent {false};
for (auto b = list.begin(); b != list.end(); ++b) {
if (b->time < scheduler.time() - std::chrono::minutes(10) || b->nodes.empty()) {
/* This bucket hasn't seen any positive confirmation for a long
time. Pick a random id in this bucket's range, and send a request
to a random node. */
InfoHash id = list.randomId(b, rd);
auto q = b;
/* If the bucket is empty, we try to fill it from a neighbour.
We also sometimes do it gratuitiously to recover from
buckets full of broken nodes. */
if (std::next(b) != list.end() && (q->nodes.empty() || rand_trial(rd)))
q = std::next(b);
if (b != list.begin() && (q->nodes.empty() || rand_trial(rd))) {
auto r = std::prev(b);
if (!r->nodes.empty())
q = r;
}
auto n = q->randomNode(rd);
if (n and not n->isPendingMessage()) {
want_t want = -1;
if (network_engine.want() != want) {
auto otherbucket = findBucket(id, q->af == AF_INET ? AF_INET6 : AF_INET);
if (otherbucket && otherbucket->nodes.size() < TARGET_NODES)
/* The corresponding bucket in the other family
is emptyish -- querying both is useful. */
want = WANT4 | WANT6;
else if (rand_trial_38(rd))
/* Most of the time, this just adds overhead.
However, it might help stitch back one of
the DHTs after a network collapse, so query
both, but only very occasionally. */
want = WANT4 | WANT6;
}
if (logger_)
logger_->debug("[node {}] Sending find {} for bucket maintenance", n->toString(), id.to_view());
// auto start = scheduler.time();
network_engine.sendFindNode(n, id, want, nullptr, [this, n](const net::Request&, bool over) {
if (over) {
const auto& end = scheduler.time();
// using namespace std::chrono;
// if (logger_)
// logger_->d(n->id, "[node %s] Bucket maintenance op expired after %s",
// n->toString().c_str(), print_duration(end-start).c_str());
scheduler.edit(nextNodesConfirmation, end + Node::MAX_RESPONSE_TIME);
}
});
sent = true;
}
}
}
return sent;
}
void
Dht::dataPersistence(InfoHash id)
{
const auto& now = scheduler.time();
auto str = store.find(id);
if (str != store.end() and now > str->second.maintenance_time) {
if (logger_)
logger_->debug("[storage {}] Maintenance ({} values, {} bytes)",
id.to_view(),
str->second.valueCount(),
str->second.totalSize());
maintainStorage(*str);
str->second.maintenance_time = now + MAX_STORAGE_MAINTENANCE_EXPIRE_TIME;
scheduler.add(str->second.maintenance_time, std::bind(&Dht::dataPersistence, this, id));
}
}
size_t
Dht::maintainStorage(decltype(store)::value_type& storage, bool force, const DoneCallback& donecb)
{
const auto& now = scheduler.time();
size_t announce_per_af = 0;
auto maintain = [&](sa_family_t af) {
bool want = true;
auto nodes = buckets(af).findClosestNodes(storage.first, now);
if (!nodes.empty()) {
if (force || storage.first.xorCmp(nodes.back()->id, myid) < 0) {
for (auto& value : storage.second.getValues()) {
const auto& vt = getType(value.data->type);
if (force || value.created + vt.expiration > now + MAX_STORAGE_MAINTENANCE_EXPIRE_TIME) {
// gotta put that value there
announce(storage.first, af, value.data, donecb, value.created);
++announce_per_af;
}
}
want = false;
}
}
return want;
};
bool want4 = maintain(AF_INET), want6 = maintain(AF_INET6);
if (not want4 and not want6) {
if (logger_)
logger_->debug("Discarding storage values {}", storage.first.to_view());
auto diff = storage.second.clear(storage.first);
total_store_size += diff.size_diff;
total_values += diff.values_diff;
}
return announce_per_af;
}
time_point
Dht::periodic(const uint8_t* buf, size_t buflen, SockAddr from, const time_point& now)
{
scheduler.syncTime(now);
if (buflen) {
try {
network_engine.processMessage(buf, buflen, std::move(from));
} catch (const std::exception& e) {
if (logger_)
logger_->warn("Unable to process message: {}", e.what());
}
}
return scheduler.run();
}
void
Dht::expire()
{
uniform_duration_distribution<> time_dis(std::chrono::minutes(2), std::chrono::minutes(6));
auto expire_stuff_time = scheduler.time() + duration(time_dis(rd));
expireBuckets(dht4.buckets);
expireBuckets(dht6.buckets);
expireStore();
expireSearches();
scheduler.add(expire_stuff_time, std::bind(&Dht::expire, this));
}
void
Dht::onConnected()
{
stopBootstrap();
auto restartSearches = [this](std::map<InfoHash, Sp<Search>>& searches) {
const auto& now = scheduler.time();
for (auto& srp : searches) {
auto& sr = *srp.second;
if (sr.done)
continue;
scheduler.edit(sr.nextSearchStep, now);
}
};
restartSearches(dht4.searches);
restartSearches(dht6.searches);
auto callbacks = std::move(onConnectCallbacks_);
while (not callbacks.empty()) {
callbacks.front()();
callbacks.pop();
}
}
void
Dht::onDisconnected()
{
if (not bootstrapJob)
bootstrap();
}
void
Dht::bootstrap()
{
if (dht4.status != NodeStatus::Disconnected || dht6.status != NodeStatus::Disconnected) {
// The job that led here has already run, and the scheduler does not
// clear the handle to a job it has executed. onDisconnected() reads a
// set handle as "a bootstrap is already scheduled" and does nothing,
// so leaving it set here means the node never tries to rejoin the
// network once it has lost it. Clearing it is what says that no
// bootstrap is pending any more.
bootstrapJob.reset();
return;
}
if (logger_)
logger_->debug("[{}] Bootstraping", myid.to_view());
bootstrap_pending = false;
for (const auto& boootstrap : bootstrap_nodes) {
try {
auto ips = network_engine.getSocket()->resolve(boootstrap.first, boootstrap.second);
for (auto& ip : ips) {
if (ip.getPort() == 0)
ip.setPort(net::DHT_DEFAULT_PORT);
pingNode(ip);
}
} catch (const std::exception& e) {
if (logger_)
logger_->error("[{}] Unable to resolve {}:{}: {}",
myid.to_view(),
boootstrap.first,
boootstrap.second,
e.what());
}
}
scheduler.cancel(bootstrapJob);
bootstrapJob = scheduler.add(scheduler.time() + bootstrap_period, std::bind(&Dht::bootstrap, this));
bootstrap_period = std::min(bootstrap_period * 2, BOOTSTRAP_PERIOD_MAX);
}
void
Dht::startBootstrap()
{
stopBootstrap();
bootstrap_pending = !bootstrap_nodes.empty();
bootstrapJob = scheduler.add(scheduler.time(), std::bind(&Dht::bootstrap, this));
}
void
Dht::stopBootstrap()
{
scheduler.cancel(bootstrapJob);
bootstrap_pending = false;
bootstrap_period = BOOTSTRAP_PERIOD;
}
void
Dht::confirmNodes()
{
using namespace std::chrono;
bool soon = false;
const auto& now = scheduler.time();
if (dht4.searches.empty() and dht4.status == NodeStatus::Connected) {
if (logger_)
logger_->debug("[confirm nodes] Initial IPv4 'get' for my id ({})", myid.to_view());
search(myid, AF_INET);
}
if (dht6.searches.empty() and dht6.status == NodeStatus::Connected) {
if (logger_)
logger_->debug("[confirm nodes] Initial IPv6 'get' for my id ({})", myid.to_view());
search(myid, AF_INET6);
}
soon |= bucketMaintenance(dht4.buckets);
soon |= bucketMaintenance(dht6.buckets);
if (!soon) {
if (dht4.buckets.grow_time >= now - seconds(150))
soon |= neighbourhoodMaintenance(dht4.buckets);
if (dht6.buckets.grow_time >= now - seconds(150))
soon |= neighbourhoodMaintenance(dht6.buckets);
}
/* In order to maintain all buckets' age within 600 seconds, worst
case is roughly 27 seconds, assuming the table is 22 bits deep.
We want to keep a margin for neighborhood maintenance, so keep
this within 25 seconds. */
auto time_dis = soon ? uniform_duration_distribution<> {seconds(5), seconds(25)}
: uniform_duration_distribution<> {seconds(60), seconds(180)};
auto confirm_nodes_time = now + time_dis(rd);
scheduler.edit(nextNodesConfirmation, confirm_nodes_time);
}
std::vector<ValuesExport>
Dht::exportValues() const
{
const auto& now = scheduler.time();
const auto system_now = system_clock::now();
std::vector<ValuesExport> e {};
e.reserve(store.size());
for (const auto& h : store) {
ValuesExport ve;
ve.first = h.first;
msgpack::sbuffer buffer;
msgpack::packer<msgpack::sbuffer> pk(&buffer);
const auto& vals = h.second.getValues();
pk.pack_array(vals.size());
for (const auto& v : vals) {
pk.pack_array(4);
pk.pack(to_system_time(v.created, now, system_now));
v.data->msgpack_pack(pk);
if (const auto& store_addr = v.store_bucket ? v.store_bucket->getAddr() : SockAddr {}) {
pk.pack_bin(store_addr.getLength());
pk.pack_bin_body(reinterpret_cast<const char*>(store_addr.get()), store_addr.getLength());
} else {
pk.pack_bin(0);
}
pk.pack(to_system_time(v.expiration, now, system_now));
}
ve.second = {buffer.data(), buffer.data() + buffer.size()};
e.push_back(std::move(ve));
}
return e;
}
void
Dht::importValues(const std::vector<ValuesExport>& import)
{
const auto& now = scheduler.time();
const auto system_now = system_clock::now();
unsigned imported = 0, ignored = 0;
size_t imported_size = 0;
for (const auto& value : import) {
if (value.second.empty()) {
ignored++;
continue;
}
try {
msgpack::unpacked msg;
msgpack::unpack(msg, (const char*) value.second.data(), value.second.size());
auto valarr = msg.get();
if (valarr.type != msgpack::type::ARRAY)
throw msgpack::type_error();
for (unsigned i = 0; i < valarr.via.array.size; i++) {
auto& valel = valarr.via.array.ptr[i];
if (valel.type != msgpack::type::ARRAY or valel.via.array.size < 2)
throw msgpack::type_error();
time_point val_time;
time_point expiration = time_point::min();
Value tmp_val;
SockAddr store_addr;
try {
const bool uses_system_time = valel.via.array.ptr[0].type == msgpack::type::EXT;
if (uses_system_time) {
val_time = from_system_time(valel.via.array.ptr[0].as<system_clock::time_point>(),
now,
system_now);
} else {
val_time = time_point {
time_point::duration {valel.via.array.ptr[0].as<time_point::duration::rep>()}};
}
tmp_val.msgpack_unpack(valel.via.array.ptr[1]);
if (valel.via.array.size >= 3) {
auto addr_blob = valel.via.array.ptr[2].as<Blob>();
if (not addr_blob.empty()) {
store_addr = SockAddr(reinterpret_cast<const sockaddr*>(addr_blob.data()),
static_cast<socklen_t>(addr_blob.size()));
}
}
if (valel.via.array.size >= 4) {
if (uses_system_time) {
expiration = from_system_time(valel.via.array.ptr[3].as<system_clock::time_point>(),
now,
system_now);
} else {
expiration = time_point {
time_point::duration {valel.via.array.ptr[3].as<time_point::duration::rep>()}};
}
}
} catch (const std::exception&) {
if (logger_)
logger_->error("Error reading value at {}", value.first.to_view());
ignored++;
continue;
}
val_time = std::min(val_time, now);
auto val_size = tmp_val.size();
if (storageStore(value.first,
std::make_shared<Value>(std::move(tmp_val)),
val_time,
store_addr,
false,
expiration)) {
imported++;
imported_size += val_size;
} else {
ignored++;
}
}
} catch (const std::exception&) {
ignored++;
if (logger_)
logger_->error("Error reading values at {}", value.first.to_view());
continue;
}
}
if (logger_)
logger_->debug("Imported {} values, {}, ignored {}", imported, dht::printByteCount(imported_size), ignored);
}
std::vector<NodeExport>
Dht::exportNodes() const
{
const auto& now = scheduler.time();
std::vector<NodeExport> nodes;
const auto b4 = dht4.buckets.findBucket(myid);
if (b4 != dht4.buckets.end()) {
for (auto& n : b4->nodes)
if (n->isGood(now))
nodes.emplace_back(n->exportNode());
}
const auto b6 = dht6.buckets.findBucket(myid);
if (b6 != dht6.buckets.end()) {
for (auto& n : b6->nodes)
if (n->isGood(now))
nodes.emplace_back(n->exportNode());
}
for (auto b = dht4.buckets.begin(); b != dht4.buckets.end(); ++b) {
if (b == b4)
continue;
for (auto& n : b->nodes)
if (n->isGood(now))
nodes.emplace_back(n->exportNode());
}
for (auto b = dht6.buckets.begin(); b != dht6.buckets.end(); ++b) {
if (b == b6)
continue;
for (auto& n : b->nodes)
if (n->isGood(now))
nodes.emplace_back(n->exportNode());
}
return nodes;
}
void
Dht::insertNode(const InfoHash& id, const SockAddr& addr)
{
if (addr.getFamily() != AF_INET && addr.getFamily() != AF_INET6)
return;
scheduler.syncTime();
network_engine.insertNode(id, addr);
}
void
Dht::pingNode(SockAddr sa, DoneCallbackSimple&& cb)
{
scheduler.syncTime();
if (logger_)
logger_->debug("Sending ping to {}", sa);
auto& count = dht(sa.getFamily()).pending_pings;
count++;
network_engine.sendPing(
std::move(sa),
[&count, cb](const net::Request&, net::RequestAnswer&&) {
count--;
if (cb)
cb(true);
},
[&count, cb](const net::Request&, bool last) {
if (last) {
count--;
if (cb)
cb(false);
}
});
}
void
Dht::onError(Sp<net::Request> req, net::DhtProtocolException e)
{
const auto& node = req->node;
if (e.getCode() == net::DhtProtocolException::UNAUTHORIZED) {
if (logger_)
logger_->error("[node {}] Token flush", node->toString());
node->authError();
for (auto& srp : searches(node->getFamily())) {
auto& sr = srp.second;
for (auto& n : sr->nodes) {
if (n->node != node)
continue;
n->token.clear();
n->last_get_reply = time_point::min();
for (auto& listen : n->listenStatus) {
if (listen.second.req and listen.second.req->pending())
n->node->cancelRequest(listen.second.req);
listen.second.req = {};
n->node->closeSocket(listen.second.socketId);
if (listen.second.refresh) {
listen.second.refresh->cancel();
listen.second.refresh = {};
}
listen.second.cache.onSynced(false);
}
searchSendGetValues(sr);
scheduler.edit(sr->nextSearchStep, scheduler.time());
break;
}
}
} else if (e.getCode() == net::DhtProtocolException::NOT_FOUND) {
if (logger_)
logger_->error("[node {}] Returned error 404: storage not found", node->toString());
node->cancelRequest(req);
}
}
void
Dht::onReportedAddr(const InfoHash& /*id*/, const SockAddr& addr)
{
if (addr)
reportedAddr(addr);
}
net::RequestAnswer
Dht::onPing(Sp<Node>)
{
return {};
}
net::RequestAnswer
Dht::onFindNode(Sp<Node> node, const InfoHash& target, want_t want)
{
const auto& now = scheduler.time();
net::RequestAnswer answer;
answer.ntoken = makeToken(node->getAddr(), false);
if (want & WANT4)
answer.nodes4 = dht4.buckets.findClosestNodes(target, now, TARGET_NODES);
if (want & WANT6)
answer.nodes6 = dht6.buckets.findClosestNodes(target, now, TARGET_NODES);
return answer;
}
net::RequestAnswer
Dht::onGetValues(Sp<Node> node, const InfoHash& hash, want_t, const Query& query)
{
if (not hash) {
if (logger_)
logger_->warn("[node {}] Eek! Got get_values with no info_hash", node->toString());
throw net::DhtProtocolException {net::DhtProtocolException::NON_AUTHORITATIVE_INFORMATION,
net::DhtProtocolException::GET_NO_INFOHASH};
}
const auto& now = scheduler.time();
net::RequestAnswer answer {};
auto st = store.find(hash);
answer.ntoken = makeToken(node->getAddr(), false);
answer.nodes4 = dht4.buckets.findClosestNodes(hash, now, TARGET_NODES);
answer.nodes6 = dht6.buckets.findClosestNodes(hash, now, TARGET_NODES);
if (st != store.end() && not st->second.empty()) {
answer.values = st->second.get(query.where.getFilter());
if (logger_)
logger_->debug("[node {}] Sending {} values", node->toString(), answer.values.size());
}
return answer;
}
void
Dht::onGetValuesDone(const Sp<Node>& node, net::RequestAnswer& a, Sp<Search>& sr, const Sp<Query>& orig_query)
{
if (not sr) {
if (logger_)
logger_->warn("[search unknown] Got reply to 'get'. Ignoring.");
return;
}
/* if (logger_)
logger_->d(sr->id, "[search %s] [node %s] Got reply to 'get' with %u nodes",
sr->id.toString().c_str(), node->toString().c_str(), a.nodes4.size()+a.nodes6.size());*/
if (not a.ntoken.empty()) {
if (not a.values.empty() or not a.fields.empty()) {
if (logger_)
logger_->debug("[search {}] [node {}] Found {} values",
sr->id.to_view(),
node->toString(),
a.values.size());
for (auto& getp : sr->callbacks) { /* call all callbacks for this search */
auto& get = getp.second;
if (not(get.get_cb or get.query_cb)
or (orig_query and get.query and not get.isSatisfiedBy(*orig_query)))
continue;
if (get.query_cb) { /* in case of a request with query */
if (not a.fields.empty()) {
get.query_cb(a.fields);
} else if (not a.values.empty()) {
std::vector<Sp<FieldValueIndex>> fields;
fields.reserve(a.values.size());
for (const auto& v : a.values)
fields.emplace_back(
std::make_shared<FieldValueIndex>(*v, orig_query ? orig_query->select : Select {}));
get.query_cb(fields);
}
} else if (get.get_cb) { /* in case of a vanilla get request */
std::vector<Sp<Value>> tmp;
for (const auto& v : a.values)
if (not get.filter or get.filter(*v))
tmp.emplace_back(v);
if (not tmp.empty())
get.get_cb(tmp);
}
}
/* callbacks for local search listeners */
/*std::vector<std::pair<ValueCallback, std::vector<Sp<Value>>>> tmp_lists;
for (auto& l : sr->listeners) {
if (!l.second.get_cb or (orig_query and l.second.query and not
l.second.query->isSatisfiedBy(*orig_query))) continue; std::vector<Sp<Value>> tmp; for (const auto& v :
a.values) if (not l.second.filter or l.second.filter(*v)) tmp.emplace_back(v); if (not tmp.empty())
tmp_lists.emplace_back(l.second.get_cb, std::move(tmp));
}
for (auto& l : tmp_lists)
l.first(l.second, false);*/
} else if (not a.expired_values.empty()) {
if (logger_)
logger_->warn("[search {}] [node {}] {} expired values",
sr->id.toString(),
node->toString(),
a.expired_values.size());
}
} else {
if (logger_)
logger_->warn("[node {}] No token provided. Ignoring response content.", node->toString());
network_engine.blacklistNode(node);
}
if (not sr->done) {
searchSendGetValues(sr);
// Force to recompute the next step time
scheduler.edit(sr->nextSearchStep, scheduler.time());
}
}
net::RequestAnswer
Dht::onListen(Sp<Node> node, const InfoHash& hash, const Blob& token, size_t socket_id, const Query& query, int version)
{
if (not hash) {
if (logger_)
logger_->warn("[node {}] Listen with no info_hash", node->toString());
throw net::DhtProtocolException {net::DhtProtocolException::NON_AUTHORITATIVE_INFORMATION,
net::DhtProtocolException::LISTEN_NO_INFOHASH};
}
if (not tokenMatch(token, node->getAddr())) {
if (logger_)
logger_->warn("[node {}] Incorrect token {} for 'listen'", node->toString(), hash.toString());
throw net::DhtProtocolException {net::DhtProtocolException::UNAUTHORIZED,
net::DhtProtocolException::LISTEN_WRONG_TOKEN};
}
Query q = query;
storageAddListener(hash, node, socket_id, std::move(q), version);
net::RequestAnswer answer {};
answer.ntoken = makeToken(node->getAddr(), false);
return answer;
}
void
Dht::onListenDone(const Sp<Node>& node, net::RequestAnswer& answer, Sp<Search>& sr)
{
// if (logger_)
// logger_->d(sr->id, node->id, "[search %s] [node %s] Got listen confirmation",
// sr->id.toString().c_str(), node->toString().c_str(), answer.values.size());
if (not sr->done) {
const auto& now = scheduler.time();
if (not answer.ntoken.empty())
if (auto sn = sr->getNode(node))
sn->refreshToken(answer.ntoken);
searchSendGetValues(sr);
scheduler.edit(sr->nextSearchStep, now);
}
}
net::RequestAnswer
Dht::onAnnounce(Sp<Node> n,
const InfoHash& hash,
const Blob& token,
const std::vector<Sp<Value>>& values,
const time_point& creation_date)
{
auto& node = *n;
if (not hash) {
if (logger_)
logger_->warn("Put with no info_hash");
throw net::DhtProtocolException {net::DhtProtocolException::NON_AUTHORITATIVE_INFORMATION,
net::DhtProtocolException::PUT_NO_INFOHASH};
}
if (!tokenMatch(token, node.getAddr())) {
if (logger_)
logger_->warn("[node {}] Incorrect token {} for 'put'", node.toString(), hash.toString());
throw net::DhtProtocolException {net::DhtProtocolException::UNAUTHORIZED,
net::DhtProtocolException::PUT_WRONG_TOKEN};
}
{
// We store a value only if we think we're part of the
// SEARCH_NODES nodes around the target id.
auto closest_nodes = buckets(node.getFamily()).findClosestNodes(hash, scheduler.time(), SEARCH_NODES);
if (closest_nodes.size() >= TARGET_NODES and hash.xorCmp(closest_nodes.back()->id, myid) < 0) {
if (logger_)
logger_->warn("[node {}] Announce too far from the target. Dropping value.", node.toString());
return {};
}
}
auto created = std::min(creation_date, scheduler.time());
for (const auto& v : values) {
if (v->id == Value::INVALID_ID) {
if (logger_)
logger_->warn("[value {}] Incorrect value id", hash.to_view());
throw net::DhtProtocolException {net::DhtProtocolException::NON_AUTHORITATIVE_INFORMATION,
net::DhtProtocolException::PUT_INVALID_ID};
}
auto lv = getLocalById(hash, v->id);
Sp<Value> vc = v;
if (lv) {
if (*lv == *vc) {
storageRefresh(hash, v->id);
if (logger_)
logger_->debug("[store {}] [node {}] Refreshed value {:016x}",
hash.toString(),
node.toString(),
v->id);
} else {
const auto& type = getType(lv->type);
if (type.editPolicy(hash, lv, vc, node.id, node.getAddr())) {
if (logger_)
logger_->debug("[store {}] Editing {}", hash.toString(), vc->toString());
storageStore(hash, vc, created, node.getAddr());
} else {
if (logger_)
logger_->debug("[store {}] Rejecting edition of {} because of storage policy",
hash.toString(),
vc->toString());
}
}
} else {
// Allow the value to be edited by the storage policy
const auto& type = getType(vc->type);
if (type.storePolicy(hash, vc, node.id, node.getAddr())) {
// if (logger_)
// logger_->d(hash, node.id, "[store %s] Storing %s", hash.toString().c_str(),
// std::to_string(vc->id).c_str());
storageStore(hash, vc, created, node.getAddr());
} else {
if (logger_)
logger_->debug("[store {}] Rejecting storage of {}", hash.toString(), vc->toString());
}
}
}
return {};
}
net::RequestAnswer
Dht::onRefresh(Sp<Node> node, const InfoHash& hash, const Blob& token, const Value::Id& vid)
{
using namespace net;
if (not tokenMatch(token, node->getAddr())) {
if (logger_)
logger_->warn("[node {}] Incorrect token {} for 'put'", node->toString(), hash.toString());
throw DhtProtocolException {DhtProtocolException::UNAUTHORIZED, DhtProtocolException::PUT_WRONG_TOKEN};
}
if (storageRefresh(hash, vid)) {
if (logger_)
logger_->debug("[store {}] [node {}] Refreshed value {:016x}", hash.toString(), node->toString(), vid);
} else {
if (logger_)
logger_->debug("[store {}] [node {}] Got refresh for unknown value", hash.toString(), node->toString());
throw DhtProtocolException {DhtProtocolException::NOT_FOUND, DhtProtocolException::STORAGE_NOT_FOUND};
}
return {};
}
bool
Dht::storageRefresh(const InfoHash& id, Value::Id vid)
{
const auto& now = scheduler.time();
auto s = store.find(id);
if (s != store.end()) {
// Values like for a permanent put can be refreshed. So, inform remote listeners that the value
// need to be refreshed
auto& st = s->second;
if (not st.listeners.empty()) {
if (logger_)
logger_->debug("[store {}] {} remote listeners", id.to_view(), st.listeners.size());
std::vector<Value::Id> ids = {vid};
for (const auto& node_listeners : st.listeners) {
for (const auto& l : node_listeners.second) {
if (logger_)
logger_->warn("[store {}] [node {}] Sending refresh",
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);
}
}
}
auto expiration = s->second.refresh(id, now, vid, types);
if (expiration.first) {
scheduler.cancel(expiration.first->expiration_job);
if (expiration.second != time_point::max()) {
expiration.first->expiration_job = scheduler.add(expiration.second,
std::bind(&Dht::expireStorage, this, id));
}
}
return true;
}
return false;
}
void
Dht::onAnnounceDone(const Sp<Node>& node, net::RequestAnswer& answer, Sp<Search>& sr)
{
if (logger_)
logger_->debug("[search {}] [node {}] Got reply to put!", sr->id.toString(), node->toString());
searchSendGetValues(sr);
sr->checkAnnounced(answer.vid);
}
struct DhtState
{
unsigned v {1};
InfoHash id;
std::vector<NodeExport> nodes;
std::vector<ValuesExport> values;
MSGPACK_DEFINE_MAP(v, id, nodes, values)
};
void
Dht::saveState(const std::string& path) const
{
DhtState state;
state.id = myid;
state.nodes = exportNodes();
state.values = exportValues();
std::ofstream file(path);
msgpack::pack(file, state);
}
void
Dht::loadState(const std::string& path)
{
if (logger_)
logger_->debug("Importing state from {}", path);
try {
// Import nodes from binary file
msgpack::unpacker pac;
{
// Read whole file
std::ifstream file(path, std::ios::binary | std::ios::ate);
if (!file.is_open()) {
return;
}
auto size = file.tellg();
file.seekg(0, std::ios::beg);
pac.reserve_buffer(size);
file.read(pac.buffer(), size);
pac.buffer_consumed(size);
}
// Import nodes
msgpack::object_handle oh;
if (pac.next(oh)) {
auto state = oh.get().as<DhtState>();
if (logger_)
logger_->debug("Importing {} nodes", state.nodes.size());
if (state.id)
myid = state.id;
std::vector<Sp<Node>> tmpNodes;
tmpNodes.reserve(state.nodes.size());
for (const auto& node : state.nodes)
tmpNodes.emplace_back(network_engine.insertNode(node.id, node.addr));
importValues(state.values);
}
} catch (const std::exception& e) {
if (logger_)
logger_->warn("Error importing state from {}: {}", path, e.what());
}
}
} // namespace dht