diff --git a/daemon/HTTPServer.cpp b/daemon/HTTPServer.cpp index bb24b48f..410ee86f 100644 --- a/daemon/HTTPServer.cpp +++ b/daemon/HTTPServer.cpp @@ -1206,6 +1206,7 @@ namespace http { } s << "\r\n
\r\n"; } +#ifndef NO_TORRENTS auto& torrentsTunnels = i2p::client::context.GetTorrentsTunnels (); if (!torrentsTunnels.empty ()) { @@ -1223,10 +1224,12 @@ namespace http { } s << "\r\n
\r\n"; } +#endif } static void ShowTorrentsPage (std::stringstream& s, std::map& params) { +#ifndef NO_TORRENTS bool js; i2p::config::GetOption("http.javascript", js); if (!js) { @@ -1284,6 +1287,9 @@ namespace http { } else s << "

Not found rpc tunnel " << tunnelname << "

\r\n"; +#else + s << "

Torrents are not supported

\r\n"; +#endif } HTTPConnection::HTTPConnection (std::string hostname, std::shared_ptr socket): diff --git a/libi2pd/Datagram.cpp b/libi2pd/Datagram.cpp index 9e3362a2..9fb7776e 100644 --- a/libi2pd/Datagram.cpp +++ b/libi2pd/Datagram.cpp @@ -98,10 +98,10 @@ namespace datagram { constexpr uint8_t flags[] = { 0x00, 0x02 }; // datagram2, no options // signature - std::vector signedData (len + m_From.size () + 2); - memcpy (signedData.data (), m_From.data (), m_From.size ()); - memcpy (signedData.data () + m_From.size (), flags, 2); - memcpy (signedData.data () + m_From.size () + 2, payload, len); + std::vector signedData (len + 32 + 2); + memcpy (signedData.data (), session->GetRemoteIdent (), 32); + memcpy (signedData.data () + 32, flags, 2); + memcpy (signedData.data () + 32 + 2, payload, len); m_Owner->Sign (signedData.data (), signedData.size (), m_Signature.data ()); // TODO: offline signatures and options msg = CreateDataMessage ({{m_From.data (), m_From.size ()}, {flags, 2}, {payload, len}, @@ -267,7 +267,7 @@ namespace datagram return; } std::vector signedData (len + 32 - identityLen - signatureLen); - memcpy (signedData.data (), identity.GetIdentHash (), 32); + memcpy (signedData.data (), m_Owner->GetIdentity ()->GetIdentHash (), 32); memcpy (signedData.data () + 32, buf + identityLen, signedData.size () - 32); verified = transientVerifier ? transientVerifier->Verify (signedData.data (), signedData.size (), buf + len - signatureLen) : identity.Verify (signedData.data (), signedData.size (), buf + len - signatureLen); @@ -488,10 +488,10 @@ namespace datagram void DatagramDestination::CleanUp () { - if (m_Sessions.empty ()) return; auto now = i2p::util::GetMillisecondsSinceEpoch(); - LogPrint(eLogDebug, "DatagramDestination: clean up sessions"); std::unique_lock lock(m_SessionsMutex); + if (m_Sessions.empty ()) return; + LogPrint(eLogDebug, "DatagramDestination: clean up sessions"); // for each session ... for (auto it = m_Sessions.begin (); it != m_Sessions.end (); ) { diff --git a/libi2pd/Datagram.h b/libi2pd/Datagram.h index 22011153..478ec51d 100644 --- a/libi2pd/Datagram.h +++ b/libi2pd/Datagram.h @@ -80,6 +80,7 @@ namespace datagram bool IsRatchets () const { return m_RoutingSession && m_RoutingSession->IsRatchets (); } void SetRemoteLeaseSet (std::shared_ptr ls) { m_RemoteLeaseSet = ls; } std::shared_ptr GetRemoteLeaseSet () const { return m_RemoteLeaseSet; } + const i2p::data::IdentHash& GetRemoteIdent () const { return m_RemoteIdent; } DatagramVersion GetVersion () const { return m_Version; } void SetVersion (DatagramVersion version) { m_Version = version; } diff --git a/libi2pd/HTTP.cpp b/libi2pd/HTTP.cpp index 03321cf5..43a110bc 100644 --- a/libi2pd/HTTP.cpp +++ b/libi2pd/HTTP.cpp @@ -133,6 +133,7 @@ namespace http } /* hostname[:port][/path] */ + if (pos_p >= url.length ()) return false; /* nothing left after schema or user part */ if (url.at(pos_p) == '[') // ipv6 { auto pos_b = url.find(']', pos_p); @@ -616,6 +617,11 @@ namespace http return false; /* too large chunk */ char * buf = new char[len]; in.read (buf, len); + if (in.gcount () != len) /* chunk is shorter than announced */ + { + delete[] buf; + return false; + } out.write (buf, len); delete[] buf; std::getline (in, hexLen); // read \r\n after chunk diff --git a/libi2pd/NTCP2.cpp b/libi2pd/NTCP2.cpp index efbc29f2..f601d16b 100644 --- a/libi2pd/NTCP2.cpp +++ b/libi2pd/NTCP2.cpp @@ -1283,8 +1283,14 @@ namespace transport LogPrint (eLogDebug, "NTCP2: Block type ", (int)blk, " of size ", size); if (offset + size > len) { - LogPrint (eLogError, "NTCP2: Unexpected block length ", size); - break; + if (blk == eNTCP2BlkPadding) + // padding is always last block and we set size up to end of frame + size = len - offset; + else + { + LogPrint (eLogError, "NTCP2: Unexpected block length ", size, " of type ", (int)blk); + break; + } } switch (blk) { diff --git a/libi2pd/Reseed.cpp b/libi2pd/Reseed.cpp index ee02a953..5e47b8c5 100644 --- a/libi2pd/Reseed.cpp +++ b/libi2pd/Reseed.cpp @@ -330,7 +330,9 @@ namespace data { int numFiles = 0; size_t contentPos = s.tellg (); - while (!s.eof ()) + // not !eof: seekg clears eofbit, so a stream broken in the middle of a + // header stays not-eof forever and the loop never ends + while (s.good ()) { uint32_t signature; s.read ((char *)&signature, 4); @@ -357,13 +359,18 @@ namespace data uint16_t fileNameLength, extraFieldLength; s.read ((char *)&fileNameLength, 2); fileNameLength = le16toh (fileNameLength); + s.read ((char *)&extraFieldLength, 2); + extraFieldLength = le16toh (extraFieldLength); + if (!s.good ()) + { + LogPrint (eLogError, "Reseed: Truncated zip local file header"); + return numFiles; + } if ( fileNameLength >= 255 ) { // too big LogPrint(eLogError, "Reseed: SU3 fileNameLength too large: ", fileNameLength); return numFiles; } - s.read ((char *)&extraFieldLength, 2); - extraFieldLength = le16toh (extraFieldLength); char localFileName[255]; s.read (localFileName, fileNameLength); localFileName[fileNameLength] = 0; @@ -371,7 +378,7 @@ namespace data // take care about data descriptor if presented if (bitFlag & ZIP_BIT_FLAG_DATA_DESCRIPTOR) { - size_t pos = s.tellg (); + auto pos = s.tellg (); if (!FindZipDataDescriptor (s)) { LogPrint (eLogError, "Reseed: SU3 archive data descriptor not found"); @@ -402,6 +409,12 @@ namespace data uint8_t * compressed = new uint8_t[compressedSize]; s.read ((char *)compressed, compressedSize); + if (!s.good ()) + { + LogPrint (eLogError, "Reseed: Truncated zip file data"); + delete[] compressed; + return numFiles; + } if (compressionMethod) // we assume Deflate { z_stream inflator; @@ -483,7 +496,7 @@ namespace data bool Reseeder::FindZipDataDescriptor (std::istream& s) { size_t nextInd = 0; - while (!s.eof ()) + while (s.good ()) // a broken stream never reaches eof, see ProcessZIPStream { uint8_t nextByte; s.read ((char *)&nextByte, 1); diff --git a/libi2pd/Streaming.cpp b/libi2pd/Streaming.cpp index d59ab3a5..6afddfce 100644 --- a/libi2pd/Streaming.cpp +++ b/libi2pd/Streaming.cpp @@ -1531,6 +1531,9 @@ namespace stream void Stream::SendPackets (const std::list& packets) { + // the local destination is a reference and it can be gone already: a handler + // queued before the destination was stopped runs after it + if (m_Status == eStreamStatusTerminated) return; if (!m_RemoteLeaseSet) { CancelRemoteLeaseChange (); diff --git a/libi2pd/TunnelEndpoint.cpp b/libi2pd/TunnelEndpoint.cpp index c8b31343..8bdb90ba 100644 --- a/libi2pd/TunnelEndpoint.cpp +++ b/libi2pd/TunnelEndpoint.cpp @@ -322,6 +322,12 @@ namespace tunnel LogPrint (eLogInfo, "TunnelMessage: Message expired"); return; } + if (msg.data->GetLength () < I2NP_HEADER_SIZE) + { + // GetPayloadLength subtracts the header size, a shorter message wraps it around + LogPrint (eLogError, "TunnelMessage: Message of ", msg.data->GetLength (), " bytes is shorter than I2NP header, dropped"); + return; + } uint8_t typeID = msg.data->GetTypeID (); LogPrint (eLogDebug, "TunnelMessage: Handle fragment of ", msg.data->GetLength (), " bytes, msg type ", (int)typeID); diff --git a/libi2pd_client/ClientContext.cpp b/libi2pd_client/ClientContext.cpp index d5089e78..2a60cd29 100644 --- a/libi2pd_client/ClientContext.cpp +++ b/libi2pd_client/ClientContext.cpp @@ -117,9 +117,11 @@ namespace client m_AddressBook.StartResolvers (); +#ifndef NO_TORRENTS // start torrents RPC server for (auto& it: m_TorrentsRPCServers) it.second->Start (); +#endif } std::shared_ptr ClientContext::DetachHttpProxy () @@ -168,6 +170,7 @@ namespace client } m_ServerTunnels.clear (); +#ifndef NO_TORRENTS // start torrents RPC server for (auto& it: m_TorrentsRPCServers) it.second->Stop (); @@ -178,6 +181,7 @@ namespace client it.second->Stop (); } m_TorrentsTunnels.clear (); +#endif if (m_SamBridge) { @@ -980,7 +984,7 @@ namespace client } else if (type == I2P_TUNNELS_SECTION_TYPE_TORRENTS) { - +#ifndef NO_TORRENTS // mandatory params std::string torrentsDir = section.second.get (TORRENTS_TUNNEL_TORRENTS_DIR); // optional params @@ -1038,6 +1042,9 @@ namespace client } else LogPrint (eLogError, "Clients: Failed to create torrents tunnel ", name, ". Duplicate keys ", keys); +#else + LogPrint (eLogError, "Clients: Torrents tunnels are not supported. Comppile without NO_TORRENTS"); +#endif } else LogPrint (eLogError, "Clients: Unknown section type = ", type, " of ", name, " in ", tunConf); @@ -1216,6 +1223,7 @@ namespace client } } +#ifndef NO_TORRENTS std::shared_ptr ClientContext::CreateTorrentsRPCServer ( std::string_view address, uint16_t port) { @@ -1238,5 +1246,6 @@ namespace client } return nullptr; } +#endif } } diff --git a/libi2pd_client/ClientContext.h b/libi2pd_client/ClientContext.h index 663a3276..f9e37925 100644 --- a/libi2pd_client/ClientContext.h +++ b/libi2pd_client/ClientContext.h @@ -152,7 +152,9 @@ namespace client void CreateNewSharedLocalDestination (); void AddLocalDestination (std::shared_ptr localDestination); +#ifndef NO_TORRENTS std::shared_ptr CreateTorrentsRPCServer (std::string_view address, uint16_t port); +#endif private: @@ -171,8 +173,10 @@ namespace client std::map > m_ClientForwards; // local endpoint -> udp tunnel std::map, std::shared_ptr > m_ServerForwards; // -> udp tunnel +#ifndef NO_TORRENTS std::unordered_map > m_TorrentsTunnels; // local destination -> torrents tunnel std::unordered_map > m_TorrentsRPCServers; +#endif SAMBridge * m_SamBridge; BOBCommandChannel * m_BOBCommandChannel; @@ -194,8 +198,10 @@ namespace client const decltype(m_ServerTunnels)& GetServerTunnels () const { return m_ServerTunnels; }; const decltype(m_ClientForwards)& GetClientForwards () const { return m_ClientForwards; } const decltype(m_ServerForwards)& GetServerForwards () const { return m_ServerForwards; } +#ifndef NO_TORRENTS const decltype(m_TorrentsTunnels)& GetTorrentsTunnels () const { return m_TorrentsTunnels; } const decltype(m_TorrentsRPCServers)& GetTorrentsRPCServers () const { return m_TorrentsRPCServers; } +#endif std::shared_ptr GetHttpProxy () const { std::lock_guard l(m_ProxyMutex); diff --git a/libi2pd_client/Torrents.cpp b/libi2pd_client/Torrents.cpp index d7f24531..5d580504 100644 --- a/libi2pd_client/Torrents.cpp +++ b/libi2pd_client/Torrents.cpp @@ -6,6 +6,8 @@ * See full license text in LICENSE file at top of project tree */ +#ifndef NO_TORRENTS + #include #include #include @@ -147,6 +149,13 @@ namespace torrents return ret; } + static std::string CreateInteger (int64_t v) + { + std::string ret ("i"); + ret += std::to_string (v); ret += "e"; + return ret; + } + static std::string CreateDictionary (const std::vector >& items) { std::stringstream s; @@ -154,7 +163,7 @@ namespace torrents for (const auto& [name, value]: items) if (!name.empty () && !value.empty ()) { - s << name; s << value; + s << CreateByteString (name); s << value; } s << 'e'; return s.str (); @@ -322,11 +331,15 @@ namespace torrents } } - Torrent::Torrent (std::string_view buf): + Torrent::Torrent (): m_Length (0), m_PieceLength (0), m_IsComplete (false), m_IsStopped (false), m_Uploaded (0), m_Downloaded (0) { ResetStats (); + } + + Torrent::Torrent (std::string_view buf): Torrent () + { ParseDictionary (buf, [this](std::string_view key, std::string_view buf)->size_t { if (key == "announce") @@ -341,6 +354,10 @@ namespace torrents }); } + Torrent::Torrent (const InfoHash& infoHash): Torrent () + { + m_InfoHash = infoHash; + } size_t Torrent::ParsePieces (std::string_view buf) { @@ -356,7 +373,6 @@ namespace torrents return len; } - size_t Torrent::ParseInfo (std::string_view buf) { size_t len = ParseDictionary (buf, [this](std::string_view key, std::string_view buf)->size_t @@ -398,11 +414,20 @@ namespace torrents return 0; }); if (!len) return 0; + // save info + m_Info.resize (len); + memcpy (m_Info.data (), (const uint8_t *)buf.data (), len); // calculate info hash - SHA1 ((const uint8_t *)buf.data (), len, m_InfoHash.data ()); + SHA1 (m_Info.data (), len, m_InfoHash.data ()); return len; } + std::string Torrent::CreateTorrentFileContent () const + { + if (m_Info.empty ()) return ""; + return CreateDictionary ( {{ "info", std::string_view ((const char *)m_Info.data (), m_Info.size ()) }} ); + } + bool Torrent::IsSafeName (std::string_view name) { if (name.empty () || name == "." || name == "..") return false; @@ -490,9 +515,8 @@ namespace torrents void Torrent::ParseTrackerResponse (size_t trackerID, std::string_view buf) { - if (trackerID >= m_TrackersInfo.size ()) - m_TrackersInfo.resize (trackerID + 1, TrackerInfo{{}, MIN_TRACKER_REQUESTS_INTERVAL, - 0, 0, 0, i2p::util::GetSecondsSinceEpoch ()}); + CheckTrackerStatsSize (trackerID); + std::get<6>(m_TrackerStats[trackerID]) = ""; // clear error ParseDictionary (buf, [this, trackerID](std::string_view key, std::string_view buf)->size_t { if (key == "interval") @@ -501,8 +525,8 @@ namespace torrents if (l) { int interval = std::max (MIN_TRACKER_REQUESTS_INTERVAL, (int)value*1000); // in milliseconds - std::get<1>(m_TrackersInfo[trackerID]) = interval; - std::get<2>(m_TrackersInfo[trackerID]) = i2p::util::GetMonotonicMilliseconds () + interval; // reset next request + std::get<1>(m_TrackerStats[trackerID]) = interval; + std::get<2>(m_TrackerStats[trackerID]) = i2p::util::GetMonotonicMilliseconds () + interval; // reset next request } return l; } @@ -511,29 +535,34 @@ namespace torrents else if (key == "complete") { auto [seeders, l] = ExtractInteger (buf); - if (l) std::get<3>(m_TrackersInfo[trackerID]) = seeders; + if (l) std::get<3>(m_TrackerStats[trackerID]) = seeders; return l; } else if (key == "incomplete") { auto [leechers, l] = ExtractInteger (buf); - if (l) std::get<4>(m_TrackersInfo[trackerID]) = leechers; + if (l) std::get<4>(m_TrackerStats[trackerID]) = leechers; return l; } else if (key == "failure reason") { auto [reason, l] = ExtractByteString (buf); LogPrint (eLogError, "Torrents: Tracker error: ", reason); + std::get<6>(m_TrackerStats[trackerID]) = reason; + // double interval if tracker failure + int interval = std::max (MIN_TRACKER_REQUESTS_INTERVAL, std::get<1>(m_TrackerStats[trackerID])*2); + std::get<1>(m_TrackerStats[trackerID]) = interval; + std::get<2>(m_TrackerStats[trackerID]) = i2p::util::GetMonotonicMilliseconds () + interval; return l; } return 0; }); - std::get<5>(m_TrackersInfo[trackerID]) = i2p::util::GetSecondsSinceEpoch (); + std::get<5>(m_TrackerStats[trackerID]) = i2p::util::GetSecondsSinceEpoch (); } size_t Torrent::ParsePeers (size_t trackerID, std::string_view buf) { - auto& peers = std::get<0>(m_TrackersInfo[trackerID]); + auto& peers = std::get<0>(m_TrackerStats[trackerID]); peers.clear (); auto [hashes, len] = ExtractByteString (buf); while (!hashes.empty ()) @@ -547,10 +576,9 @@ namespace torrents void Torrent::HandleDatagramTrackerResponse (size_t trackerID, uint32_t interval, const uint8_t * hashes, size_t hashesLen, int numSeeders, int numLeechers) { - if (trackerID >= m_TrackersInfo.size ()) - m_TrackersInfo.resize (trackerID + 1, TrackerInfo{{}, MIN_TRACKER_REQUESTS_INTERVAL, - 0, 0, 0, i2p::util::GetSecondsSinceEpoch () }); - auto& [peers, trackerRequestInterval, nextRequestTime, seeders, leechers, lastUpdateTime] = m_TrackersInfo[trackerID]; + CheckTrackerStatsSize (trackerID); + auto& [peers, trackerRequestInterval, nextRequestTime, seeders, leechers, + lastUpdateTime, error] = m_TrackerStats[trackerID]; trackerRequestInterval = interval*1000; // milliseconds nextRequestTime = i2p::util::GetMonotonicMilliseconds () + trackerRequestInterval; seeders = numSeeders; @@ -567,6 +595,19 @@ namespace torrents } } + void Torrent::SetTrackerError (size_t trackerID, std::string_view error) + { + CheckTrackerStatsSize (trackerID); + std::get<6>(m_TrackerStats[trackerID]) = error; + } + + void Torrent::CheckTrackerStatsSize (size_t trackerID) + { + if (trackerID >= m_TrackerStats.size ()) + m_TrackerStats.resize (trackerID + 1, TrackerStats{{}, MIN_TRACKER_REQUESTS_INTERVAL, + 0, 0, 0, i2p::util::GetSecondsSinceEpoch (), ""}); + } + std::pair, bool> Torrent::CreateBitfield () const { size_t numPieces = m_Pieces.size (); @@ -778,10 +819,18 @@ namespace torrents } else { - if (piece.IsComplete ()) currentCompletedSize += (filesIT->second - currentSize); + size_t leftoverSize = filesIT->second - currentSize; + if (piece.IsComplete ()) currentCompletedSize += leftoverSize; completed.push_back (currentCompletedSize); - currentSize = 0; currentCompletedSize = 0; + currentSize = m_PieceLength - leftoverSize; filesIT++; + while (filesIT != m_Files.end () && filesIT->second <= currentSize) + { + completed.push_back (filesIT->second); + currentSize -= filesIT->second; + filesIT++; + } + currentCompletedSize = piece.IsComplete () ? currentSize : 0; if (filesIT == m_Files.end ()) break; } } @@ -791,10 +840,10 @@ namespace torrents std::unordered_set Torrent::GetPeers () const { - if (m_TrackersInfo.size () == 1) - return std::get<0>(m_TrackersInfo.front ()); + if (m_TrackerStats.size () == 1) + return std::get<0>(m_TrackerStats.front ()); std::unordered_set ret; - for (const auto& it: m_TrackersInfo) + for (const auto& it: m_TrackerStats) { const auto& peers = std::get<0>(it); ret.insert (peers.begin (), peers.end ()); @@ -804,16 +853,16 @@ namespace torrents uint64_t Torrent::GetNextTrackerRequestTime (size_t trackerID) const { - if (trackerID < m_TrackersInfo.size ()) return std::get<2>(m_TrackersInfo[trackerID]); + if (trackerID < m_TrackerStats.size ()) return std::get<2>(m_TrackerStats[trackerID]); return 0; } void Torrent::SetNextTrackerRequestTime (size_t trackerID, uint64_t ts) { - if (trackerID >= m_TrackersInfo.size ()) - m_TrackersInfo.resize (trackerID + 1, TrackerInfo{{}, MIN_TRACKER_REQUESTS_INTERVAL, - 0, 0, 0, i2p::util::GetSecondsSinceEpoch ()}); - std::get<2>(m_TrackersInfo[trackerID]) = ts; + if (trackerID >= m_TrackerStats.size ()) + m_TrackerStats.resize (trackerID + 1, TrackerStats{{}, MIN_TRACKER_REQUESTS_INTERVAL, + 0, 0, 0, i2p::util::GetSecondsSinceEpoch (), ""}); + std::get<2>(m_TrackerStats[trackerID]) = ts; } TorrentStatus Torrent::GetStatus () const @@ -828,7 +877,7 @@ namespace torrents m_Stream (stream), m_ReceiveBufferOffset (0), m_NextMsgLength (0), m_IsHandshakeSent (false), m_IsEstablished (false), m_IsChoked (true), m_IsRemoteChoked (true), m_IsInterested (false), m_IsRemoteInterested (false), m_LastReceiveTime (0), m_LastSendTime (0), - m_NumRequests (0), m_NumPieces (0), m_LastRequestedPieceIndex (-1), + m_NumRequests (0), m_NumPieces (0), m_LastRequestedPieceIndex (-1), m_RemoteMetadataSize (0), m_Downloaded (0), m_Uploaded (0) { ResetStats (); @@ -1197,6 +1246,8 @@ namespace torrents void PeerConnection::HandleHaveMsg (const uint8_t * buf, size_t len) { if (len < 4) return; + if (m_RemoteBitfield.empty ()) // bitfield was not received before because was empty + m_RemoteBitfield.resize (m_Torrent->GetNumPieces ()); uint32_t index = bufbe32toh (buf); if (index < m_RemoteBitfield.size ()) { @@ -1233,7 +1284,7 @@ namespace torrents void PeerConnection::HandleBitfieldMsg (const uint8_t * buf, size_t len) { - if (!m_Torrent) return; + if (!m_Torrent || !m_Torrent->GetLength ()) return; // we are magnet and don't have torrent info yet m_IsInterested = false; size_t numPieces = m_Torrent->GetNumPieces (); m_RemoteBitfield.resize (numPieces); @@ -1275,6 +1326,13 @@ namespace torrents size_t numPieces = m_Torrent->GetNumPieces (); m_RemoteBitfield.resize (numPieces); m_RemoteBitfield.set (); + if (!m_Torrent->IsComplete ()) + { + m_IsInterested = true; + SendInterestedMsg (); + } + else + Terminate (); // we don't need this connection } void PeerConnection::HandleHaveNoneMsg () @@ -1563,7 +1621,23 @@ namespace torrents ParseDictionary (std::string_view ((const char *)(buf + 1), len -1), [this](std::string_view key, std::string_view buf)->size_t { - if (key == "v") + if (key == "m") + { + return ParseDictionary (buf, [this](std::string_view msg, std::string_view msgID)->size_t + { + auto [id, l] = ExtractInteger (msgID); + if (l) + AddExtendedMsgHandler (msg, id); + return l; + }); + } + else if (key == "metadata_size") + { + auto [s, l] = ExtractInteger (buf); + if (l) m_RemoteMetadataSize = s; + return l; + } + else if (key == "v") { auto [v, l] = ExtractByteString (buf); if (l) m_RemoteName = v; @@ -1571,20 +1645,131 @@ namespace torrents } return 0; }); + if (!m_Torrent->GetLength () && m_RemoteMetadataSize) // torrent is magnet and peer supports BEP9 + // request first piece of info + SendExtendedMsg (EXTENSION_MSGID_UT_METADATA, CreateDictionary ({{ "msg_type", CreateInteger (0) }, { "piece", CreateInteger (0) }})); + } + else + { + auto it = m_ExtendedMessageHandlers.find (buf[0]); + if (it != m_ExtendedMessageHandlers.end ()) + (this->*(it->second))(buf + 1, len - 1); + else + LogPrint (eLogInfo, "Torrents: Unexpected extended message type ", (int)buf[0], " received"); } } - void PeerConnection::SendExtendedMsg () + void PeerConnection::AddExtendedMsgHandler (std::string_view extensionName, int64_t msgID) { - std::string payload = CreateDictionary ({{ CreateByteString ("v"), CreateByteString ("i2pd") }}); - std::vector sendBuffer (payload.length () + 1 + 5); - htobe32buf (sendBuffer.data (), payload.length () + 1 + 1); // length + if (extensionName == EXTENSION_NAME_UT_METADATA) + m_ExtendedMessageHandlers.emplace (msgID, &PeerConnection::HandleUtMetadataExtension); + } + + void PeerConnection::SendExtendedMsg (uint8_t extendedMsgID, std::string_view payload, std::string_view data) + { + std::string str; + if (!extendedMsgID) // handshake + { + str = CreateDictionary ({ + { "m", CreateDictionary ({ + { EXTENSION_NAME_UT_METADATA, CreateInteger (EXTENSION_MSGID_UT_METADATA) } + }) }, + { "metadata_size", CreateInteger (m_Torrent->GetInfo ().size ()) }, + { "v", CreateByteString ("i2pd") } + }); + payload = str; + } + std::vector sendBuffer (payload.length () + data.length () + 1 + 5); + htobe32buf (sendBuffer.data (), payload.length () + data.length () + 1 + 1); // length sendBuffer[4] = eMessageTypeExtended; // msg ID - sendBuffer[5] = 0; // handshake + sendBuffer[5] = extendedMsgID; memcpy (sendBuffer.data () + 6, payload.data (), payload.size ()); + if (!data.empty ()) + memcpy (sendBuffer.data () + 6 + payload.size (), data.data (), data.size ()); WriteToStream (sendBuffer.data (), sendBuffer.size ()); } + void PeerConnection::HandleUtMetadataExtension (const uint8_t * buf, size_t len) + { + if (!m_Torrent) return; + int msgType = -1, piece = -1; + auto payloadLen = ParseDictionary (std::string_view ((const char *)buf, len), + [&msgType, &piece](std::string_view key, std::string_view buf)->size_t + { + if (key == "msg_type") + { + auto [value, l] = ExtractInteger (buf); + if (l) msgType = value; + return l; + } + else if (key == "piece") + { + auto [value, l] = ExtractInteger (buf); + if (l) piece = value; + return l; + } + // ignore total_szie + return 0; + }); + if (msgType >=0 && piece >= 0) + { + switch (msgType) + { + case 0: // request + { + auto& info = m_Torrent->GetInfo (); + size_t offset = piece*REQUEST_BLOCK_SIZE; + if (offset < info.size ()) + { + size_t totalSize = std::min (info.size () - offset, REQUEST_BLOCK_SIZE); + SendExtendedMsg (EXTENSION_MSGID_UT_METADATA, + CreateDictionary ({{ "msg_type", CreateInteger (1) }, + { "piece", CreateInteger (piece) }, + { "total_size", CreateInteger (m_Torrent->GetInfo ().size ()) } }), + std::string_view ((const char *)info.data () + offset, totalSize)); + } + else + SendExtendedMsg (EXTENSION_MSGID_UT_METADATA, CreateDictionary ({{ "msg_type", CreateInteger (2) }, { "piece", CreateInteger (piece) }})); + break; + } + case 1: // data + { + size_t offset = piece*REQUEST_BLOCK_SIZE; + if (offset > m_RemoteMetadataSize) break; + size_t size = m_RemoteMetadataSize - offset; + if (size > REQUEST_BLOCK_SIZE) size = REQUEST_BLOCK_SIZE; + if (payloadLen + size > len) break; + if (offset == m_RemoteMetadata.size () && size) + { + m_RemoteMetadata.resize (m_RemoteMetadata.size () + size); + memcpy (m_RemoteMetadata.data () + offset, buf + payloadLen, size); + if (m_RemoteMetadata.size () < m_RemoteMetadataSize) + // request next piece + SendExtendedMsg (EXTENSION_MSGID_UT_METADATA, CreateDictionary ({{ "msg_type", CreateInteger (0) }, { "piece", CreateInteger (piece + 1) }})); + else + { + // all info received + LogPrint (eLogDebug, "Torrents: ut_metadata ", m_RemoteMetadataSize, " bytes of info received"); + uint8_t digest[SHA_DIGEST_LENGTH]; + SHA1 (m_RemoteMetadata.data (), m_RemoteMetadata.size (), digest); + if (!memcmp (m_Torrent->GetInfoHash ().data (), digest, SHA_DIGEST_LENGTH)) + GetTorrentsTunnel ()->UpdateTorrentInfo (m_Torrent, std::string_view ((const char *)m_RemoteMetadata.data (), m_RemoteMetadata.size ())); + else + LogPrint (eLogError, "Torrents: ut_metadata info doesn't match infoHash"); + Close (); // we need to reconnect to receive bitfield + } + } + break; + } + case 2: // reject + LogPrint (eLogError, "Torrents: ut_metadata piece ", piece, " request rejected"); + break; + default: + LogPrint (eLogInfo, "Torrents: ut_metadata msg_type ", msgType, " is not supported"); + } + } + } + std::optional PeerConnection::GetNextBlockToRequest () { auto block = m_Torrent->GetNextBlockToRequest (shared_from_this (), true); // skip already requested pieces @@ -1627,3 +1812,5 @@ namespace torrents } } } + +#endif // NO_TORRENTS diff --git a/libi2pd_client/Torrents.h b/libi2pd_client/Torrents.h index 38095366..99f54ccf 100644 --- a/libi2pd_client/Torrents.h +++ b/libi2pd_client/Torrents.h @@ -9,6 +9,8 @@ #ifndef TORRENTS_H__ #define TORRENTS_H__ +#ifndef NO_TORRENTS + #include #include #include @@ -53,6 +55,10 @@ namespace torrents constexpr size_t REQUEST_MSG_LENGTH = REQUEST_MSG_PAYLOAD_LENGTH + 5; constexpr size_t HAVE_MSG_PAYLOAD_LENGTH = 4; + // extensions + constexpr std::string_view EXTENSION_NAME_UT_METADATA { "ut_metadata" }; + constexpr uint8_t EXTENSION_MSGID_UT_METADATA = 1; + enum MessageType { eMessageTypeChoke = 0, @@ -146,14 +152,18 @@ namespace torrents using RequestedBlock = std::tuple; // (index, offset, len) class Torrent final { + using TrackerStats = std::tuple, + int, uint64_t, int, int, uint64_t, std::string>; + // (peers, interval, next tracker request time in monotonic milliseconds, + // seeders, leechers, last update time in seconds since epoch, error) public: - using InfoHash = std::array; - using TrackerInfo = std::tuple, int, uint64_t, int, int, uint64_t>; - // (peers, interval, next tracker request time in monotonic milliseconds, - // seeders, leechers, last update time in seconds since epoch) + using InfoHash = std::array; Torrent (std::string_view buf); + Torrent (const InfoHash& infoHash); + size_t ParseInfo (std::string_view buf); + std::string CreateTorrentFileContent () const; void ParseTrackerResponse (size_t trackerID, std::string_view buf); void HandleDatagramTrackerResponse (size_t trackerID, uint32_t interval, const uint8_t * hashes, size_t hashesLen, int numSeeders, int numLeechers); @@ -173,7 +183,8 @@ namespace torrents std::list >& GetFiles () { return m_Files; } size_t GetLength () const { return m_Length; } size_t GetPieceLength () const { return m_PieceLength; } - int GetInterval (size_t trackerID) const { return (trackerID < m_TrackersInfo.size ()) ? std::get<1>(m_TrackersInfo[trackerID]) : MIN_TRACKER_REQUESTS_INTERVAL; } + int GetInterval (size_t trackerID) const { return (trackerID < m_TrackerStats.size ()) ? std::get<1>(m_TrackerStats[trackerID]) : MIN_TRACKER_REQUESTS_INTERVAL; } + const std::vector GetInfo () const { return m_Info; } const InfoHash& GetInfoHash () const { return m_InfoHash; } size_t GetLeft () const; std::string GetHexStringInfoHash () const; // in url format @@ -210,27 +221,31 @@ namespace torrents int GetNumUploadingToPeers () const { return m_NumUploadingToPeers; } void SetNumUploadingToPeers (int numUploadingToPeers) { m_NumUploadingToPeers = numUploadingToPeers; } - int GetNumSeeders (size_t trackerID) const { return (trackerID < m_TrackersInfo.size ()) ? std::get<3>(m_TrackersInfo[trackerID]) : 0; } - int GetNumLeechers (size_t trackerID) const { return (trackerID < m_TrackersInfo.size ()) ? std::get<4>(m_TrackersInfo[trackerID]) : 0; } - int GetNumPeers (size_t trackerID) const { return (trackerID < m_TrackersInfo.size ()) ? std::get<0>(m_TrackersInfo[trackerID]).size () : 0; } - uint64_t GetLastTrackerUpdateTime (size_t trackerID) const { return (trackerID < m_TrackersInfo.size ()) ? std::get<5>(m_TrackersInfo[trackerID]) : 0; } + int GetNumSeeders (size_t trackerID) const { return (trackerID < m_TrackerStats.size ()) ? std::get<3>(m_TrackerStats[trackerID]) : 0; } + int GetNumLeechers (size_t trackerID) const { return (trackerID < m_TrackerStats.size ()) ? std::get<4>(m_TrackerStats[trackerID]) : 0; } + int GetNumPeers (size_t trackerID) const { return (trackerID < m_TrackerStats.size ()) ? std::get<0>(m_TrackerStats[trackerID]).size () : 0; } + uint64_t GetLastTrackerUpdateTime (size_t trackerID) const { return (trackerID < m_TrackerStats.size ()) ? std::get<5>(m_TrackerStats[trackerID]) : 0; } + std::string GetTrackerError (size_t trackerID) const { return (trackerID < m_TrackerStats.size ()) ? std::get<6>(m_TrackerStats[trackerID]) : ""; } + void SetTrackerError (size_t trackerID, std::string_view error); private: + Torrent (); size_t ParsePieces (std::string_view buf); - size_t ParseInfo (std::string_view buf); size_t ParsePeers (size_t trackerID, std::string_view buf); size_t ParseFiles (std::string_view buf); static bool IsSafeName (std::string_view name); // a name from a torrent file before it becomes a path + void CheckTrackerStatsSize (size_t trackerID); private: std::string m_Name, m_Announce; std::filesystem::path m_FullPath; size_t m_Length, m_PieceLength; + std::vector m_Info; // for BEP9 InfoHash m_InfoHash; // SHA1 std::vector m_Pieces; - std::vector m_TrackersInfo; + std::vector m_TrackerStats; bool m_IsComplete, m_IsStopped; std::list > m_Files; // list of (path, length) size_t m_Uploaded, m_Downloaded; @@ -242,6 +257,8 @@ namespace torrents class TorrentsTunnel; class PeerConnection: public i2p::client::I2PServiceHandler, public std::enable_shared_from_this { + using ExtendedMessageHandler = void (PeerConnection::*)(const uint8_t * buf, size_t len); + public: using PeerID = std::array; @@ -310,7 +327,9 @@ namespace torrents void SendChokeMsg (); void HandleChokeMsg (); void HandleExtendedMsg (const uint8_t * buf, size_t len); - void SendExtendedMsg (); + void SendExtendedMsg (uint8_t extendedMsgID = 0, std::string_view payload = "", std::string_view data = ""); + void AddExtendedMsgHandler (std::string_view extensionName, int64_t msgID); + void HandleUtMetadataExtension (const uint8_t * buf, size_t len); // BEP9 std::optional GetNextBlockToRequest (); bool RequestNextBlocks (); @@ -332,6 +351,10 @@ namespace torrents std::list m_IncomingRequestsQueue; int m_LastRequestedPieceIndex; std::unique_ptr m_HandshakeReceiveTimer; + std::unordered_map m_ExtendedMessageHandlers; + // BEP9 + size_t m_RemoteMetadataSize; + std::vector m_RemoteMetadata; // stats uint64_t m_DownloadRate, m_UploadRate; // B/sec uint64_t m_LastBlockDownloadTimestamp, m_LastBlockUploadTimestamp; // monotonic milliseconds @@ -340,4 +363,7 @@ namespace torrents }; } } + +#endif // NO_TORRENTS + #endif diff --git a/libi2pd_client/TorrentsRPC.cpp b/libi2pd_client/TorrentsRPC.cpp index f85bfceb..d23ad40d 100644 --- a/libi2pd_client/TorrentsRPC.cpp +++ b/libi2pd_client/TorrentsRPC.cpp @@ -6,6 +6,8 @@ * See full license text in LICENSE file at top of project tree */ +#ifndef NO_TORRENTS + #include #if !defined(ANDROID) && (BOOST_VERSION >= 108100) // boost::json since 1.75, we allow it since 1.81 due to std::string_view compatibility #include @@ -46,7 +48,7 @@ namespace torrents private: std::string SuccessResponse (int64_t id, boost::json::object&& arguments); - std::string ResultResponse (int64_t id, boost::json::object&& result); + std::string ResultResponse (int64_t id, boost::json::value&& result); std::string ErrorResponse (JSONRPCErrorCode errorCode, int64_t id, std::string_view message); static int64_t GetTag (boost::json::object& jsonRequest); static std::vector GetTorrentIds (boost::json::object& arguments); @@ -54,6 +56,7 @@ namespace torrents boost::json::array GetPeers (std::shared_ptr torrent) const; boost::json::array GetTrackers (std::shared_ptr torrent) const; boost::json::array GetTrackerStats (std::shared_ptr torrent) const; + boost::json::array GetFiles (std::shared_ptr torrent) const; static std::string_view RecognizeClientByPeerID (const PeerConnection::PeerID& peerID); std::string HandleTorrentAdd (boost::json::object&& jsonRequest); @@ -76,7 +79,7 @@ namespace torrents return boost::json::serialize(response); } - std::string JSONRPCHandler::ResultResponse (int64_t id, boost::json::object&& result) + std::string JSONRPCHandler::ResultResponse (int64_t id, boost::json::value&& result) { boost::json::object response; response["jsonrpc"] = "2.0"; @@ -166,32 +169,40 @@ namespace torrents std::string JSONRPCHandler::HandleTorrentAdd (boost::json::object&& jsonRequest) { auto arguments = jsonRequest.at ("arguments").as_object (); - auto b64torrent = arguments.at ("metainfo").as_string (); - std::string torrentFileContent; - torrentFileContent.resize (boost::beast::detail::base64::decoded_size (b64torrent.size ())); - boost::beast::detail::base64::decode (torrentFileContent.data (), b64torrent.data (), b64torrent.size ()); - auto [torrent, id] = m_Tunnel->AddTorrent (torrentFileContent); + std::shared_ptr torrent; int id = 0; + if (arguments.contains ("metainfo")) + { + auto b64torrent = arguments.at ("metainfo").as_string (); + std::string torrentFileContent; + torrentFileContent.resize (boost::beast::detail::base64::decoded_size (b64torrent.size ())); + boost::beast::detail::base64::decode (torrentFileContent.data (), b64torrent.data (), b64torrent.size ()); + std::tie (torrent, id) = m_Tunnel->AddTorrent (torrentFileContent); + } + else if (arguments.contains ("filename")) + std::tie (torrent, id) = m_Tunnel->AddMagnet (arguments.at ("filename").as_string ()); - boost::json::object response, torrentInfo; if (torrent) { + boost::json::object response, torrentInfo; std::string hexHash; boost::algorithm::hex (torrent->GetInfoHash ().begin(), torrent->GetInfoHash ().end(), std::back_inserter(hexHash)); torrentInfo["id"] = id; torrentInfo["hashString"] = hexHash; torrentInfo["name"] = torrent->GetName (); - } - if (id) - { - response["torrent-added"] = torrentInfo; - LogPrint (eLogDebug, "TorrentsRPC: torrent added ", torrentInfo["name"].as_string ()); + if (id) + { + response["torrent-added"] = torrentInfo; + LogPrint (eLogDebug, "TorrentsRPC: torrent added ", torrentInfo["name"].as_string ()); + } + else + { + response["torrent-duplicate"] = torrentInfo; + LogPrint (eLogDebug, "TorrentsRPC: duplicate torrent ", torrentInfo["name"].as_string ()); + } + return SuccessResponse (GetTag (jsonRequest), std::move (response)); } else - { - response["torrent-duplicate"] = torrentInfo; - LogPrint (eLogDebug, "TorrentsRPC: duplicate torrent ", torrentInfo["name"].as_string ()); - } - return SuccessResponse (GetTag (jsonRequest), std::move (response)); + return ResultResponse (GetTag (jsonRequest), boost::json::string ("invalid or corrupt torrent file")); } std::string JSONRPCHandler::HandleTorrentRemove (boost::json::object&& jsonRequest) @@ -244,6 +255,8 @@ namespace torrents t["trackers"] = GetTrackers (torrent); else if (field == "trackerStats") t["trackerStats"] = GetTrackerStats (torrent); + else if (field == "files") + t["files"] = GetFiles (torrent); else { auto fieldValue = GetFieldValue (field.as_string (), torrent); @@ -296,35 +309,6 @@ namespace torrents return boost::json::value(b64pieces); } }, - { "files", [](std::shared_ptr torrent) - { - boost::json::array files; - const auto& torrentFiles = torrent->GetFiles (); - if (torrentFiles.empty ()) - { - boost::json::object file; - file["name"] = torrent->GetFullPath ().string (); - file["length"] = torrent->GetLength (); - file["bytesCompleted"] = torrent->GetLength () - torrent->GetLeft (); - files.push_back (file); - } - else - { - auto filesCompleted = torrent->GetFilesCompleted (); - size_t ind = 0; - for (const auto& [filePath, fileSize]: torrentFiles) - { - boost::json::object file; - file["name"] = filePath.string (); - file["length"] = fileSize; - file["bytesCompleted"] = (ind < filesCompleted.size ()) ? filesCompleted[ind] : 0; - files.push_back (file); - ind++; - } - } - return files; - } - }, { "wanted", [](std::shared_ptr torrent) { boost::json::array wanted; @@ -401,7 +385,8 @@ namespace torrents peer["clientIsIntersted"] = it->IsInterested (); peer["peerIsInterested"] = it->IsRemoteInterested (); peer["flagStr"] = flags; - peer["progress"] = torrent->GetLength () ? ((float)it->GetDownloaded ())/(float)(torrent->GetLength ()) : 1.0; + const auto& remoteBitfield = it->GetRemoteBitfield (); + peer["progress"] = remoteBitfield.size () ? ((float)remoteBitfield.count ())/((float)remoteBitfield.size ()) : 0.0; peers.push_back (peer); } } @@ -411,12 +396,11 @@ namespace torrents boost::json::array JSONRPCHandler::GetTrackers (std::shared_ptr torrent) const { boost::json::array trackers; - const auto& tunnelTrackers = m_Tunnel->GetTrackers (); - for (size_t i = 0; i < tunnelTrackers.size (); i++) + for (size_t i = 0; i < m_Tunnel->GetNumTrackers (); i++) { boost::json::object tracker; tracker["id"] = i; - tracker["announce"] = tunnelTrackers[i]; + tracker["announce"] = m_Tunnel->GetTrackerAnnounce (i); tracker["tier"] = 0; trackers.push_back (tracker); } @@ -426,14 +410,13 @@ namespace torrents boost::json::array JSONRPCHandler::GetTrackerStats (std::shared_ptr torrent) const { boost::json::array trackers; - const auto& tunnelTrackers = m_Tunnel->GetTrackers (); - for (size_t i = 0; i < tunnelTrackers.size (); i++) + for (size_t i = 0; i < m_Tunnel->GetNumTrackers (); i++) { boost::json::object tracker; tracker["id"] = i; - tracker["announce"] = tunnelTrackers[i]; + tracker["announce"] = m_Tunnel->GetTrackerAnnounce (i); i2p::http::URL announceURL; - announceURL.parse (tunnelTrackers[i]); + announceURL.parse (m_Tunnel->GetTrackerAnnounce (i)); tracker["host"]= announceURL.host; tracker["announceState"] = 1; tracker["scrapeState"] = 0; @@ -445,8 +428,17 @@ namespace torrents tracker["seederCount"] = torrent->GetNumSeeders (i); tracker["leecherCount"] = torrent->GetNumLeechers (i); tracker["lastAnnouncePeerCount"] = torrent->GetNumPeers (i); - tracker["lastAnnounceResult"] = "Success"; // TODO: - tracker["lastAnnounceSucceeded"] = true; // TODO: + auto trackerError = torrent->GetTrackerError (i); + if (trackerError.empty ()) + { + tracker["lastAnnounceResult"] = "Success"; + tracker["lastAnnounceSucceeded"] = true; + } + else + { + tracker["lastAnnounceResult"] = trackerError; + tracker["lastAnnounceSucceeded"] = false; + } tracker["lastAnnounceTimedOut"] = false; // TODO: tracker["nextAnnounceTime"] = (torrent->GetNextTrackerRequestTime (i) - i2p::util::GetMonotonicMilliseconds () + i2p::util::GetMillisecondsSinceEpoch ())/1000; @@ -463,6 +455,36 @@ namespace torrents return trackers; } + boost::json::array JSONRPCHandler::GetFiles (std::shared_ptr torrent) const + { + boost::json::array files; + const auto& torrentFiles = torrent->GetFiles (); + if (torrentFiles.empty ()) + { + boost::json::object file; + file["name"] = torrent->GetName (); + file["length"] = torrent->GetLength (); + file["bytesCompleted"] = torrent->GetLength () - torrent->GetLeft (); + files.push_back (file); + } + else + { + const auto& torrentsDir = m_Tunnel->GetTorrentsDir (); + auto filesCompleted = torrent->GetFilesCompleted (); + size_t ind = 0; + for (const auto& [filePath, fileSize]: torrentFiles) + { + boost::json::object file; + file["name"] = std::filesystem::relative (filePath, torrentsDir).string (); + file["length"] = fileSize; + file["bytesCompleted"] = (ind < filesCompleted.size ()) ? filesCompleted[ind] : 0; + files.push_back (file); + ind++; + } + } + return files; + } + std::string_view JSONRPCHandler::RecognizeClientByPeerID (const PeerConnection::PeerID& peerID) { static constexpr std::array i2psnark { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0x03, 0x03 }; @@ -526,28 +548,32 @@ namespace torrents { if (!ecode) { +#ifdef JSON_SUPPORTED if (m_Request.method() == boost::beast::http::verb::post) { auto path = m_Request.target (); auto tunnel = m_Server.GetTunnel ( {path.data (), path.size ()}); // boost::beast::string_view to std::string_view if (tunnel) { - if (m_Request[boost::beast::http::field::content_type] == "application/json" || + static constexpr std::string_view appjson { "application/json" }; +#if __cplusplus >= 202002L // C++20 + if (m_Request[boost::beast::http::field::content_type].starts_with (appjson) || // starts with +#else + if (m_Request[boost::beast::http::field::content_type].substr (0, appjson.size ()) == appjson || // starts with +#endif m_Request[boost::beast::http::field::content_type] == "application/x-www-form-urlencoded") { -#ifdef JSON_SUPPORTED + JSONRPCHandler jsonHandler (tunnel); auto response = jsonHandler.HandleRequest (m_Request.body ()); if (!response.empty ()) SendResponse (boost::beast::http::status::ok, response); -#else - SendResponse (boost::beast::http::status::not_implemented, std::string ("Your boost version ") + BOOST_LIB_VERSION + " is too old"); -#endif } else SendResponse (boost::beast::http::status::no_content); } - else { + else + { LogPrint(eLogDebug, "TorrentRPC ", path, " not found ", tunnel); SendResponse (boost::beast::http::status::not_found); } @@ -556,6 +582,9 @@ namespace torrents SendResponse (boost::beast::http::status::no_content, "", true); else SendResponse (boost::beast::http::status::method_not_allowed); +#else + SendResponse (boost::beast::http::status::not_implemented, std::string ("Your boost version ") + BOOST_LIB_VERSION + " is too old"); +#endif } } @@ -609,7 +638,10 @@ namespace torrents std::string rpcPath ("/"); if (!path.empty ()) rpcPath += std::string (path) + "/"; - rpcPath += "rpc/"; + // insert both pathes rpc and rpc/ + rpcPath += "rpc"; + m_Tunnels.emplace (rpcPath, tunnel); + rpcPath += "/"; m_Tunnels.emplace (rpcPath, tunnel); } @@ -649,3 +681,5 @@ namespace torrents } } } + +#endif diff --git a/libi2pd_client/TorrentsRPC.h b/libi2pd_client/TorrentsRPC.h index a04fdb04..342ef878 100644 --- a/libi2pd_client/TorrentsRPC.h +++ b/libi2pd_client/TorrentsRPC.h @@ -9,6 +9,8 @@ #ifndef TORRENTS_RPC_H__ #define TORRENTS_RPC_H__ +#ifndef NO_TORRENTS + #include #include #include @@ -76,3 +78,5 @@ namespace torrents } #endif + +#endif diff --git a/libi2pd_client/TorrentsTunnel.cpp b/libi2pd_client/TorrentsTunnel.cpp index 724f7c07..1c106e8d 100644 --- a/libi2pd_client/TorrentsTunnel.cpp +++ b/libi2pd_client/TorrentsTunnel.cpp @@ -6,8 +6,11 @@ * See full license text in LICENSE file at top of project tree */ +#ifndef NO_TORRENTS + #include #include +#include #include #include #include "Log.h" @@ -33,7 +36,12 @@ namespace torrents m_PeerID += localDestination->GetIdentHash ().ToBase64 (); m_PeerID.resize (20, '0'); if (!trackers.empty ()) - boost::split(m_Trackers, trackers, boost::is_any_of(","), boost::token_compress_on); + { + std::vector trackersList; + boost::split(trackersList, trackers, boost::is_any_of(","), boost::token_compress_on); + for (const auto& it: trackersList) + m_Trackers.emplace_back (TrackerInfo{ it, 0, 0, 0 }); + } } void TorrentsTunnel::Start () @@ -41,7 +49,7 @@ namespace torrents i2p::client::I2PService::Start (); m_DiskIOService.Start (); - auto dgramDest = GetLocalDestination ()->CreateDatagramDestination (false, i2p::datagram::eDatagramV3); + auto dgramDest = GetLocalDestination ()->CreateDatagramDestination (true, i2p::datagram::eDatagramV3); if (dgramDest) dgramDest->SetRawReceiver (std::bind (&TorrentsTunnel::HandleRecvFromI2PRaw, std::static_pointer_cast(shared_from_this ()), @@ -126,6 +134,20 @@ namespace torrents } } + void TorrentsTunnel::SaveTorrentFile (std::shared_ptr torrent) + { + if (!torrent) return; + auto torrentFilePath = torrent->GetFullPath (); torrentFilePath += ".torrent"; + std::ofstream f(torrentFilePath, std::ofstream::binary); + if (f) + { + auto content = torrent->CreateTorrentFileContent (); + f.write (content.data (), content.size ()); + } + else + LogPrint (eLogError, "TorrentsTunnel: Can't open ", torrentFilePath); + } + void TorrentsTunnel::InitTorrentFiles (std::shared_ptr torrent) { if (!torrent) return; @@ -307,20 +329,80 @@ namespace torrents if (m_Torrents.find (torrent->GetInfoHash ()) == m_Torrents.end ()) { torrent->SetFullPath (m_TorrentsDir/std::filesystem::path (torrent->GetName ())); - { - auto torrentFilePath = torrent->GetFullPath (); torrentFilePath += ".torrent"; - std::ofstream f(torrentFilePath, std::ofstream::binary); - if (f) - f.write (torrentFileContent.data (), torrentFileContent.size ()); - else - return { torrent, 0 }; - } - InitTorrentFiles (torrent); + std::string content (torrentFileContent); + boost::asio::post (GetDiskIOService (), [this, torrent, content]() + { + auto torrentFilePath = torrent->GetFullPath (); torrentFilePath += ".torrent"; + { + // TODO: replace by SaveTorrentFile + std::ofstream f(torrentFilePath, std::ofstream::binary); + if (f) + f.write (content.data (), content.size ()); + else + LogPrint (eLogError, "TorrentsTunnel: Can't open ", torrentFilePath); + } + InitTorrentFiles (torrent); + }); return { torrent, InsertTorrent (torrent) }; } return { torrent, 0 }; } + std::pair, int> TorrentsTunnel::AddMagnet (std::string_view magnet) + { + // magnet:?xt=urn:btih:&dn=&tr= + static constexpr std::string_view magnetPrefix { "magnet:?" }; +#if __cplusplus >= 202002L // C++20 + if (magnet.starts_with (magnetPrefix)) +#else + if (magnet.substr (0, magnetPrefix.size ()) == magnetPrefix) +#endif + { + Torrent::InfoHash infoHash; + bool isInfoHashFound = false; + magnet = magnet.substr (magnetPrefix.size ()); + while (!magnet.empty()) + { + auto pos = magnet.find ('&'); + std::string_view param; + if (pos != std::string::npos) + { + param = magnet.substr (0, pos); + magnet = magnet.substr (pos + 1); + } + else + { + param = magnet; + magnet = ""; + } + static constexpr std::string_view hashPrefix { "xt=urn:btih:" }; +#if __cplusplus >= 202002L // C++20 + if (param.starts_with (hashPrefix)) +#else + if (param.substr (0, hashPrefix.size ()) == hashPrefix) +#endif + { + std::string_view hexStr = param.substr (hashPrefix.size (), infoHash.size ()*2); + try + { + boost::algorithm::unhex (hexStr.begin(), hexStr.end(), infoHash.begin()); + isInfoHashFound = true; + } + catch (std::exception& ex) + { + LogPrint (eLogInfo, "TorentsTunnel: Can't unhex magnet hash ", hexStr); + } + } + } + if (isInfoHashFound && m_Torrents.find (infoHash) == m_Torrents.end ()) + { + auto torrent = std::make_shared (infoHash); + return { torrent, InsertTorrent (torrent) }; + } + } + return { nullptr, 0 }; + } + int TorrentsTunnel::InsertTorrent (std::shared_ptr torrent) { if (!torrent) return 0; @@ -439,6 +521,18 @@ namespace torrents return true; } + void TorrentsTunnel::UpdateTorrentInfo (std::shared_ptr torrent, std::string_view info) + { + if (!torrent) return; + torrent->ParseInfo (info); + torrent->SetFullPath (m_TorrentsDir/std::filesystem::path (torrent->GetName ())); + boost::asio::post (GetDiskIOService (), [this, torrent]() + { + SaveTorrentFile (torrent); + InitTorrentFiles (torrent); + }); + } + void TorrentsTunnel::Accept () { auto localDestination = GetLocalDestination (); @@ -473,7 +567,7 @@ namespace torrents if (!torrent) return; i2p::http::URL reqURL; if (trackerID < m_Trackers.size()) - reqURL.parse (m_Trackers[trackerID]); + reqURL.parse (std::get<0>(m_Trackers[trackerID])); else reqURL.parse (torrent->GetAnnounce ()); #if __cplusplus >= 202002L // C++20 @@ -487,7 +581,20 @@ namespace torrents } if (reqURL.schema == "udp") { - ConnectToDatagramTracker (torrent, trackerID, reqURL.host, reqURL.port, event); + if (trackerID >= m_Trackers.size()) return; + auto ts = i2p::util::GetMonotonicMilliseconds (); + auto& [announce, connectionID, expiration, fromPort] = m_Trackers[trackerID]; + if (connectionID && ts <= expiration) + SendAnnounceToDatagramTracker (trackerID, connectionID, torrent, reqURL.host, reqURL.port, fromPort, event); + else + { + if (!expiration || ts >= expiration) // don't connect if pending connection + { + connectionID = 0; + expiration = ts + DATAGRAM_TRACKER_TRANSACTION_TIMEOUT; + ConnectToDatagramTracker (trackerID, reqURL.host, reqURL.port); + } + } return; } std::map params; @@ -604,7 +711,7 @@ namespace torrents auto it = m_DatragramTrackerTransactions.begin (); while (it != m_DatragramTrackerTransactions.end ()) { - if (ts > (std::get<5>(it->second) + DATAGRAM_TRACKER_TRANSACTION_TIMEOUT)*1000LL) + if (ts > std::get<3>(it->second) + DATAGRAM_TRACKER_TRANSACTION_TIMEOUT) it = m_DatragramTrackerTransactions.erase (it); else it++; @@ -816,12 +923,21 @@ namespace torrents { uint32_t transactionID = bufbe32toh (buf); LogPrint (eLogDebug, "TorrentsTunnel: Datagram tracker action error response ", transactionID); - m_DatragramTrackerTransactions.erase (transactionID); - LogPrint (eLogInfo, "TorrentsTunnel: Datagram tracker error response: ", std::string_view ((const char *)(buf + 4), len - 4)); + auto it = m_DatragramTrackerTransactions.find (transactionID); + if (it == m_DatragramTrackerTransactions.end ()) + { + LogPrint (eLogInfo, "TorrentsTunnel: Datagram tracker transaction ", transactionID, " not found"); + return; + } + std::string_view error ((const char *)(buf + 4), len - 4); + LogPrint (eLogInfo, "TorrentsTunnel: Datagram tracker error response: ", error); + auto [trackerID, fromPort, torrent, ts] = it->second; + if (!torrent.expired ()) // response to announce + torrent.lock ()->SetTrackerError (trackerID, error); + m_DatragramTrackerTransactions.erase (it); } - void TorrentsTunnel::ConnectToDatagramTracker (std::shared_ptr torrent, - size_t trackerID, std::string_view dest, uint16_t port, TrackerAnnounceEvent event) + void TorrentsTunnel::ConnectToDatagramTracker (size_t trackerID, std::string_view dest, uint16_t port) { LogPrint (eLogDebug, "TorrentsTunnel: Connecting to datagram tracker ", dest, ":", port); auto address = i2p::client::context.GetAddressBook ().GetAddress (dest); @@ -840,10 +956,11 @@ namespace torrents auto session = dgramDest->GetSession (address->identHash); if (session) { - m_DatragramTrackerTransactions.emplace (transactionID, std::make_tuple (torrent, - trackerID, address->identHash, port, fromPort, i2p::util::GetMonotonicSeconds (), event)); + m_DatragramTrackerTransactions.emplace (transactionID, std::make_tuple ( + trackerID, fromPort, std::shared_ptr(nullptr), i2p::util::GetMonotonicMilliseconds ())); session->SetVersion (i2p::datagram::eDatagramV2); // send datagram2 dgramDest->SendDatagram (session, connectRequest, 16, fromPort, port); + dgramDest->FlushSendQueue (session); } else LogPrint (eLogInfo, "TorrentsTunnel: Can't obtain datagram session to ", dest); @@ -871,48 +988,69 @@ namespace torrents return; } uint64_t connectionID = bufbe64toh (buf + 4); - auto [torrent, trackerID, ident, port, fromPort, ts, event] = it->second; - if (!torrent.expired ()) - SendAnnounceToDatagramTracker (transactionID, connectionID, torrent.lock (), ident, port, fromPort, event); - else - m_DatragramTrackerTransactions.erase (it); + int lifetime = DATAGRAM_TRACKER_CONNECTION_EXPIRATION; + if (len >= 14) + { + lifetime = std::max (bufbe16toh (buf + 12)*1000, DATAGRAM_TRACKER_CONNECTION_EXPIRATION); + LogPrint (eLogDebug, "TorrentsTunnel: Datagram tracker connection lifetime set to ", lifetime/1000, " seconds"); + } + auto [trackerID, fromPort, torrent, ts] = it->second; + if (trackerID < m_Trackers.size ()) + { + auto& [announce, connID, connExpiration, connFromPort] = m_Trackers[trackerID]; + connID = connectionID; + connExpiration = i2p::util::GetMonotonicMilliseconds () + lifetime; + connFromPort = fromPort; + } + m_DatragramTrackerTransactions.erase (it); } - void TorrentsTunnel::SendAnnounceToDatagramTracker (uint32_t transactionID, - uint64_t connectionID, std::shared_ptr torrent, const i2p::data::IdentHash& ident, - uint16_t port, uint16_t fromPort, TrackerAnnounceEvent event) + void TorrentsTunnel::SendAnnounceToDatagramTracker (size_t trackerID, uint64_t connectionID, + std::shared_ptr torrent, std::string_view dest, uint16_t port, + uint16_t fromPort, TrackerAnnounceEvent event) { - auto localDestination = GetLocalDestination (); - auto dgramDest = localDestination->GetDatagramDestination (); - if (dgramDest) + LogPrint (eLogDebug, "TorrentsTunnel: Sending announce to datagram tracker ", dest, ":", port); + auto address = i2p::client::context.GetAddressBook ().GetAddress (dest); + if (address && address->IsIdentHash ()) { - uint8_t announce[98]; - htobe64buf (announce, connectionID); // connection_id - htobe32buf (announce + 8, eDatagramTrackerActionAnnounce); // action - htobe32buf (announce + 12, transactionID); // transaction_id - memcpy (announce + 16, torrent->GetInfoHash ().data (), 20); // info_hash - memcpy (announce + 36, m_PeerID.data (), 20); // peer_id - auto left = torrent->GetLeft (); - htobe64buf (announce + 56, torrent->GetLength () - left); // downloaded - htobe64buf (announce + 64, left); // left - htobe64buf (announce + 72, torrent->GetUploaded ()); // uploaded - htobe32buf (announce + 80, event); // event - htobe32buf (announce + 84, 0); // IP address 0:not used - htobe32buf (announce + 88, 0); // key, ignored - int numWant = 0; - if (!torrent->IsComplete () && (event == eTrackerAnnounceEventNone || event == eTrackerAnnounceEventStarted)) - numWant = TRACKER_MAX_NUM_WANT; - htobe32buf (announce + 92, numWant); // num_want - htobe16buf (announce + 96, fromPort); // from port - auto session = dgramDest->GetSession (ident); - if (session) + auto localDestination = GetLocalDestination (); + auto dgramDest = localDestination->GetDatagramDestination (); + if (dgramDest) { - session->SetVersion (i2p::datagram::eDatagramV3); // send datagram3 - dgramDest->SendDatagram (session, announce, 98, fromPort, port); + uint8_t announce[98]; + htobe64buf (announce, connectionID); // connection_id + htobe32buf (announce + 8, eDatagramTrackerActionAnnounce); // action + uint32_t transactionID = localDestination->GetRng()(); + htobe32buf (announce + 12, transactionID); // transaction_id + memcpy (announce + 16, torrent->GetInfoHash ().data (), 20); // info_hash + memcpy (announce + 36, m_PeerID.data (), 20); // peer_id + auto left = torrent->GetLeft (); + htobe64buf (announce + 56, torrent->GetLength () - left); // downloaded + htobe64buf (announce + 64, left); // left + htobe64buf (announce + 72, torrent->GetUploaded ()); // uploaded + htobe32buf (announce + 80, event); // event + htobe32buf (announce + 84, 0); // IP address 0:not used + htobe32buf (announce + 88, 0); // key, ignored + int numWant = 0; + if (!torrent->IsComplete () && (event == eTrackerAnnounceEventNone || event == eTrackerAnnounceEventStarted)) + numWant = TRACKER_MAX_NUM_WANT; + htobe32buf (announce + 92, numWant); // num_want + htobe16buf (announce + 96, fromPort); // from port + auto session = dgramDest->GetSession (address->identHash); + if (session) + { + m_DatragramTrackerTransactions.emplace (transactionID, std::make_tuple ( + trackerID, fromPort, torrent, i2p::util::GetMonotonicMilliseconds ())); + session->SetVersion (i2p::datagram::eDatagramV3); // send datagram3 + dgramDest->SendDatagram (session, announce, 98, fromPort, port); + dgramDest->FlushSendQueue (session); + } + else + LogPrint (eLogInfo, "TorrentsTunnel: Can't obtain datagram session"); } - else - LogPrint (eLogInfo, "TorrentsTunnel: Can't obtain datagram session"); } + else + LogPrint (eLogInfo, "TorrentsTunnel: Tracker not found: ", dest); } void TorrentsTunnel::HandleAnnounceResponse (const uint8_t * buf, size_t len) @@ -930,7 +1068,7 @@ namespace torrents LogPrint (eLogInfo, "TorrentsTunnel: Datagram tracker transaction ", transactionID, " not found"); return; } - auto [torrent, trackerID, ident, port, fromPort, ts, event] = it->second; + auto [trackerID, fromPort, torrent, ts] = it->second; if (!torrent.expired ()) { uint32_t interval = bufbe32toh (buf + 4); @@ -942,3 +1080,5 @@ namespace torrents } } } + +#endif // NO_TORRENTS diff --git a/libi2pd_client/TorrentsTunnel.h b/libi2pd_client/TorrentsTunnel.h index 80f7b915..1c4a942b 100644 --- a/libi2pd_client/TorrentsTunnel.h +++ b/libi2pd_client/TorrentsTunnel.h @@ -6,6 +6,8 @@ * See full license text in LICENSE file at top of project tree */ +#ifndef NO_TORRENTS + #ifndef TORRENTS_TUNNEL_H__ #define TORRENTS_TUNNEL_H__ @@ -32,7 +34,8 @@ namespace i2p namespace torrents { constexpr int TRACKER_RESPONSE_TIMEOUT = 8; // in seconds - constexpr int DATAGRAM_TRACKER_TRANSACTION_TIMEOUT = 60; // in seconds + constexpr int DATAGRAM_TRACKER_TRANSACTION_TIMEOUT = 10000; // in milliseconds + constexpr int DATAGRAM_TRACKER_CONNECTION_EXPIRATION = 60000; // in milliseconds constexpr int TRACKER_REQUESTS_CHECK_TIMEOUT = 1900; // in milliseconds constexpr int RECONNECT_CHECK_INTERVAL = 70; // in seconds constexpr int TRACKER_REQUESTS_INTERVAL_VARIANCE = 3000; // in milliseconds @@ -76,6 +79,9 @@ namespace torrents void Stop () { StopWorkAndFinishTasks (); } }; + using TrackerInfo = std::tuple; + // (announce, connection_id, connection expiration time in monotonic milliseconds, connction from_port) + public: TorrentsTunnel (std::string_view name, std::shared_ptr localDestination, @@ -86,11 +92,15 @@ namespace torrents auto& GetDiskIOService () { return m_DiskIOService.GetService (); }; const std::string& GetPeerID () const { return m_PeerID; } - const std::vector& GetTrackers () const { return m_Trackers; } + const std::filesystem::path& GetTorrentsDir () const { return m_TorrentsDir; } + std::string GetTrackerAnnounce (size_t id) const { return (id < m_Trackers.size ()) ? std::get<0>(m_Trackers[id]) : ""; } + size_t GetNumTrackers () const { return m_Trackers.size (); } std::shared_ptr FindTorrent (const Torrent::InfoHash& infoHash) const; std::shared_ptr FindTorrentByID (int id) const; std::vector GetTorrentIDs () const; - std::pair, int> AddTorrent (std::string_view torrentFileContent); // (tunnel, id) + std::pair, int> AddTorrent (std::string_view torrentFileContent); // retrun (torrent, id) + std::pair, int> AddMagnet (std::string_view magnet); // return (torrent, id) + void UpdateTorrentInfo (std::shared_ptr torrent, std::string_view info); // magnet bool RemoveTorrent (int id, bool deleteFiles); bool StopTorrent (int id); bool StartTorrent (int id); @@ -103,6 +113,7 @@ namespace torrents void Accept (); void ReadTorrentFile (const std::filesystem::path& torrentFilePath); + void SaveTorrentFile (std::shared_ptr torrent); void InitTorrentFiles (std::shared_ptr torrent); int InsertTorrent (std::shared_ptr torrent); // returns id > 0 if success and 0 if failed void RemoveTorrent (std::shared_ptr torrent, bool deleteFiles); @@ -134,32 +145,32 @@ namespace torrents void UpdateStats (); void HandleRecvFromI2PRaw (uint16_t fromPort, uint16_t toPort, const uint8_t * buf, size_t len); - void ConnectToDatagramTracker (std::shared_ptr torrent, size_t trackerID, - std::string_view dest, uint16_t port, TrackerAnnounceEvent event); + void ConnectToDatagramTracker (size_t trackerID, std::string_view dest, uint16_t port); void HandleConnectResponse (const uint8_t * buf, size_t len); void HandleErrorResponse (const uint8_t * buf, size_t len); void HandleAnnounceResponse (const uint8_t * buf, size_t len); - void SendAnnounceToDatagramTracker (uint32_t transactionID, uint64_t connectionID, - std::shared_ptr torrent, const i2p::data::IdentHash& ident, - uint16_t port, uint16_t fromPort, TrackerAnnounceEvent event); + void SendAnnounceToDatagramTracker (size_t trackerID, uint64_t connectionID, + std::shared_ptr torrent, std::string_view dest, uint16_t port, + uint16_t fromPort, TrackerAnnounceEvent event); private: std::string m_Name, m_PeerID; // 20 characters std::filesystem::path m_TorrentsDir; - std::vector m_Trackers; + std::vector m_Trackers; std::map > m_Torrents; std::map > m_TorrentsByID; mutable std::mutex m_TorrentsMutex; boost::asio::steady_timer m_TrackerRequestsCheckTimer, m_KeepAliveCheckTimer, m_ReconnectCheckTimer, m_TorrentsStatusUpdateTimer; DiskIOService m_DiskIOService; - std::unordered_map, size_t, i2p::data::IdentHash, - uint16_t, uint16_t, uint64_t, TrackerAnnounceEvent> > m_DatragramTrackerTransactions; - // transactionID->(torrent, trackerID, ident, port, fromPort, ts in monotonic seconds, event) + std::unordered_map, uint64_t > > m_DatragramTrackerTransactions; + // transactionID->(trackerID, from_port, torrent, timestamp monotonic milliseconds) }; } } #endif + +#endif // NO_TORRENTS diff --git a/libi2pd_client/UDPTunnel.cpp b/libi2pd_client/UDPTunnel.cpp index 6c509c08..a2076df3 100644 --- a/libi2pd_client/UDPTunnel.cpp +++ b/libi2pd_client/UDPTunnel.cpp @@ -470,15 +470,20 @@ namespace client m_IsUniqueLocal (true), m_Name (name), m_LocalAddress (localAddress), m_RemoteEndpoint (forwardTo), m_LocalDest (localDestination), m_inPort(inPort), m_Gzip (gzip) { + // without a reference the destination counts as unused and a config reload + // stops it, leaving the tunnel with a destination that receives nothing + if (m_LocalDest) m_LocalDest->Acquire (); } I2PUDPServerTunnel::~I2PUDPServerTunnel () { Stop (); + if (m_LocalDest) m_LocalDest->Release (); } void I2PUDPServerTunnel::Start () { + m_IsStarted = true; m_LocalDest->Start (); auto dgram = m_LocalDest->CreateDatagramDestination (m_Gzip); @@ -549,6 +554,11 @@ namespace client void I2PUDPServerTunnel::Stop () { + // a config reload builds a second tunnel object and drops it when the same + // one is already there. Its destructor must not reset the receiver of the + // live tunnel: they share the datagram destination + if (!m_IsStarted) return; + m_IsStarted = false; if (m_StatsTimer) m_StatsTimer->cancel (); if (m_CleanupTimer) m_CleanupTimer->cancel (); auto dgram = m_LocalDest->GetDatagramDestination (); @@ -594,15 +604,18 @@ namespace client m_ResolveThread (nullptr), m_LocalSocket (nullptr), RemotePort (remotePort), m_LastPort (0), m_cancel_resolve (false), m_Gzip (gzip), m_DatagramVersion (datagramVersion) { + if (m_LocalDest) m_LocalDest->Acquire (); } I2PUDPClientTunnel::~I2PUDPClientTunnel () { Stop (); + if (m_LocalDest) m_LocalDest->Release (); } void I2PUDPClientTunnel::Start () { + m_IsStarted = true; UDPConnection::Start (); // Reset flag in case of tunnel reload if (m_cancel_resolve) m_cancel_resolve = false; @@ -637,6 +650,8 @@ namespace client void I2PUDPClientTunnel::Stop () { + if (!m_IsStarted) return; // see the comment in I2PUDPServerTunnel::Stop + m_IsStarted = false; if (m_KeepAliveTimer) m_KeepAliveTimer->cancel (); if (m_StatsTimer) m_StatsTimer->cancel (); diff --git a/libi2pd_client/UDPTunnel.h b/libi2pd_client/UDPTunnel.h index 3b77615f..8c0e522d 100644 --- a/libi2pd_client/UDPTunnel.h +++ b/libi2pd_client/UDPTunnel.h @@ -194,6 +194,7 @@ namespace client UDPSessionPtr m_LastSession; uint16_t m_inPort; bool m_Gzip; + bool m_IsStarted = false; // a tunnel that never started shares nothing to tear down uint32_t m_NumRawNoSession = 0; size_t m_MaxWindow = I2P_UDP_MIN_MAX_NUM_UNACKED_DATAGRAMS; std::unique_ptr m_StatsTimer; @@ -264,6 +265,7 @@ namespace client uint16_t RemotePort, m_LastPort; bool m_cancel_resolve; bool m_Gzip; + bool m_IsStarted = false; // see the comment in I2PUDPServerTunnel i2p::datagram::DatagramVersion m_DatagramVersion; std::shared_ptr m_LastSession; uint32_t m_KeepAliveInterval = 0; diff --git a/tests/test-http-merge_chunked.cpp b/tests/test-http-merge_chunked.cpp index 31b6a298..bdba5c3b 100644 --- a/tests/test-http-merge_chunked.cpp +++ b/tests/test-http-merge_chunked.cpp @@ -21,5 +21,11 @@ int main() { assert(MergeChunkedResponse(in, out) == true); assert(out.str() == "HTTP response with \r\nchunks."); + /* a chunk that promises more bytes than the response carries */ + std::stringstream truncated("20\r\nshort\r\n"); + std::stringstream out2; + + assert(MergeChunkedResponse(truncated, out2) == false); + return 0; } diff --git a/tests/test-http-url.cpp b/tests/test-http-url.cpp index a5021c43..061c83ce 100644 --- a/tests/test-http-url.cpp +++ b/tests/test-http-url.cpp @@ -125,6 +125,12 @@ int main() { assert(url->frag == "frag"); delete url; + /* nothing after the schema or the user part: must not throw */ + url = new URL; + assert(url->parse("http://") == false); + assert(url->parse("http://user@") == false); + delete url; + return 0; }