this transmission-rpc adaptation is still a little broken, but it'll be ready soon.

This commit is contained in:
idk
2020-01-25 00:06:28 -05:00
parent 47760ab1df
commit 2526de4b97
13 changed files with 520 additions and 142 deletions

View File

@@ -1,14 +1,14 @@
I2P in Private Browsing Mode(Firefox-Only)
==========================================
This is an **Experimental** webextension which introduces a set of new "Private
Browsing" modes to Firefox-based browsers(Supporting webextensions) that makes
it easier to configure a browser to use I2P securely and adds features for
making I2P applications easier to use. It does this by isolating I2P-specific
settings to Contextual Identities within Firefox, then loading them
automatically when the user requests them. It also adds convenience and
management features specific to I2P like protocol handlers and native messaging
systems.
This is an webextension which introduces a set of new "Private Browsing" modes
to Firefox-based browsers(Supporting webextensions) that makes it easier to
configure a browser to use I2P securely and adds features for making I2P
applications easier to use. It does this by isolating I2P-specific settings to
Contextual Identities within Firefox, then loading them automatically when the
user requests them. It also adds convenience and management features, like an
embedded I2P console and Bittorrent integration with clients using the
transmission-rpc API.
Installation(Cross-Platform):
-----------------------------

View File

@@ -93,7 +93,7 @@ gettingInfo.then(got => {
let port = info.value.http.split(":")[1];
if (port == "7644") {
var createBookmark = browser.bookmarks.create({
url: "http://localhost:7647/i2ptunnelmgr",
url: "http://localhost:7647/i2ptunnel",
title: "Hidden Services Manager",
parentId: bookmarkToolbar[0].id
});
@@ -101,11 +101,7 @@ gettingInfo.then(got => {
} else {
var createRhizomeBookmark = browser.bookmarks.create({
url:
"http://" +
control_host +
":" +
control_port +
"/i2ptunnelmgr",
"http://" + control_host + ":" + control_port + "/i2ptunnel",
title: "Hidden Services Manager",
parentId: bookmarkToolbar[0].id
});

View File

@@ -87,19 +87,23 @@
</li>-->
<li class="application">
<a class="applicationName" href="toopie.html" id="window-visit-toopie" target="_blank">Toopie</a> <span class="applicationDesc" id="toopie">For information about your I2P router status, go here:</span>
<a class="applicationName" href="toopie.html" id="window-visit-toopie">Toopie</a> <span class="applicationDesc" id="toopie">For information about your I2P router status, go here:</span>
</li>
<li class="application">
<a class="applicationName" href="http://127.0.0.1:7657/i2ptunnel" id="window-visit-i2ptunnel" target="_blank">Hidden Services Manager</a> <span class="applicationDesc" id="i2ptunnel">I2P has a web-based interface for configuring .i2p services like web sites, to set up your own web sites, go here:</span>
<a class="applicationName" href="http://127.0.0.1:7657/" id="window-visit-router">Router Console</a> <span class="applicationDesc" id="routerconsole">The entrypoint for all other I2P applications is the I2P Router Console. To visit it, click here.</span>
</li>
<li class="application">
<a class="applicationName" href="http://127.0.0.1:7657/susimail" id="window-visit-susimail" target="_blank">E-Mail</a> <span class="applicationDesc" id="susimail">I2P also bundles a webmail client which can be used to access in-I2P e-mail. To use it, go here:</span>
<a class="applicationName" href="http://127.0.0.1:7657/i2ptunnel" id="window-visit-i2ptunnel">Hidden Services Manager</a> <span class="applicationDesc" id="i2ptunnel">I2P has a web-based interface for configuring .i2p services like web sites, to set up your own web sites, go here:</span>
</li>
<li class="application">
<a class="applicationName" href="http://127.0.0.1:7657/i2psnark" id="window-visit-snark" target="_blank">BitTorrent</a> <span class="applicationDesc" id="snark">I2P is capable of anonymous Peer-to-Peer file sharing, to use the built-in bittorrent client go here:</span>
<a class="applicationName" href="http://127.0.0.1:7657/susimail" id="window-visit-susimail">E-Mail</a> <span class="applicationDesc" id="susimail">I2P also bundles a webmail client which can be used to access in-I2P e-mail. To use it, go here:</span>
</li>
<li class="application">
<a class="applicationName" href="http://127.0.0.1:7657/i2psnark" id="window-visit-snark">BitTorrent</a> <span class="applicationDesc" id="snark">I2P is capable of anonymous Peer-to-Peer file sharing, to use the built-in bittorrent client go here:</span>
</li>
</ul>

Binary file not shown.

View File

@@ -87,7 +87,7 @@ document.addEventListener("click", clickEvent => {
showBrowsing();
} else if (clickEvent.target.id === "torrent-action") {
console.log("showing a torrent action");
showTorrents();
showTorrentsMenu();
} else if (clickEvent.target.id === "window-preface-title") {
console.log("attempting to create homepage tab");
goHome();
@@ -151,7 +151,7 @@ function showBrowsing() {
y.style.display = "none";
}
function showTorrents() {
function showTorrentsMenu() {
var x = document.getElementById("browserpanel");
x.style.display = "none";
var y = document.getElementById("torrentpanel");

View File

@@ -7,8 +7,10 @@
},
"permissions": [
"theme",
"alarms",
"browsingData",
"bookmarks",
"contextMenus",
"management",
"notifications",
"proxy",
@@ -48,7 +50,10 @@
"page": "options/options.html"
},
"background": {
"persistent": true,
"scripts": [
"torrent/common.js",
"torrent/background.js",
"config.js",
"i2pcontrol/i2pcontrol.js",
"host.js",

View File

@@ -19,7 +19,7 @@
</select>
</section>
<section class="scheme-options proxy-options">
<section class="scheme-options proxy-options" id="proxy-options">
<div class="title">
Proxy Options
</div>
@@ -36,7 +36,7 @@
</div>
</section>-->
<section class="scheme-options control-options">
<section class="scheme-options console-options" id="console-options">
<div>
<div class="title">
Router Console Options
@@ -49,7 +49,7 @@
</div>
</section>
<section class="scheme-options control-options">
<section class="scheme-options control-options" id="control-options">
<div>
<div class="title">
I2PControl RPC Client Options
@@ -66,23 +66,24 @@
</div>
</section>
<section class="scheme-options control-options">
<section class="scheme-options transmission-options" id="transmission-options">
<div>
<div class="title">
Bittorrent RPC Client Options
</div>
<p id="rpcHelpText">Configure your Bittorrent options here.</p>
<label id="btRpcPortText">Torrent RPC Host:</label> <input data="btrpchost" id="btrpchost" type="text" value="127.0.0.1">
<label id="btRpcHostText">Torrent RPC Host:</label> <input data="btrpchost" id="btrpchost" type="text" value="127.0.0.1">
<br>
<label id="btRpcHostText">Torrent RPC Port:</label> <input data="btrpcport" id="btrpcport" type="text" value="7657">
<label id="btRpcPortText">Torrent RPC Port:</label> <input data="btrpcport" id="btrpcport" type="text" value="7657">
<br>
<label id="btRpcPathText">Torrent RPC Path:</label> <input data="btrpcpath" id="btrpcpath" type="text" value="transmission/rpc">
<label id="btRpcPathText">Torrent RPC Path:</label> <input data="btrpcpath" id="btrpcpath" type="text" value="transmission/">
<br>
<label id="rpcPassText">Torrent RPC Password:</label> <input data="btrpcpass" id="btrpcpass" type="text" value="itoopie">
<label id="rpcPassText">Torrent RPC Password:</label> <input data="btrpcpass" id="btrpcpass" type="text" value="">
</div>
</section>
<input id="save-button" type="button" value="Save preferences">
<script src="options.js"></script>
<script src="options.js"></script> <!--<script src="/torrent/browser-polyfill.min.js"></script>
<script src="/torrent/options.js"></script>-->
</body>
</html>

View File

@@ -217,14 +217,23 @@ function checkStoredSettings(storedSettings) {
} else defaultSettings["bt_rpc_pass"] = storedSettings.bt_rpc_pass;
console.log("(options)(browserinfo) NATIVE PROXYSETTINGS", info.value);
defaultSettings["base_url"] =
"http://" +
defaultSettings["bt_rpc_host"] +
":" +
defaultSettings["bt_rpc_port"] +
"/" +
defaultSettings["bt_rpc_path"];
console.log(
"(options)",
defaultSettings["proxy_sheme"],
defaultSettings["proxy_scheme"],
defaultSettings["proxy_host"],
defaultSettings["proxy_port"],
defaultSettings["control_host"],
defaultSettings["control_port"]
defaultSettings["control_port"],
defaultSettings["base_url"]
);
chrome.storage.local.set(defaultSettings);
return defaultSettings;
}
@@ -323,6 +332,8 @@ function storeSettings() {
let bt_rpc_port = getBTRPCPort();
let bt_rpc_path = getBTRPCPath();
let bt_rpc_pass = getBTRPCPass();
let base_url =
"http://" + bt_rpc_host + ":" + bt_rpc_port + "/" + bt_rpc_path;
chrome.storage.local.set({
proxy_scheme,
proxy_host,

247
torrent/background.js Normal file
View File

@@ -0,0 +1,247 @@
"use strict";
////// Session extraction
function setupExtractor() {
browser.webRequest.onHeadersReceived.removeListener(extractSession);
browser.storage.local.get("server").then(({ server }) => {
if (!server) {
return;
}
console.log("Session extractor setup for", server.base_url);
browser.webRequest.onBeforeSendHeaders.addListener(
extractSession,
{ urls: [server.base_url + "*"] },
["requestHeaders"]
);
});
}
setupExtractor();
function extractSession(requestDetails) {
const hdr = requestDetails.requestHeaders.filter(
x => x.name.toLowerCase() === "x-transmission-session-id"
)[0];
if (!hdr) {
return;
}
browser.storage.local.get("server").then(({ server }) => {
server.session = hdr.value;
browser.storage.local.set({ server });
});
}
////// Adding
function blobToBase64(blob) {
return new Promise((resolve, reject) => {
const rdr = new FileReader();
rdr.onload = () => resolve(rdr.result.substr(rdr.result.indexOf(",") + 1));
rdr.onerror = reject;
rdr.readAsDataURL(blob);
});
}
function addUrl(torrentUrl, downloadDir) {
let p,
params = {};
if (downloadDir) {
params = { "download-dir": downloadDir };
}
if (torrentUrl.startsWith("magnet:")) {
console.log("Adding magnet", torrentUrl);
params.filename = torrentUrl;
p = rpcCall("torrent-add", params);
} else {
// Download the torrent file *in the browser* to support private torrents
console.log("Downloading torrent", torrentUrl);
p = fetch(torrentUrl, {
method: "GET",
credentials: "include"
})
.then(resp => {
if (resp.ok) {
return resp.blob();
}
throw new Error("Could not download torrent");
})
.then(blobToBase64)
.then(b64 => {
params.metainfo = b64;
return rpcCall("torrent-add", params);
});
}
return p.then(x => {
updateBadge();
return x;
});
}
////// magnet: Handler
function handleUrl(requestDetails) {
return addUrl(
decodeURIComponent(
requestDetails.url.replace("http://transmitter.web-extension/", "")
)
).then(x => {
return browser.storage.local.get("server").then(({ server }) => {
return { redirectUrl: server.base_url + "web/" };
});
});
}
browser.webRequest.onBeforeRequest.addListener(
handleUrl,
{ urls: ["http://transmitter.web-extension/*"] },
["blocking"]
);
////// Context menu
function createContextMenu() {
browser.storage.local.get("server").then(({ server }) => {
browser.contextMenus.removeAll();
if (!server || !server.locations || !server.locations.length) {
browser.contextMenus.create({
id: "transmitter-add",
title: "Download with Transmission remote",
contexts: ["link"]
});
} else {
browser.contextMenus.create({
id: "transmitter-add",
title: "Download to Default location",
contexts: ["link"]
});
server.locations.forEach(location => {
browser.contextMenus.create({
id: "transmitter-add-loc-" + location.index,
title: "Download to " + location.name,
contexts: ["link"]
});
});
}
});
}
createContextMenu();
browser.contextMenus.onClicked.addListener((info, tab) => {
if (info.menuItemId === "transmitter-add") {
return addUrl(info.linkUrl);
} else if (info.menuItemId.startsWith("transmitter-add-loc-")) {
let index = parseInt(info.menuItemId.substr("transmitter-add-loc-".length));
browser.storage.local.get("server").then(({ server }) => {
let path = server.locations[index].path;
addUrl(info.linkUrl, path);
});
}
});
////// Badge
function updateBadge() {
browser.storage.local.get("server").then(({ server }) => {
if (
server.badge !== "num" &&
server.badge !== "dl" &&
server.badge !== "ul" &&
server.badge !== "auto"
) {
return;
}
return rpcCall("session-stats", {}).then(response => {
const args = response.arguments; // lol the name 'arguments' means destructuring in strict mode is impossible
switch (server.badge) {
case "num":
browser.browserAction.setBadgeBackgroundColor({ color: "gray" });
browser.browserAction.setBadgeText({
text: "" + args.activeTorrentCount
});
break;
case "dl":
browser.browserAction.setBadgeBackgroundColor({ color: "green" });
browser.browserAction.setBadgeText({
text: formatSpeed(args.downloadSpeed)
});
break;
case "ul":
browser.browserAction.setBadgeBackgroundColor({ color: "blue" });
browser.browserAction.setBadgeText({
text: formatSpeed(args.uploadSpeed)
});
break;
case "auto":
if (args.downloadSpeed > 0) {
browser.browserAction.setBadgeBackgroundColor({ color: "green" });
browser.browserAction.setBadgeText({
text: formatSpeed(args.downloadSpeed)
});
} else if (args.uploadSpeed > 0) {
browser.browserAction.setBadgeBackgroundColor({ color: "blue" });
browser.browserAction.setBadgeText({
text: formatSpeed(args.uploadSpeed)
});
} else {
browser.browserAction.setBadgeBackgroundColor({ color: "gray" });
browser.browserAction.setBadgeText({
text: "" + args.activeTorrentCount
});
}
break;
}
});
});
}
browser.alarms.onAlarm.addListener(alarm => {
if (alarm.name === "transmitter-badge-update") {
return updateBadge();
}
});
function setupBadge() {
browser.alarms.clear("transmitter-badge-update").then(x => {
browser.storage.local.get("server").then(({ server }) => {
if (!server) {
return;
}
browser.alarms.create("transmitter-badge-update", {
periodInMinutes: parseInt(server.badge_interval || "1")
});
updateBadge();
});
});
}
setupBadge();
////// Storage updates
browser.storage.onChanged.addListener((changes, area) => {
if (!Object.keys(changes).includes("server")) {
return;
}
const oldv = changes.server.oldValue;
const newv = changes.server.newValue;
if (
!oldv ||
oldv.base_url !== newv.base_url ||
oldv.username !== newv.username ||
oldv.password !== newv.password ||
oldv.badge_interval !== newv.badge_interval ||
oldv.badge !== newv.badge ||
arraysEqualDeep(oldv.locations, newv.locations)
) {
setupExtractor();
setupBadge();
updateBadge();
createContextMenu();
}
});
function arraysEqualDeep(arr1, arr2) {
return JSON.stringify(arr1) !== JSON.stringify(arr2);
}

60
torrent/common.js Normal file
View File

@@ -0,0 +1,60 @@
"use strict";
////// RPC
function rpcCall(meth, args) {
return browser.storage.local.get(function(server) {
const myHeaders = {
"Content-Type": "application/json",
"x-transmission-session-id": server.session
};
//console.log("(torrent)", server.session)
if (server.username !== "" || server.btrpcpass !== "") {
myHeaders["Authorization"] =
"Basic " +
btoa((server.username || "") + ":" + (server.btrpcpass || ""));
}
//console.log("(torrent) rpc", server.base_url);
return fetch(server.base_url + "rpc", {
method: "POST",
headers: myHeaders,
body: JSON.stringify({ method: meth, arguments: args }),
credentials: "include" // allows HTTPS client certs!
})
.then(function(response) {
const session = response.headers.get("x-transmission-session-id");
if (session) {
browser.storage.local.get({}).then(function(storage) {
storage.session = session;
browser.storage.local.set(storage);
});
}
if (response.status === 409) {
return rpcCall(meth, args);
}
if (response.status >= 200 && response.status < 300) {
return response;
}
const error = new Error(response.statusText);
error.response = response;
throw error;
})
.then(function(response) {
return response.json();
});
});
}
////// Util
function formatSpeed(s) {
// Firefox shows 4 characters max
if (s < 1000 * 1000) {
return (s / 1000).toFixed() + "K";
}
if (s < 1000 * 1000 * 1000) {
return (s / 1000 / 1000).toFixed() + "M";
}
// You probably don't have that download speed…
return (s / 1000 / 1000 / 1000).toFixed() + "T";
}

107
torrent/popup.js Normal file
View File

@@ -0,0 +1,107 @@
"use strict";
const torrentsPane = document.getElementById("torrents-pane");
const configPane = document.getElementById("config-pane");
for (const opener of document.querySelectorAll(".config-opener")) {
opener.addEventListener("click", e => {
browser.runtime.openOptionsPage();
});
}
function showConfig(server) {
torrentsPane.hidden = true;
configPane.hidden = false;
}
const torrentsSearch = document.getElementById("torrents-search");
const torrentsList = document.getElementById("torrents-list");
const torrentsTpl = document.getElementById("torrents-tpl");
const torrentsError = document.getElementById("torrents-error");
const getArgs = {
fields: ["name", "percentDone", "rateDownload", "rateUpload", "queuePosition"]
};
let cachedTorrents = [];
function renderTorrents(newTorrents) {
if (torrentsList.children.length < newTorrents.length) {
const dif = newTorrents.length - torrentsList.children.length;
for (let i = 0; i < dif; i++) {
const node = document.importNode(torrentsTpl.content, true);
torrentsList.appendChild(node);
}
} else if (torrentsList.children.length > newTorrents.length) {
const oldLen = torrentsList.children.length;
const dif = oldLen - newTorrents.length;
for (let i = 1; i <= dif; i++) {
torrentsList.removeChild(torrentsList.children[oldLen - i]);
}
}
for (let i = 0; i < newTorrents.length; i++) {
const torr = newTorrents[i];
const cont = torrentsList.children[i];
const speeds =
"↓ " +
formatSpeed(torr.rateDownload) +
"B/s ↑ " +
formatSpeed(torr.rateUpload) +
"B/s";
cont.querySelector(".torrent-name").textContent = torr.name;
cont.querySelector(".torrent-speeds").textContent = speeds;
cont.querySelector(".torrent-progress").value = torr.percentDone * 100;
}
}
function searchTorrents() {
let newTorrents = cachedTorrents;
const val = torrentsSearch.value.toLowerCase().trim();
if (val.length > 0) {
newTorrents = newTorrents.filter(x => x.name.toLowerCase().includes(val));
}
renderTorrents(newTorrents);
}
torrentsSearch.addEventListener("change", searchTorrents);
torrentsSearch.addEventListener("keyup", searchTorrents);
function refreshTorrents(server) {
return rpcCall("torrent-get", getArgs).then(function(response) {
console.log("(torrent) refreshing", response);
let newTorrents = response.arguments.torrents;
newTorrents.sort((x, y) => y.queuePosition - x.queuePosition);
cachedTorrents = newTorrents;
torrentsSearch.hidden = newTorrents.length <= 8;
if (torrentsSearch.hidden) {
torrentsSearch.value = "";
renderTorrents(newTorrents);
} else {
searchTorrents();
}
});
}
function refreshTorrentsLogErr(server) {
return refreshTorrents(server).catch(err => {
console.error(err);
torrentsError.textContent = "Error: " + err.toString();
});
}
function showTorrents(server) {
torrentsPane.hidden = false;
configPane.hidden = true;
for (const opener of document.querySelectorAll(".webui-opener")) {
opener.href = server.base_url + "web/";
}
refreshTorrents(server).catch(_ => refreshTorrentsLogErr(server));
setInterval(() => refreshTorrentsLogErr(server), 2000);
}
//let store =
browser.storage.local.get(function(server) {
console.log("(torrent) querying storage", server);
if (server && server.base_url && server.base_url !== "") {
showTorrents(server);
} else {
showConfig(server);
}
});

View File

@@ -1,91 +0,0 @@
var hellot = "hello bittorrent";
var xTransmissionSessionId = "";
function makeid(length) {
var result = "";
var characters =
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
var charactersLength = characters.length;
for (var i = 0; i < length; i++) {
result += characters.charAt(Math.floor(Math.random() * charactersLength));
}
return result;
}
function torrentsend(
message,
control_host = "127.0.0.1",
control_port = "7657",
control_path = "transmission/rpc"
) {
async function postData(url = "", data = {}) {
// Default options are marked with *
let requestBody = JSON.stringify(data);
console.log("(torrent-rpc) send", requestBody, data);
let opts = {
method: "POST", // *GET, POST, PUT, DELETE, etc.
mode: "cors", // no-cors, *cors, same-origin
cache: "no-cache", // *default, no-cache, reload, force-cache, only-if-cached
credentials: "same-origin", // include, *same-origin, omit
headers: {
"Content-Type": "application/json",
"X-Transmission-Session-Id": xTransmissionSessionId
},
redirect: "follow", // manual, *follow, error
referrerPolicy: "no-referrer", // no-referrer, *client
body: requestBody // body data type must match "Content-Type" header
};
const response = await fetch(url, opts);
console.log("(torrent-rpc) response", response);
return await response.json(); // parses JSON response into native JavaScript objects
}
return postData(
"http://" + control_host + ":" + control_port + "/" + control_path + "/",
message
);
/* return postData(
"http://" + control_host + ":" + control_port + "/" + control_path,
message
);*/
}
function sessionStats(
password = "transmission",
control_host = "127.0.0.1",
control_port = "7657",
control_path = "transmission/rpc"
) {
var json = new Object();
json["id"] = makeid(6);
json["jsonrpc"] = "2.0";
json["method"] = "session-stats";
//json["params"] = new Object();
return torrentsend(json, control_host, control_port, control_path);
}
async function GetTorrentToken(
password,
control_host = "127.0.0.1",
control_port = "7657",
control_path = "transmission/rpc"
) {
let me = sessionStats(password);
return await me.then(gettorrenttoken);
}
function gettorrenttoken(authtoken) {
console.log(authtoken);
return authtoken.result.Token;
}
function TorrentDone(result) {
console.log("(torrent-rpc) recv", result);
}
function TorrentError(result) {
console.log("(torrent-rpc) recv err", result);
}
var result = GetTorrentToken();
result.then(TorrentDone, TorrentError);

View File

@@ -4,7 +4,8 @@
<meta charset="utf-8">
<link href="search.css" rel="stylesheet">
<link href="home.css" rel="stylesheet">
<link href="info.css" rel="stylesheet">
<link href="info.css" rel="stylesheet"><!--<link href="torrent/popup.css" rel="stylesheet">-->
<title>
</title>
</head>
@@ -111,26 +112,63 @@
<p>
</p>
</div>
<div id="torrentpanel">
<div class="panel" id="torrentstatus">
<div class="section-header panel-section panel-section-header">
<div class="text-section-header" id="text-section-torrents-header">
<h3>Torrent Downloads</h3>
</div>
</div>
<div id="torrentsection">
Torrents go here.
</div>
</div>
</div>
<script src="context.js"></script>
<script src="privacy.js"></script>
<script src="info.js"></script>
<script crossorigin="anonymous" src="content.js"></script>
<script src="i2pcontrol/i2pcontrol.js"></script>
</div>
</div>
<div id="torrentpanel">
<div class="panel" id="torrentstatus">
<div class="section-header panel-section panel-section-header">
<div class="text-section-header" id="text-section-torrents-header">
<h3>Torrent Downloads</h3>
</div>
</div>
<div hidden="" id="config-pane">
<strong>Torrent RPC Configuration</strong>
<br>
The server address is not set yet.
<br>
<a class="config-opener" href="#">Open the settings</a> to continue.
</div>
<div hidden="" id="torrents-pane">
<header>
<h1>Torrent Controls</h1>
<a class="webui-opener" href="#" target="_blank"><img alt="Open Web UI" src="icon.svg"></a> <!--<a class="config-opener" href="#">
<img alt="Settings" src="gear.svg"></a>
<a class="info-opener" href="https://github.com/myfreeweb/transmitter" target="_blank">
<img alt="Extension Info" src="info.svg"></a>-->
</header>
<input id="torrents-search" placeholder="Search…" type="search">
<template id="torrents-tpl">
<ul>
<li>
<div class="torrent-head">
<div class="torrent-name">
</div>
<div class="torrent-speeds">
</div>
</div>
<progress class="torrent-progress" max="100"></progress>
</li>
</ul>
</template>
<ul id="torrents-list">
</ul>
<div id="torrents-error">
</div>
</div>
</div>
</div>
<script src="context.js"></script>
<script src="privacy.js"></script>
<script src="i2pcontrol/i2pcontrol.js"></script>
<script src="info.js"></script>
<script crossorigin="anonymous" src="content.js"></script>
<script src="torrent/common.js"></script>
<script src="torrent/popup.js"></script>
</body>
</html>