/* * 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 */ #include #if BOOST_VERSION >= 108100 // boost::json since 1.75, we allow it since 1.81 due to std::string_view compatibility #include #define JSON_SUPPORTED #endif #include #include #include #include #include "Log.h" #include "Torrents.h" #include "TorrentsRPC.h" namespace i2p { namespace torrents { #ifdef JSON_SUPPORTED enum JSONRPCErrorCode { eMethodNotFound = -32601, eInvalidParam = -32602, eParseError = -32700 }; class JSONRPCHandler { public: JSONRPCHandler (std::shared_ptr tunnel): m_Tunnel (tunnel) {}; std::string HandleRequest (std::string_view request); private: std::string SuccessResponse (int64_t id, boost::json::object&& arguments); std::string ResultResponse (int64_t id, boost::json::object&& result); std::string ErrorResponse (JSONRPCErrorCode errorCode, int64_t id, std::string_view message); int64_t GetTag (boost::json::object& jsonRequest) const; static boost::json::value GetFieldValue (std::string_view field, std::shared_ptr torrent); boost::json::array GetPeers (std::shared_ptr torrent); static std::string_view RecognizeClientByPeerID (const PeerConnection::PeerID& peerID); std::string HandleTorrentAdd (boost::json::object&& jsonRequest); std::string HandleTorrentRemove (boost::json::object&& jsonRequest); std::string HandleTorrentGet (boost::json::object&& jsonRequest); // https://github.com/transmission/transmission/blob/main/docs/rpc-spec.md#2-message-format std::string HandleSessionGet (boost::json::object&& jsonRequest); // for transmission-rpc library private: std::shared_ptr m_Tunnel; }; std::string JSONRPCHandler::SuccessResponse (int64_t id, boost::json::object&& arguments) { boost::json::object response; response["result"] = "success"; response["arguments"] = arguments; if (id) response["id"] = id; return boost::json::serialize(response); } std::string JSONRPCHandler::ResultResponse (int64_t id, boost::json::object&& result) { boost::json::object response; response["jsonrpc"] = "2.0"; response["result"] = result; if (id) response["id"] = id; return boost::json::serialize(response); } std::string JSONRPCHandler::ErrorResponse (JSONRPCErrorCode errorCode, int64_t id, std::string_view message) { boost::json::object response; response["jsonrpc"] = "2.0"; if (id) response["id"] = id; boost::json::object error; error["code"] = errorCode; error["message"] = message; response["error"] = error; return boost::json::serialize(response); } int64_t JSONRPCHandler::GetTag (boost::json::object& jsonRequest) const { if (jsonRequest.contains ("tag")) return jsonRequest.at ("tag").as_int64 (); return 0; } std::string JSONRPCHandler::HandleRequest (std::string_view request) { try { auto jsonRequest = boost::json::parse (request).as_object (); auto method = jsonRequest.at ("method").as_string (); if (method == "torrent-add") return HandleTorrentAdd (std::move (jsonRequest)); else if (method == "torrent-remove") return HandleTorrentRemove (std::move (jsonRequest)); else if (method == "torrent-get") return HandleTorrentGet (std::move (jsonRequest)); else if (method == "session-get") return HandleSessionGet( std::move (jsonRequest)); else { LogPrint (eLogInfo, "TorrentsRPC: Method not found ", method); return ErrorResponse (eMethodNotFound, jsonRequest.at ("tag").as_int64 (), "Method not found"); } } catch (const std::exception& ex) { LogPrint (eLogInfo, "TorrentsRPC: Failed to parse JSON: ", ex.what ()); return ErrorResponse (eParseError, 0, "Parse error"); } return ""; } std::string JSONRPCHandler::HandleSessionGet (boost::json::object&& jsonRequest) { //todo // заглушка. наебываем систему тут, переделать потом на норм boost::json::object response, arguments; response["result"] = "success"; response["version"] = "4.0.0"; response["rpc-version"] = 17; arguments["version"] = "4.0.0"; // не нужно наверно response["arguments"] = arguments; response["tag"] = 0; return SuccessResponse (GetTag (jsonRequest), std::move (response)); } 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); boost::json::object response, torrentInfo; if (torrent) { 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 ()); } else { response["torrent-duplicate"] = torrentInfo; LogPrint (eLogDebug, "TorrentsRPC: duplicate torrent ", torrentInfo["name"].as_string ()); } return SuccessResponse (GetTag (jsonRequest), std::move (response)); } std::string JSONRPCHandler::HandleTorrentRemove (boost::json::object&& jsonRequest) { auto arguments = jsonRequest.at ("arguments").as_object (); bool deleteFiles = false; if (arguments.contains ("delete-local-data")) deleteFiles = arguments.at ("delete-local-data").as_bool (); std::vector torrentIds; if (arguments.contains ("ids")) { auto ids = arguments.at ("ids"); if (ids.is_array ()) for (const auto& it: ids.as_array ()) torrentIds.push_back (it.as_int64 ()); else torrentIds.push_back (ids.as_int64 ()); } for (auto id: torrentIds) m_Tunnel->RemoveTorrent (id, deleteFiles); boost::json::object response; // always empty return SuccessResponse (GetTag (jsonRequest), std::move (response)); } std::string JSONRPCHandler::HandleTorrentGet (boost::json::object&& jsonRequest) { auto arguments = jsonRequest.at ("arguments").as_object (); std::vector torrentIds; bool torrentsRequested = arguments.contains ("ids"); if (torrentsRequested) { auto ids = arguments.at ("ids"); if (ids.is_array ()) for (const auto& it: ids.as_array ()) torrentIds.push_back (it.as_int64 ()); else torrentIds.push_back (ids.as_int64 ()); } else torrentIds = m_Tunnel->GetTorrentIDs (); auto fields = arguments.at ("fields").as_array (); boost::json::object response; response["jsonrpc"] = "2.0"; boost::json::array torrents; for (auto id: torrentIds) { auto torrent = m_Tunnel->FindTorrentByID (id); if (torrent) { boost::json::object t; t["id"] = id; for (const auto& field: fields) { if (field == "id") continue; else if (field == "peers") t["peers"] = GetPeers (torrent); else { auto fieldValue = GetFieldValue (field.as_string (), torrent); if (!fieldValue.is_null ()) t[field.as_string ()] = fieldValue; } } torrents.push_back (t); } } response["torrents"] = torrents; return ResultResponse (GetTag (jsonRequest), std::move (response)); } boost::json::value JSONRPCHandler::GetFieldValue (std::string_view field, std::shared_ptr torrent) { const static std::map)> > fields = { { "name", [](std::shared_ptr torrent) { return boost::json::value(torrent->GetName ()); } }, { "status", [](std::shared_ptr torrent) { return boost::json::value(torrent->GetStatus ()); } }, { "isFinished", [](std::shared_ptr torrent) { return boost::json::value(torrent->IsComplete ()); } }, { "sizeWhenDone", [](std::shared_ptr torrent) { return boost::json::value(torrent->GetLength ()); } }, { "leftUntilDone", [](std::shared_ptr torrent) { return boost::json::value(torrent->GetLeft ()); } }, { "rateDownload", [](std::shared_ptr torrent) { return boost::json::value(torrent->GetDownloadRate ()); } }, { "rateUpload", [](std::shared_ptr torrent) { return boost::json::value(torrent->GetUploadRate ()); } }, { "peersGettingFromUs", [](std::shared_ptr torrent) { return boost::json::value(torrent->GetNumUploadingToPeers ()); } }, { "peersSendingToUs", [](std::shared_ptr torrent) { return boost::json::value(torrent->GetNumDownloadingFromPeers ()); } }, { "pieceCount", [](std::shared_ptr torrent) { return boost::json::value(torrent->GetNumPieces ()); } }, { "pieceSize", [](std::shared_ptr torrent) { return boost::json::value(torrent->GetPieceLength ()); } }, { "totalSize", [](std::shared_ptr torrent) { return boost::json::value(torrent->GetLength ()); } }, { "percentDone", [](std::shared_ptr torrent) { if(!torrent->GetLength()) return boost::json::value ( 100.0 ); double left = torrent->GetLength () - torrent->GetLeft(); double percent = (left*100)/torrent->GetLength(); return boost::json::value( percent ); } }, { "hashString", [](std::shared_ptr torrent) { std::string hexHash; boost::algorithm::hex (torrent->GetInfoHash ().begin(), torrent->GetInfoHash ().end(), std::back_inserter(hexHash)); return boost::json::value(hexHash); } }, { "pieces", [](std::shared_ptr torrent) { auto [bitfield, empry] = torrent->CreateBitfield (); std::string b64pieces; b64pieces.resize (boost::beast::detail::base64::encoded_size (bitfield.size())); boost::beast::detail::base64::encode (b64pieces.data (), bitfield.data(), bitfield.size()); 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; size_t numFiles = torrent->GetFiles ().size (); if (!numFiles) numFiles = 1; wanted.resize (numFiles); std::fill (wanted.begin(), wanted.end(), 1); return wanted; } }, { "priorities", [](std::shared_ptr torrent) { boost::json::array priorities; size_t numFiles = torrent->GetFiles ().size (); if (!numFiles) numFiles = 1; priorities.resize (numFiles); std::fill (priorities.begin(), priorities.end(), 0); return priorities; } }, { "error", [](std::shared_ptr torrent) { return boost::json::value(0); } }, // no error { "eta", [](std::shared_ptr torrent) { return boost::json::value(600); } }, // TODO: { "uploadRatio", [](std::shared_ptr torrent) { float ratio = torrent->GetDownloaded () ? ((float)torrent->GetUploaded ())/((float)torrent->GetDownloaded ()) : 100.0; return boost::json::value (ratio); } } }; if (torrent) { auto it = fields.find (field); if (it != fields.end ()) return it->second (torrent); } return boost::json::value (); } boost::json::array JSONRPCHandler::GetPeers (std::shared_ptr torrent) { boost::json::array peers; std::list > conns; boost::asio::post (m_Tunnel->GetService (), boost::asio::use_future ([tunnel = m_Tunnel, torrent, &conns]() { conns = tunnel->GetTorrentConnections (torrent); })).wait (); for (const auto& it: conns) { if (it->IsEstablished ()) { boost::json::object peer; std::string flags; auto stream = it->GetStream (); bool isIncoming = stream ? stream->IsIncoming () : false; if (isIncoming) flags.push_back ('I'); auto identHashStr = stream ? stream->GetRemoteIdentity ()->GetIdentHash ().ToBase64 () : ""; peer["address"] = identHashStr.substr (0, 4); peer["port"] = TORRENT_PORT; peer["identHash"] = identHashStr; peer["clientName"] = RecognizeClientByPeerID (it->GetRemotePeerID ()); peer["isDowloadingFrom"] = it->IsDownloading (); peer["isUploading_to"] = it->IsUploading (); peer["rateToClient"] = it->GetDownloadRate (); peer["rateToPeer"] = it->GetUploadRate (); peer["isIncoming"] = isIncoming; peer["clientIsChoked"] = it->IsChoked (); peer["peerIsChoked"] = it->IsRemoteChoked (); peer["clientIsIntersted"] = it->IsInterested (); peer["peerIsInterested"] = it->IsRemoteInterested (); peer["flagStr"] = flags; peer["progress"] = torrent->GetLength () ? ((float)it->GetDownloaded ())/(float)(torrent->GetLength ()) : 1.0; peers.push_back (peer); } } return peers; } 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 }; static const std::map twoChars = { { "qB", "qBittorrent" }, { "XD", "XD"}, { "BI", "BiglyBT" }, { "AZ", "Vuze" }, { "LT", "libtorrent" } }; if (peerID.size () >= i2psnark.size () && !memcmp (peerID.data (), i2psnark.data (), i2psnark.size ())) return "I2PSnark"; std::string_view peerIDStr ((const char *)peerID.data (), peerID.size ()); if (peerIDStr.substr (0, 6) == "-I2PD-") return "i2pd"; if (peerIDStr[0] == '-') { auto it = twoChars.find (peerIDStr.substr (1,2)); if (it != twoChars.end ()) return it->second; } return "Unknown"; } #endif TorrentsRPCSession::TorrentsRPCSession (TorrentsRPCServer& server, boost::asio::ip::tcp::socket&& s): m_Server (server), m_Socket (std::move (s)) { } void TorrentsRPCSession::ReceiveRequest () { boost::beast::http::async_read (m_Socket, m_ReceiveBuffer, m_Request, boost::beast::bind_front_handler (&TorrentsRPCSession::HandleRequest, shared_from_this())); } void TorrentsRPCSession::HandleRequest (const boost::system::error_code& ecode, size_t bytes_transferred) { if (!ecode) { 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" || 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 { LogPrint(eLogDebug, "TorrentRPC ", path, " not found ", tunnel); SendResponse (boost::beast::http::status::not_found); } } else SendResponse (boost::beast::http::status::method_not_allowed); } } void TorrentsRPCSession::SendResponse (boost::beast::http::status result, std::string_view data) { m_Response.version (11); // HTTP/1.1 m_Response.result (result); m_Response.set (boost::beast::http::field::server, "i2pd torents RPC"); m_Response.set(boost::beast::http::field::content_type, (result == boost::beast::http::status::ok) ? "application/json; charset=UTF-8" : "text/plain"); m_Response.body () = data; m_Response.prepare_payload (); boost::beast::http::async_write (m_Socket, m_Response, [s = shared_from_this ()](const boost::system::error_code& ecode, size_t bytes_transferred) { if (ecode) LogPrint (eLogWarning, "TorrentsRPC: Failed to send response: ", ecode.message ()); }); } TorrentsRPCServer::TorrentsRPCServer (std::string_view address, uint16_t port): RunnableServiceWithWork ("TRPC"), m_Acceptor (GetService (), boost::asio::ip::tcp::endpoint (boost::asio::ip::make_address (address), port)) { } void TorrentsRPCServer::Start () { if (!IsRunning ()) { StartIOService (); Accept (); } } void TorrentsRPCServer::Stop () { if (IsRunning ()) { m_Acceptor.cancel (); StopIOService (); } } void TorrentsRPCServer::AddTunnel (std::string_view path, std::shared_ptr tunnel) { std::string rpcPath ("/"); if (!path.empty ()) rpcPath += std::string (path) + "/"; rpcPath += "rpc/"; m_Tunnels.emplace (rpcPath, tunnel); } std::shared_ptr TorrentsRPCServer::GetTunnel (std::string_view path) const { auto it = m_Tunnels.find (path); if (it != m_Tunnels.end ()) return it->second.lock (); return nullptr; } void TorrentsRPCServer::Accept () { m_Acceptor.async_accept (boost::asio::make_strand (GetService ()), [this](const boost::system::error_code& ecode, boost::asio::ip::tcp::socket socket) { if (!ecode) { LogPrint (eLogDebug, "TorrentsRPC: Accepted incoming request"); auto session = std::make_shared(*this, std::move(socket)); session->ReceiveRequest (); Accept (); } }); } } }