diff --git a/contrib/torrents/web/index.html b/contrib/torrents/web/index.html new file mode 100644 index 00000000..a2302af1 --- /dev/null +++ b/contrib/torrents/web/index.html @@ -0,0 +1,314 @@ + + + +
+ Torrent Client +
+
+
+ + + + + diff --git a/contrib/torrents/web/torrent-rpc.js b/contrib/torrents/web/torrent-rpc.js new file mode 100644 index 00000000..db6c130d --- /dev/null +++ b/contrib/torrents/web/torrent-rpc.js @@ -0,0 +1,91 @@ +class TorrentClient { + getEndpoint() { + return `../rpc`; + } + + getStatusString(status) { + switch (status) { + case 4: return 'DOWNLOADING'; + case 6: return 'SEEDING'; + case 0: return 'STOPPED'; + case 2: return 'CHECKING'; + default: return 'QUEUED'; + } + } + + formatBytes(bytesPerSec) { + if (!bytesPerSec) return '0 B/s'; + if (bytesPerSec > 1024 * 1024) { + return (bytesPerSec / (1024 * 1024)).toFixed(2) + ' MB/s'; + } + return (bytesPerSec / 1024).toFixed(2) + ' KB/s'; + } + + async addTorrentByMagnet(magnet) + { + // TODO: check link + const magnetRegex = /^magnet:\?xt=urn:btih:[a-f0-9]{40}(&.+)?$/i; + if(!magnetRegex.test(magnet)) return false; + const res = await fetch(this.getEndpoint(), { + method: "POST", + body: JSON.stringify({ 'method': 'torrent-add', 'arguments': { 'filename': magnet } }), + headers: { 'Content-Type': 'application/json' } + }); + return res.json(); + + } + async addTorrent(b64metainfo) { + const res = await fetch(this.getEndpoint(), { + method: "POST", + body: JSON.stringify({ 'method': 'torrent-add', 'arguments': { 'metainfo': b64metainfo } }), + headers: { 'Content-Type': 'application/json' } + }); + return res.json(); + } + + async removeTorrent(id) { + const res = await fetch(this.getEndpoint(), { + method: "POST", + body: JSON.stringify({ 'method': 'torrent-remove', 'arguments': { 'ids': [id], 'delete-local-data': true } }), + headers: { 'Content-Type': 'application/json' } + }); + return res.json(); + } + + async getTorrents() { + const res = await fetch(this.getEndpoint(), { + method: "POST", + body: JSON.stringify({ + 'method': 'torrent-get', + 'arguments': { "fields": ["id", "name", "status", "rateDownload", "rateUpload", "totalSize", "percentDone", "files", "peers", "trackers", "trackerStats","hashString"] } + }), + headers: { 'Content-Type': 'application/json' } + }); + return res.json(); + } + async stopTorrent(ids = []) { + const res = await fetch(this.getEndpoint(), { + method: "POST", + body: JSON.stringify({ + 'method': 'torrent-stop', + 'arguments': ids.length ? { ids } : {}, + 'tag': 666 + }), + headers: { 'Content-Type': 'application/json' } + }); + return res.json(); +} + +async startTorrent(ids = []) { + const res = await fetch(this.getEndpoint(), { + method: "POST", + body: JSON.stringify({ + 'method': 'torrent-start', + 'arguments': ids.length ? { ids } : {}, + 'tag': 666 + }), + headers: { 'Content-Type': 'application/json' } + }); + return res.json(); +} +} \ No newline at end of file diff --git a/daemon/HTTPServer.cpp b/daemon/HTTPServer.cpp index f174af20..125fc519 100644 --- a/daemon/HTTPServer.cpp +++ b/daemon/HTTPServer.cpp @@ -1210,20 +1210,56 @@ namespace http { s << "\r\n
\r\n"; } #ifndef NO_TORRENTS + auto findTunnelEndpoint = [](const std::string& tunnelname) -> std::pair + { + LogPrint(eLogDebug,"HTTPServer: GetTorrentsRPC"); + for (auto& [port, server] : i2p::client::context.GetTorrentsRPCServers()) + { + if (!server) + { + LogPrint(eLogError,"HTTPServer: GetTorrentsRPCServers return nothing"); + continue; + } + for (const auto& [rpcPath, weakTunnel] : server->GetTunnels()) + { + if (auto tunnel = weakTunnel.lock()) + { + LogPrint(eLogDebug, "HTTPServer: TUNNEL_OPENJS", tunnel->GetName()); + if (tunnel->GetName() == tunnelname) + { + uint16_t rpcPort = server->GetAcceptor().local_endpoint().port(); + return { rpcPort, rpcPath }; + } + } + } + } + return { 0, "" }; + }; + auto& torrentsTunnels = i2p::client::context.GetTorrentsTunnels (); if (!torrentsTunnels.empty ()) { s << "
\r\n" << tr("Torrents") << ":
\r\n
\r\n"; for (auto& it: torrentsTunnels) { - auto& ident = it.second->GetLocalDestination ()->GetIdentHash(); s << "
"; s << it.second->GetName () << " ⇐ "; s << i2p::client::context.GetAddressBook ().ToAddress(ident); - s << " | Open Torrent Client for this Tunnel"; - s << "
\r\n"<< std::endl; + auto [rpcPort, rpcPath] = findTunnelEndpoint(it.second-> GetName()); + if (rpcPort && rpcPath.length() && it.second-> SupportsWeb()) { + auto webPath = rpcPath; + webPath.erase(webPath.size() - 4); // removing "/rpc" suffix + + std::string jsRedir = "this.href = location.protocol + '//' + location.hostname + ':' + " + std::to_string(rpcPort) + " + '" + webPath + "/web/'"; + + s << "" + << " | Web Panel"; + } + s << "
\r\n"<< std::endl; } s << "\r\n
\r\n"; } diff --git a/libi2pd_client/TorrentsRPC.cpp b/libi2pd_client/TorrentsRPC.cpp index d2614dda..6cb12992 100644 --- a/libi2pd_client/TorrentsRPC.cpp +++ b/libi2pd_client/TorrentsRPC.cpp @@ -946,13 +946,15 @@ namespace torrents subUrlPath++; if (subUrlPath != urlPath.end ()) { - auto tunnel = m_Server.GetTunnel (subUrlPath->string () + "/rpc"); + auto tunnel = m_Server.GetTunnel (std::string ("/") + subUrlPath->string () + "/rpc"); if (tunnel && tunnel->SupportsWeb ()) { subUrlPath++; if (subUrlPath != urlPath.end ()) isPathValid = *subUrlPath == "web"; } + else + LogPrint (eLogDebug, "TorrentsRPC: Tunnel not found or web is not supported ", subUrlPath->string ()); } } if (!isPathValid)