From 3b169793c3211aed174f8ec2a0d71d2ed450e3e8 Mon Sep 17 00:00:00 2001
From: wipedlifepotato <60944239+wipedlifepotato@users.noreply.github.com>
Date: Mon, 24 Aug 2026 16:22:33 +0300
Subject: [PATCH] torrent-rpc-client-page
---
contrib/webconsole/torrent-rpc.js | 67 ++++
contrib/webconsole/torrent_client.html | 280 +++++++++++++
daemon/HTTPServer.cpp | 519 +++----------------------
libi2pd_client/ClientContext.h | 1 +
libi2pd_client/TorrentsRPC.cpp | 12 +
libi2pd_client/TorrentsRPC.h | 4 +
6 files changed, 420 insertions(+), 463 deletions(-)
create mode 100644 contrib/webconsole/torrent-rpc.js
create mode 100644 contrib/webconsole/torrent_client.html
diff --git a/contrib/webconsole/torrent-rpc.js b/contrib/webconsole/torrent-rpc.js
new file mode 100644
index 00000000..23bb109d
--- /dev/null
+++ b/contrib/webconsole/torrent-rpc.js
@@ -0,0 +1,67 @@
+class TorrentClient {
+ constructor(host = '127.0.0.1', port = 9191, path = 'mytorrents') {
+ this.host = host;
+ this.port = port;
+ this.path = path;
+ }
+
+ setConnection(host, port, path) {
+ this.host = host;
+ this.port = port;
+ this.path = path;
+ }
+
+ getEndpoint() {
+ return `http://${this.host}:${this.port}${this.path}`;
+ }
+
+ 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 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"] }
+ }),
+ headers: { 'Content-Type': 'application/json' }
+ });
+ return res.json();
+ }
+}
+
+//window.tjs = new TorrentClient();
diff --git a/contrib/webconsole/torrent_client.html b/contrib/webconsole/torrent_client.html
new file mode 100644
index 00000000..d3792fc9
--- /dev/null
+++ b/contrib/webconsole/torrent_client.html
@@ -0,0 +1,280 @@
+
+
+
+
+
+ TORRENT_DAEMON_INTERFACE
+ CONNECTING...
+
+
+
+ Drop torrent payload here or browse file
+
+
+
+
+
+
+
+ | ID |
+ Name |
+ Status |
+ Progress |
+ Download Rate |
+ Actions |
+
+
+
+
+ | No active transfers |
+
+
+
+
+
+
+
+
diff --git a/daemon/HTTPServer.cpp b/daemon/HTTPServer.cpp
index dd3cc941..2f1f09f6 100644
--- a/daemon/HTTPServer.cpp
+++ b/daemon/HTTPServer.cpp
@@ -92,8 +92,7 @@ namespace http {
const char HTTP_COMMAND_RELOAD_CSS[] = "reload_css";
const char HTTP_COMMAND_EXPIRELEASE[] = "expirelease";
- const char HTTP_PAGE_JSTORRENTCLIENT[] = "js_torr_client";
-
+ const char OPENJS_TORRENT_CLIENT_PAGE[] = "torrent_client_open";
static std::string ConvertTime (uint64_t time)
{
struct tm caltime;
@@ -1196,11 +1195,14 @@ namespace http {
s << "
\r\n" << tr("Torrents") << ":
\r\n\r\n";
for (auto& it: torrentsTunnels)
{
+
auto& ident = it.second->GetLocalDestination ()->GetIdentHash();
s << "
\r\n"<< std::endl;
+
}
s << "
\r\n
\r\n";
}
@@ -1395,469 +1397,59 @@ namespace http {
ShowSAMSession (s, params["sam_id"]);
else if (page == HTTP_PAGE_I2P_TUNNELS)
ShowI2PTunnels (s);
+ else if (page == OPENJS_TORRENT_CLIENT_PAGE)
+ {
+ // todo: to an another method with return;
+ std::string tunnelname = params["tunnelname"];
+ auto findTunnelEndpoint = [](const std::string& tunnelname) -> std::pair {
+ for (auto& [port, server] : i2p::client::context.GetTorrentsRPCServers())
+ {
+ if (!server) continue;
+ for (const auto& [rpcPath, weakTunnel] : server->GetTunnels())
+ {
+ if (auto tunnel = weakTunnel.lock())
+ {
+ LogPrint(eLogDebug, "HTTP SERVER DFASDFASDF tunnel", tunnel->GetName());
+ if (tunnel->GetName() == tunnelname)
+ {
+
+ uint16_t rpcPort = server->GetAcceptor().local_endpoint().port();
+ return std::pair(rpcPort, rpcPath);
+ }
+ }
+ }
+ }
+ return std::pair(0, "");
+ };
+ auto [rpcPort, rpcPath] = findTunnelEndpoint(tunnelname);
+ if (rpcPort && rpcPath.length())
+ {
+ std::string tjs_file = i2p::fs::DataDirPath("webconsole/torrent-rpc.js");
+ std::string tclient_file = i2p::fs::DataDirPath("webconsole/torrent_client.html");
+ if (i2p::fs::Exists(tjs_file)) {
+ std::ifstream f(tjs_file, std::ifstream::binary);
+ std::stringstream tjsss;
+ tjsss << f.rdbuf();
+ s << "\r\n";
+ s << "\r\n";
+ }
+ else {
+ s << "Not found $datadir/webconsole/torrent-rpc.js
\r\n";
+ }
+ if (i2p::fs::Exists(tclient_file)) {
+ std::ifstream f(tclient_file, std::ifstream::binary);
+ s << f.rdbuf() << "\r\n";
+ }
+ else {
+ s << "Not found $datadir/webconsole/torrent_client.html
\r\n";
+ }
+ } else {
+ s << "Not found rpc tunnel " << tunnelname << "
\r\n";
+ }
+ }
else if (page == HTTP_PAGE_LEASESETS)
ShowLeasesSets(s);
- else if (page == HTTP_PAGE_JSTORRENTCLIENT)
- {
- // Prepare a code. For add torrent. Content-Security-Policy also need
- s << R"TJS(
-
-
-
-
-
-
- Torrent Client Daemon
-
-
-
-
-
-
- TORRENT_DAEMON_INTERFACE
- CONNECTING...
-
-
-
-
-
- Drop torrent payload here or browse file
-
-
-
-
-
-
-
- | ID |
- Name |
- Status |
- Progress |
- Download Rate |
- Actions |
-
-
-
-
- | No active transfers |
-
-
-
-
-
-
-
-
-
-
- )TJS";
- // TODO: or withous JS though torrent class. Example of usage the library -> https://github.com/wipedlifepotato/NeonI2P_JS/blob/master/Example/torrent_client.html
- }
- else {
+ else {
res.code = 400;
ShowError(s, std::string (tr("Unknown page")) + ": " + page); // TODO
return;
@@ -1891,6 +1483,7 @@ namespace http {
i2p::context.SetAcceptsTunnels (true);
else if (cmd == HTTP_COMMAND_DISABLE_TRANSIT)
i2p::context.SetAcceptsTunnels (false);
+
else if (cmd == HTTP_COMMAND_SHUTDOWN_START)
{
i2p::context.SetAcceptsTunnels (false);
diff --git a/libi2pd_client/ClientContext.h b/libi2pd_client/ClientContext.h
index ca2253f9..a61dda38 100644
--- a/libi2pd_client/ClientContext.h
+++ b/libi2pd_client/ClientContext.h
@@ -189,6 +189,7 @@ namespace client
const decltype(m_ClientForwards)& GetClientForwards () const { return m_ClientForwards; }
const decltype(m_ServerForwards)& GetServerForwards () const { return m_ServerForwards; }
const decltype(m_TorrentsTunnels)& GetTorrentsTunnels () const { return m_TorrentsTunnels; }
+ const decltype(m_TorrentsRPCServers)& GetTorrentsRPCServers () const { return m_TorrentsRPCServers; }
std::shared_ptr GetHttpProxy () const
{
std::lock_guard l(m_ProxyMutex);
diff --git a/libi2pd_client/TorrentsRPC.cpp b/libi2pd_client/TorrentsRPC.cpp
index ca139fe5..80bbc79d 100644
--- a/libi2pd_client/TorrentsRPC.cpp
+++ b/libi2pd_client/TorrentsRPC.cpp
@@ -511,6 +511,18 @@ namespace torrents
m_Tunnels.emplace (rpcPath, tunnel);
}
+ std::pair TorrentsRPCServer::GetTunnelEndpoint (std::shared_ptr tunnel) const
+ {
+ for (const auto& [path, weakTunnel] : m_Tunnels)
+ {
+ if (auto t = weakTunnel.lock(); t == tunnel)
+ {
+ return { m_Acceptor.local_endpoint(), path };
+ }
+ }
+ return {};
+ }
+
std::shared_ptr TorrentsRPCServer::GetTunnel (std::string_view path) const
{
auto it = m_Tunnels.find (path);
diff --git a/libi2pd_client/TorrentsRPC.h b/libi2pd_client/TorrentsRPC.h
index 3215ae35..592eeeb1 100644
--- a/libi2pd_client/TorrentsRPC.h
+++ b/libi2pd_client/TorrentsRPC.h
@@ -59,6 +59,10 @@ namespace torrents
void AddTunnel (std::string_view path, std::shared_ptr tunnel);
std::shared_ptr GetTunnel (std::string_view path) const;
+ std::pair GetTunnelEndpoint (std::shared_ptr tunnel) const;
+
+ const auto& GetTunnels() const { return m_Tunnels; }
+ const auto& GetAcceptor() const { return m_Acceptor; }
private:
void Accept ();