Files
i2pd/libi2pd_client/TorrentsDHT.cpp
T
2026-09-22 20:49:14 -04:00

466 lines
12 KiB
C++

/*
* Copyright (c) 2026, The PurpleI2P Project
*
* This file is part of Purple i2pd project and licensed under BSD3
*
* See full license text in LICENSE file at top of project tree
*/
#ifndef NO_TORRENTS
#include <string.h>
#include <vector>
#include "TorrentsTunnel.h"
#include "TorrentsDHT.h"
namespace i2p
{
namespace torrents
{
std::shared_ptr<Node> Bucket::FindNode (const NodeID& id) const
{
auto it = std::find_if (nodes.begin (), nodes.end (),
[&id](std::shared_ptr<const Node> node)
{
return node->id == id;
});
return (it != nodes.end ()) ? *it : nullptr;
}
std::optional<NodeID> Bucket::GetMiddleID () const
{
uint8_t bit = std::max (start.FindLowestBit (), next ? next->start.FindLowestBit () : -1) + 1;
if (bit >= NodeID::len * 8) return {};
NodeID middleID = start;
middleID[bit >> 3] |= (0x80 >> (bit & 0x07));
return { middleID };
}
bool Bucket::Split ()
{
auto middleID = GetMiddleID ();
if (!middleID) return false;
auto newBucket = new Bucket (*middleID);
newBucket->next = next;
next = newBucket;
// move some nodes
auto it = nodes.begin ();
while (it != nodes.end ())
{
if ((*it)->id < *middleID)
it++; // stay in old bucket
else
{
// move to new bucket
auto node = *it;
it = nodes.erase (it);
newBucket->nodes.push_back (node);
}
}
return true;
}
RoutingTable::RoutingTable (const NodeID& ourNode):
m_OurNode (ourNode)
{
m_Buckets = new Bucket;
}
RoutingTable::~RoutingTable ()
{
while (m_Buckets)
{
auto bucket = m_Buckets;
m_Buckets = m_Buckets->next;
delete bucket;
}
}
Bucket * RoutingTable::FindBucket (const Torrent::InfoHash& id) const
{
if (!m_Buckets) return nullptr;
auto bucket = m_Buckets;
while (bucket->next)
{
if (id < bucket->next->start)
return bucket;
bucket = bucket->next;
}
return bucket;
}
void RoutingTable::RemoveEmptyBuckers ()
{
if (m_Buckets)
{
auto prev = m_Buckets, bucket = m_Buckets->next;
while (bucket)
{
if (bucket->nodes.empty ())
{
prev->next = bucket->next;
auto tmp = bucket;
bucket = bucket->next;
delete tmp;
}
else
{
prev = bucket;
bucket = bucket->next;
}
}
}
}
std::shared_ptr<Node> RoutingTable::AddNode (const NodeID& id, i2p::data::IdentHash& peer, uint16_t port)
{
if (id == m_OurNode) return nullptr;
auto bucket = FindBucket (id);
if (!bucket) return nullptr;
std::shared_ptr<Node> node = bucket->FindNode (id);
if (node) return node;
if (bucket->IsFull ())
{
if (!bucket->IsInBucket (m_OurNode)) return nullptr;
do
{
if (!bucket->Split ()) return nullptr;
bucket = FindBucket (id);
}
while (bucket->IsFull ());
}
if (bucket)
{
node = std::make_shared<Node>(id, peer, port);
bucket->nodes.emplace_back (node);
}
RemoveEmptyBuckers ();
return node;
}
std::list<std::pair<std::shared_ptr<Node>, Distance> > RoutingTable::FindClosestNodes (const Torrent::InfoHash& infoHash, size_t num) const
{
std::list<std::pair<std::shared_ptr<Node>, Distance> > ret;
if (num > 0)
{
auto bucket = FindBucket (infoHash);
if (bucket)
{
for (auto it: bucket->nodes)
{
auto nodeDistance = it->id ^ infoHash;
auto it1 = std::find_if (ret.begin (), ret.end (),
[&nodeDistance](const std::pair<std::shared_ptr<Node>, Distance>& alreadyFound)
{
return nodeDistance < alreadyFound.second;
});
ret.insert (it1, { it, nodeDistance } );
}
if (ret.size () > num) ret.resize (num);
}
}
return ret;
}
TorrentsDHT::TorrentsDHT (TorrentsTunnel& tunnel, uint16_t port):
m_Tunnel (tunnel), m_Port (port)
{
auto dest = tunnel.GetLocalDestination ();
if (dest)
{
memcpy (m_NodeID.data (), dest->GetIdentHash (), m_NodeID.size ());
m_NodeID[4] ^= (port >> 8);
m_NodeID[5] ^= (port & 0xFF);
memcpy (m_NodeInfo.data (), m_NodeID.data (), m_NodeID.size ());
memcpy (m_NodeInfo.data () + m_NodeID.size (), dest->GetIdentHash (), i2p::data::IdentHash::len);
htobe16buf (m_NodeInfo.data () + m_NodeInfo.size () - 2, port);
m_RoutingTable = std::make_unique<RoutingTable> (m_NodeID);
}
else
{
m_NodeID.fill (0);
m_NodeInfo.fill (0);
}
}
void TorrentsDHT::Start ()
{
auto dest = m_Tunnel.GetLocalDestination ();
if (dest)
{
auto dgramDest = dest->GetDatagramDestination ();
if (dgramDest)
dgramDest->SetReceiver (std::bind_front (&TorrentsDHT::HandleDatagram, this));
}
}
void TorrentsDHT::Stop ()
{
auto dest = m_Tunnel.GetLocalDestination ();
if (dest)
{
auto dgramDest = dest->GetDatagramDestination ();
if (dgramDest)
dgramDest->ResetReceiver ();
}
}
void TorrentsDHT::HandleRawDatagram (const uint8_t * buf, size_t len)
{
// response or error
char type = 0;
std::string transactionID, id;
ParseDictionary (std::string_view ((const char *)buf, len),
[&type, &transactionID, &id](std::string_view key, std::string_view buf)->size_t
{
if (key == "y")
{
auto [value, l] = ExtractByteString (buf);
if (l && !value.empty ()) type = value[0];
return l;
}
else if (key == "t")
{
auto [value, l] = ExtractByteString (buf);
if (l) transactionID = value;
return l;
}
else if (key == "r")
{
return ParseDictionary (buf,
[&id](std::string_view key, std::string_view buf)->size_t
{
if (key == "id")
{
auto [value, l] = ExtractByteString (buf);
if (l) id = value;
return l;
}
return 0;
});
}
return 0;
});
if (type)
{
switch (type)
{
case 'r':
HandleResponse (transactionID, id);
break;
case 'e':
LogPrint (eLogDebug, "TorrentsDHT: Error msg received");
break;
case 'q':
LogPrint (eLogError, "TorrentsDHT: Query can't come as raw datagram");
break;
default:
LogPrint (eLogInfo, "TorrentsDHT: Unxpected msg type ", (int)type);
}
}
}
void TorrentsDHT::HandleDatagram (const i2p::data::IdentityEx& from, uint16_t fromPort, uint16_t toPort,
const uint8_t * buf, size_t len, const i2p::util::Mapping * options)
{
// query
char type = 0;
std::string transactionID, query, id, infoHash;
ParseDictionary (std::string_view ((const char *)buf, len),
[&type, &transactionID, &query, &id, &infoHash](std::string_view key, std::string_view buf)->size_t
{
if (key == "y")
{
auto [value, l] = ExtractByteString (buf);
if (l && !value.empty ()) type = value[0];
return l;
}
else if (key == "t")
{
auto [value, l] = ExtractByteString (buf);
if (l) transactionID = value;
return l;
}
else if (key == "q")
{
auto [value, l] = ExtractByteString (buf);
if (l) query = value;
return l;
}
else if (key == "a")
{
return ParseDictionary (buf,
[&id, &infoHash](std::string_view key, std::string_view buf)->size_t
{
if (key == "id")
{
auto [value, l] = ExtractByteString (buf);
if (l) id = value;
return l;
}
else if (key == "info_hash")
{
auto [value, l] = ExtractByteString (buf);
if (l) infoHash = value;
return l;
}
return 0;
});
}
return 0;
});
if (type == 'q')
{
if (query == "ping")
HandlePingQuery (from.GetIdentHash (), fromPort, transactionID, id);
else if (query == "get_peers")
HandleGetPeersQuery (from.GetIdentHash (), fromPort, transactionID, id, infoHash);
else
LogPrint (eLogDebug, "TorrentsDHT: Unexpected query ", query);
}
else if (type)
LogPrint (eLogInfo, "TorrentsDHT: Unxpected msg type ", (int)type);
}
void TorrentsDHT::HandlePingQuery (const i2p::data::IdentHash& fromIdent, uint16_t fromPort,
std::string_view transactionID, std::string_view id)
{
LogPrint (eLogDebug, "TorrentsDHT: Ping query msg received");
SendPingResponse (transactionID, fromIdent, fromPort + 1); // to rport
}
void TorrentsDHT::HandleGetPeersQuery (const i2p::data::IdentHash& fromIdent, uint16_t fromPort,
std::string_view transactionID, std::string_view id, std::string_view infoHash)
{
LogPrint (eLogDebug, "TorrentsDHT: Get peers query msg received");
Torrent::InfoHash hash;
if (infoHash.size () < hash.size ())
{
LogPrint (eLogInfo, "TorrentsDHT: Requested info hash is too short ", infoHash.size ());
return;
}
memcpy (hash.data (), infoHash.data (), hash.size ());
auto torrent = m_Tunnel.FindTorrent (hash);
if (torrent)
{
auto peers = torrent->GetAllPeers ();
if (!peers.empty ())
SendGetPeersResponse (transactionID, peers, fromIdent, fromPort + 1); // to rport
}
}
void TorrentsDHT::HandleResponse (std::string_view transactionID, std::string_view id)
{
LogPrint (eLogDebug, "TorrentsDHT: Response msg received");
auto it = m_Queries.find (transactionID);
if (it != m_Queries.end ())
{
// assume ping for now
if (id.size () < NodeID::len)
{
LogPrint (eLogInfo, "TorrentsDHT: received id is too short ", id.size ());
return;
}
if (m_RoutingTable)
{
NodeID nodeID;
memcpy (nodeID.data (), id.data (), NodeID::len);
if (m_RoutingTable->AddNode (nodeID, it->second.first, it->second.second))
LogPrint (eLogDebug, "TorrentsDHT: Node ", it->second.first.ToBase64 (), ":", it->second.second, " added");
else
LogPrint (eLogError, "TorrentsDHT: Failed to add node ", it->second.first.ToBase64 ());
}
}
else
LogPrint (eLogInfo, "TorrentsDHT: Query now found");
}
void TorrentsDHT::SendDatagram (std::string_view msg, const i2p::data::IdentHash& toIdent, uint16_t toPort)
{
auto dest = m_Tunnel.GetLocalDestination ();
if (dest)
{
auto dgramDest = dest->GetDatagramDestination ();
if (dgramDest)
dgramDest->SendDatagramTo ((const uint8_t *)msg.data (), msg.size (), toIdent, m_Port, toPort);
}
}
void TorrentsDHT::SendRawDatagram (std::string_view msg, const i2p::data::IdentHash& toIdent, uint16_t toPort)
{
auto dest = m_Tunnel.GetLocalDestination ();
if (dest)
{
auto dgramDest = dest->GetDatagramDestination ();
if (dgramDest)
dgramDest->SendRawDatagramTo ((const uint8_t *)msg.data (), msg.size (), toIdent, m_Port, toPort);
}
}
void TorrentsDHT::SendQueryMsg (std::string_view query, std::string_view arguments,
const i2p::data::IdentHash& toIdent, uint16_t toPort)
{
std::string transactionID;
if (m_Tunnel.GetLocalDestination ())
{
transactionID.push_back (m_Tunnel.GetLocalDestination ()->GetRng ()() % ('z' -'a' + 1) + 'a');
transactionID.push_back (m_Tunnel.GetLocalDestination ()->GetRng ()() % ('z' -'a' + 1) + 'a');
}
else
transactionID = "xx";
auto msg = CreateDictionary ({
{ "a", arguments },
{ "q", CreateByteString (query) },
{ "t", CreateByteString (transactionID) },
{ "y", CreateByteString ("q") }
});
m_Queries.insert_or_assign (transactionID, std::make_pair (toIdent, toPort));
SendDatagram (msg, toIdent, toPort);
}
void TorrentsDHT::SendResponseMsg (std::string_view response, std::string_view transactionID,
const i2p::data::IdentHash& toIdent, uint16_t toPort)
{
auto msg = CreateDictionary ({
{ "r", response },
{ "t", CreateByteString (transactionID) },
{ "y", CreateByteString ("r") }
});
SendRawDatagram (msg, toIdent, toPort);
}
void TorrentsDHT::SendPingQuery (const i2p::data::IdentHash& toIdent, uint16_t toPort)
{
SendQueryMsg ("ping", CreateDictionary ({
{ "id", CreateByteString (std::string_view ((const char *)m_NodeID.data (), m_NodeID.size ())) }
}),
toIdent, toPort);
}
void TorrentsDHT::SendPingResponse (std::string_view transactionID, const i2p::data::IdentHash& toIdent, uint16_t toPort)
{
SendResponseMsg (CreateDictionary ({
{ "id", CreateByteString (std::string_view ((const char *)m_NodeID.data (), m_NodeID.size ())) }
}),
transactionID, toIdent, toPort);
}
void TorrentsDHT::SendGetPeersResponse (std::string_view transactionID, const std::unordered_set<i2p::data::IdentHash>& peers,
const i2p::data::IdentHash& toIdent, uint16_t toPort)
{
std::vector<std::string> values;
for (auto& it: peers)
values.emplace_back (CreateByteString (std::string_view ((const char *)it.data (), i2p::data::IdentHash::len)));
SendResponseMsg (CreateDictionary ({
{ "id", CreateByteString (std::string_view ((const char *)m_NodeID.data (), m_NodeID.size ())) },
{ "token", CreateByteString ("12345") }, // TODO:
{ "values", CreateList (values) }
}),
transactionID, toIdent, toPort);
}
}
}
#endif