/* * 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 #include "TorrentsTunnel.h" #include "TorrentsDHT.h" namespace i2p { namespace torrents { std::shared_ptr Bucket::FindNode (const NodeID& id) const { auto it = std::find_if (nodes.begin (), nodes.end (), [&id](std::shared_ptr node) { return node->id == id; }); return (it != nodes.end ()) ? *it : nullptr; } std::optional 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 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 = 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(id, peer, port); bucket->nodes.emplace_back (node); } RemoveEmptyBuckers (); return node; } std::list, Distance> > RoutingTable::FindClosestNodes (const Torrent::InfoHash& infoHash, size_t num) const { std::list, 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, 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 (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; ParseDictionary (std::string_view ((const char *)buf, len), [&type, &transactionID, &query, &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 == "q") { auto [value, l] = ExtractByteString (buf); if (l) query = value; return l; } else if (key == "a") { 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 == 'q') HandleQuery (from.GetIdentHash (), fromPort, transactionID, query, id); else if (type) LogPrint (eLogInfo, "TorrentsDHT: Unxpected msg type ", (int)type); } void TorrentsDHT::HandleQuery (const i2p::data::IdentHash& fromIdent, uint16_t fromPort, std::string_view transactionID, std::string_view query, std::string_view id) { LogPrint (eLogDebug, "TorrentsDHT: Query msg received"); if (query == "ping") SendPingResponse (transactionID, fromIdent, fromPort + 1); // to rport else LogPrint (eLogDebug, "TorrentsDHT: Unexpected query ", query); } 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); } } } #endif