diff --git a/app.js b/app.js
index 6838e85..3848c92 100644
--- a/app.js
+++ b/app.js
@@ -3,6 +3,10 @@ import path from "path";
import { fileURLToPath } from "url";
import bip39 from "bip39";
import crypto from "crypto";
+import helmet from "helmet";
+import dotenv from "dotenv";
+
+dotenv.config();
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
@@ -17,16 +21,51 @@ const turnTTL = 86400;
app.set("view engine", "ejs");
app.use(express.static(path.join(__dirname, "public")));
+app.use(helmet());
+app.use(
+ helmet.contentSecurityPolicy({
+ directives: {
+ defaultSrc: ["'self'"],
+ scriptSrc: ["'self'"],
+ connectSrc: ["'self'", trackerUrl],
+ imgSrc: ["'self'", "data:"],
+ styleSrc: ["'self'", "'unsafe-inline'"],
+ },
+ })
+);
+app.use(express.json());
+app.use(express.urlencoded({ extended: true }));
+
+const isValidInfoHash = (infoHash) => /^[0-9a-fA-F]{40}$/.test(infoHash);
+
app.get("/generate-mnemonic/:infoHash", (req, res) => {
const infoHash = req.params.infoHash;
- const mnemonic = bip39.entropyToMnemonic(infoHash);
- res.json({ mnemonic });
+
+ if (!isValidInfoHash(infoHash)) {
+ return res.status(400).json({ error: "Invalid infoHash" });
+ }
+
+ try {
+ const mnemonic = bip39.entropyToMnemonic(infoHash);
+ res.json({ mnemonic });
+ } catch (error) {
+ res.status(500).json({ error: "Failed to generate mnemonic" });
+ }
});
app.get("/get-infohash/:mnemonic", (req, res) => {
const mnemonic = req.params.mnemonic;
- const infoHash = bip39.mnemonicToEntropy(mnemonic);
- res.json({ infoHash });
+
+ if (!bip39.validateMnemonic(mnemonic)) {
+ return res.status(400).json({ error: "Invalid mnemonic" });
+ }
+
+ try {
+ const infoHash = bip39.mnemonicToEntropy(mnemonic);
+ res.json({ infoHash });
+ } catch (error) {
+ res.status(500).json({ error: "Failed to get infoHash" });
+ }
});
app.get("/turn-credentials", (req, res) => {
@@ -37,24 +76,30 @@ app.get("/turn-credentials", (req, res) => {
}
if (turnServerUrl && turnSecret) {
- const unixTimeStamp = Math.floor(Date.now() / 1000) + turnTTL;
- const username = `${unixTimeStamp}:transfercoffee`;
- const hmac = crypto.createHmac("sha1", turnSecret);
- hmac.update(username);
- const credential = hmac.digest("base64");
+ try {
+ const unixTimeStamp = Math.floor(Date.now() / 1000) + turnTTL;
+ const username = `${unixTimeStamp}:transfercoffee`;
+ const hmac = crypto.createHmac("sha1", turnSecret);
+ hmac.update(username);
+ const credential = hmac.digest("base64");
- iceServers.push({
- urls: turnServerUrl,
- username: username,
- credential: credential,
- });
+ iceServers.push({
+ urls: turnServerUrl,
+ username: username,
+ credential: credential,
+ });
+ } catch (error) {
+ return res
+ .status(500)
+ .json({ error: "Failed to generate TURN credentials" });
+ }
}
res.json({ iceServers });
});
app.get("/:mnemonic?", (req, res) => {
- var mnemonic = req.params.mnemonic || "";
+ let mnemonic = req.params.mnemonic || "";
if (mnemonic) {
mnemonic = mnemonic.replaceAll(".", " ").trim();
diff --git a/package-lock.json b/package-lock.json
index 141006a..7ea4d88 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -11,8 +11,10 @@
"dependencies": {
"bip39": "^3.1.0",
"bittorrent-tracker": "^11.1.0",
+ "dotenv": "^16.4.5",
"ejs": "^3.1.10",
- "express": "^4.19.2"
+ "express": "^4.19.2",
+ "helmet": "^7.1.0"
}
},
"node_modules/@noble/hashes": {
@@ -297,11 +299,6 @@
}
}
},
- "node_modules/bittorrent-tracker/node_modules/ip": {
- "version": "2.0.1",
- "resolved": "https://registry.npmjs.org/ip/-/ip-2.0.1.tgz",
- "integrity": "sha512-lJUL9imLTNi1ZfXT+DU6rBBdbiKGBuay9B6xGSPVjUeQwaH1RIGqef8RZkUtHioLmSNpPR5M4HVKJGm1j8FWVQ=="
- },
"node_modules/bittorrent-tracker/node_modules/ms": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
@@ -624,6 +621,18 @@
"node": ">=8"
}
},
+ "node_modules/dotenv": {
+ "version": "16.4.5",
+ "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.5.tgz",
+ "integrity": "sha512-ZmdL2rui+eB2YwhsWzjInR8LldtZHGDoQ1ugH85ppHKwpUHL7j7rN0Ti9NCnGiQbhaZ11FpR+7ao1dNsmduNUg==",
+ "license": "BSD-2-Clause",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://dotenvx.com"
+ }
+ },
"node_modules/ee-first": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
@@ -950,6 +959,15 @@
"node": ">= 0.4"
}
},
+ "node_modules/helmet": {
+ "version": "7.1.0",
+ "resolved": "https://registry.npmjs.org/helmet/-/helmet-7.1.0.tgz",
+ "integrity": "sha512-g+HZqgfbpXdCkme/Cd/mZkV0aV3BZZZSugecH03kl38m/Kmdx8jKjBikpDj2cr+Iynv4KpYEviojNdTJActJAg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=16.0.0"
+ }
+ },
"node_modules/http-errors": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz",
@@ -1005,6 +1023,12 @@
"resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz",
"integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew=="
},
+ "node_modules/ip": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/ip/-/ip-2.0.1.tgz",
+ "integrity": "sha512-lJUL9imLTNi1ZfXT+DU6rBBdbiKGBuay9B6xGSPVjUeQwaH1RIGqef8RZkUtHioLmSNpPR5M4HVKJGm1j8FWVQ==",
+ "license": "MIT"
+ },
"node_modules/ip-address": {
"version": "9.0.5",
"resolved": "https://registry.npmjs.org/ip-address/-/ip-address-9.0.5.tgz",
@@ -1873,9 +1897,10 @@
"integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="
},
"node_modules/ws": {
- "version": "8.17.0",
- "resolved": "https://registry.npmjs.org/ws/-/ws-8.17.0.tgz",
- "integrity": "sha512-uJq6108EgZMAl20KagGkzCKfMEjxmKvZHG7Tlq0Z6nOky7YF7aq4mOx6xK8TJ/i1LeK4Qus7INktacctDgY8Ow==",
+ "version": "8.17.1",
+ "resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz",
+ "integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==",
+ "license": "MIT",
"engines": {
"node": ">=10.0.0"
},
diff --git a/package.json b/package.json
index e3f2af0..94251f4 100644
--- a/package.json
+++ b/package.json
@@ -18,7 +18,9 @@
"dependencies": {
"bip39": "^3.1.0",
"bittorrent-tracker": "^11.1.0",
+ "dotenv": "^16.4.5",
"ejs": "^3.1.10",
- "express": "^4.19.2"
+ "express": "^4.19.2",
+ "helmet": "^7.1.0"
}
}
diff --git a/public/js/index.js b/public/js/index.js
index e38c6e0..e4dcb64 100644
--- a/public/js/index.js
+++ b/public/js/index.js
@@ -1,12 +1,17 @@
const client = new WebTorrent();
async function getRTCIceServers() {
- const response = await fetch("/turn-credentials");
- const data = await response.json();
- return data.iceServers;
+ try {
+ const response = await fetch("/turn-credentials");
+ if (!response.ok) throw new Error("Failed to fetch TURN credentials");
+ const data = await response.json();
+ return data.iceServers;
+ } catch (error) {
+ console.error("Error getting ICE servers:", error);
+ }
}
-async function uploadFile() {
+async function uploadFile(trackerUrl) {
const fileInput = document.getElementById("fileInput");
const file = fileInput.files[0];
const uploadStats = document.getElementById("uploadStats");
@@ -24,19 +29,21 @@ async function uploadFile() {
uploadSection.style.display = "block";
uploadButton.disabled = true;
- const rtcConfig = {
- iceServers: await getRTCIceServers(),
- };
+ try {
+ const rtcConfig = {
+ iceServers: await getRTCIceServers(),
+ };
- const opts = {
- announce: [trackerUrl],
- rtcConfig: rtcConfig,
- };
+ const opts = {
+ announce: [trackerUrl],
+ rtcConfig: rtcConfig,
+ };
- client.seed(file, opts, async (torrent) => {
- fetch(`/generate-mnemonic/${torrent.infoHash}`)
- .then((response) => response.json())
- .then((data) => {
+ client.seed(file, opts, async (torrent) => {
+ try {
+ const response = await fetch(`/generate-mnemonic/${torrent.infoHash}`);
+ if (!response.ok) throw new Error("Failed to generate mnemonic");
+ const data = await response.json();
const uploadResult = document.getElementById("uploadResult");
const downloadUrl = `${
window.location.origin
@@ -46,43 +53,45 @@ async function uploadFile() {
Note that the file will be available for download only as long as you keep this page open.`;
copyButton.style.display = "inline-block";
copyButton.setAttribute("data-url", downloadUrl);
- });
-
- let totalPeers = 0;
- const seenPeers = new Set();
-
- setInterval(async () => {
- for (const wire of torrent.wires) {
- let peerIdHash;
- try {
- peerIdHash = await sha256(wire.peerId);
- } catch (e) {
- peerIdHash = wire.peerId;
- }
-
- if (!seenPeers.has(peerIdHash)) {
- seenPeers.add(peerIdHash);
- totalPeers += 1;
- }
+ } catch (error) {
+ console.error("Error generating mnemonic:", error);
+ alert("Failed to generate mnemonic. Please try again.");
}
- const uploaded = (torrent.uploaded / (1024 * 1024)).toFixed(2);
- uploadStats.innerHTML = `Uploaded: ${uploaded} MB to ${totalPeers} peer(s)`;
- }, 1000);
- });
+ let totalPeers = 0;
+ const seenPeers = new Set();
+
+ setInterval(async () => {
+ for (const wire of torrent.wires) {
+ let peerIdHash;
+ try {
+ peerIdHash = await sha256(wire.peerId);
+ } catch (e) {
+ peerIdHash = wire.peerId;
+ }
+
+ if (!seenPeers.has(peerIdHash)) {
+ seenPeers.add(peerIdHash);
+ totalPeers += 1;
+ }
+ }
+
+ const uploaded = (torrent.uploaded / (1024 * 1024)).toFixed(2);
+ uploadStats.innerHTML = `Uploaded: ${uploaded} MB to ${totalPeers} peer(s)`;
+ }, 1000);
+ });
+ } catch (error) {
+ console.error("Error sharing file:", error);
+ }
}
function copyToClipboard() {
const copyButton = document.getElementById("copyButton");
const url = copyButton.getAttribute("data-url");
- navigator.clipboard
- .writeText(url)
- .then(() => {
- alert("URL copied to clipboard");
- })
- .catch((err) => {
- console.error("Failed to copy: ", err);
- });
+ navigator.clipboard.writeText(url).catch((err) => {
+ console.error("Failed to copy: ", err);
+ alert("Failed to copy URL to clipboard. Please try again.");
+ });
}
async function sha256(str) {
@@ -93,10 +102,11 @@ async function sha256(str) {
.join("");
}
-async function downloadFile() {
+async function downloadFile(trackerUrl) {
const mnemonicInput = document.getElementById("mnemonicInput").value;
const downloadProgressBar = document.getElementById("downloadProgressBar");
const downloadButton = document.getElementById("downloadButton");
+ const downloadResult = document.getElementById("downloadResult");
if (!mnemonicInput) {
alert("Please enter a mnemonic.");
@@ -109,89 +119,105 @@ async function downloadFile() {
downloadResult.innerHTML = "Preparing incoming file transfer, please wait...";
- const rtcConfig = {
- iceServers: await getRTCIceServers(),
- };
+ try {
+ const rtcConfig = {
+ iceServers: await getRTCIceServers(),
+ };
- fetch(`/get-infohash/${mnemonicInput}`)
- .then((response) => response.json())
- .then((data) => {
- const torrentId = data.infoHash;
+ const response = await fetch(`/get-infohash/${mnemonicInput}`);
+ if (!response.ok) throw new Error("Failed to get infoHash");
+ const data = await response.json();
+ const torrentId = data.infoHash;
- const opts = {
- announce: [trackerUrl],
- rtcConfig: rtcConfig,
- };
+ const opts = {
+ announce: [trackerUrl],
+ rtcConfig: rtcConfig,
+ };
- client.add(torrentId, opts, (torrent) => {
- torrent.files[0].getBlob((err, blob) => {
- if (err) {
- const downloadResult = document.getElementById("downloadResult");
- downloadResult.innerHTML = `Error: ${err.message}`;
- return;
- }
-
- const url = URL.createObjectURL(blob);
- const a = document.createElement("a");
- a.href = url;
- a.download = torrent.files[0].name;
- a.click();
-
- const downloadResult = document.getElementById("downloadResult");
- downloadResult.innerHTML = `File downloaded: ${torrent.files[0].name}`;
- });
-
- torrent.on("download", () => {
- const progress = Math.round(
- (torrent.downloaded / torrent.length) * 100
- );
- downloadProgressBar.style.width = `${progress}%`;
- downloadProgressBar.textContent = `${progress}%`;
- });
- });
-
- setInterval(() => {
- const downloadResult = document.getElementById("downloadResult");
-
- if (client.get(torrentId)) {
- const torrent = client.get(torrentId);
- const progress = Math.round(
- (torrent.downloaded / torrent.length) * 100
- );
- downloadResult.innerHTML = `Downloading:
- ${torrent.files[0].name}
-
-
Status: ${
- torrent.done ? "Completed, seeding" : "Downloading"
- }
-
Peers: ${torrent.numPeers}
-
-
Downloaded: ${(
- torrent.downloaded /
- (1024 * 1024)
- ).toFixed(2)} MB / ${(torrent.length / (1024 * 1024)).toFixed(
- 2
- )} MB (${progress}%)
-
Speed: ${(
- torrent.downloadSpeed /
- (1024 * 1024)
- ).toFixed(2)} MB/s
-
ETA: ${torrent.timeRemaining} seconds
-
-
Uploaded: ${(
- torrent.uploaded /
- (1024 * 1024)
- ).toFixed(2)} MB
-
Ratio: ${torrent.ratio.toFixed(2)}`;
+ client.add(torrentId, opts, (torrent) => {
+ torrent.files[0].getBlob((err, blob) => {
+ if (err) {
+ downloadResult.innerHTML = `Error: ${err.message}`;
return;
}
- downloadResult.innerHTML = "File not found. Please check the mnemonic.";
- }, 1000);
+ const url = URL.createObjectURL(blob);
+ const a = document.createElement("a");
+ a.href = url;
+ a.download = torrent.files[0].name;
+ a.click();
+
+ downloadResult.innerHTML = `File downloaded: ${torrent.files[0].name}`;
+ });
+
+ torrent.on("download", () => {
+ const progress = Math.round(
+ (torrent.downloaded / torrent.length) * 100
+ );
+ downloadProgressBar.style.width = `${progress}%`;
+ downloadProgressBar.textContent = `${progress}%`;
+ });
});
+
+ setInterval(() => {
+ if (client.get(torrentId)) {
+ const torrent = client.get(torrentId);
+ const progress = Math.round(
+ (torrent.downloaded / torrent.length) * 100
+ );
+ downloadResult.innerHTML = `Downloading:
+ ${torrent.files[0].name}
+
+
Status: ${
+ torrent.done ? "Completed, seeding" : "Downloading"
+ }
+
Peers: ${torrent.numPeers}
+
+
Downloaded: ${(
+ torrent.downloaded /
+ (1024 * 1024)
+ ).toFixed(2)} MB / ${(torrent.length / (1024 * 1024)).toFixed(
+ 2
+ )} MB (${progress}%)
+
Speed: ${(
+ torrent.downloadSpeed /
+ (1024 * 1024)
+ ).toFixed(2)} MB/s
+
ETA: ${torrent.timeRemaining} seconds
+
+
Uploaded: ${(
+ torrent.uploaded /
+ (1024 * 1024)
+ ).toFixed(2)} MB
+
Ratio: ${torrent.ratio.toFixed(2)}`;
+ return;
+ }
+
+ downloadResult.innerHTML = "File not found. Please check the mnemonic.";
+ }, 1000);
+ } catch (error) {
+ console.error("Error downloading file:", error);
+ alert("Failed to download file. Please check the mnemonic and try again.");
+ } finally {
+ downloadButton.disabled = false;
+ }
}
document.addEventListener("DOMContentLoaded", () => {
+ const configElement = document.getElementById("config");
+ const trackerUrl = configElement.getAttribute("data-tracker-url");
+ const mnemonic = configElement.getAttribute("data-mnemonic");
+
+ document
+ .getElementById("uploadButton")
+ .addEventListener("click", () => uploadFile(trackerUrl));
+ document
+ .getElementById("copyButton")
+ .addEventListener("click", copyToClipboard);
+ document
+ .getElementById("downloadButton")
+ .addEventListener("click", () => downloadFile(trackerUrl));
+
if (mnemonic) {
const mnemonicInput = document.getElementById("mnemonicInput");
const downloadButton = document.getElementById("downloadButton");
diff --git a/tracker.js b/tracker.js
index 8b3512b..d1be9d8 100644
--- a/tracker.js
+++ b/tracker.js
@@ -1,8 +1,16 @@
import { Server } from "bittorrent-tracker";
+import dotenv from "dotenv";
+
+dotenv.config();
const PORT = process.env.TRACKER_PORT || 8106;
const HOST = process.env.TRACKER_HOST || "localhost";
+const port = Number(PORT);
+if (isNaN(port) || port <= 0) {
+ throw new Error("Invalid TRACKER_PORT value. It must be a positive number.");
+}
+
const server = new Server({
udp: false,
http: false,
@@ -19,7 +27,13 @@ server.on("warning", (err) => {
});
server.on("listening", () => {
- console.log(`Tracker is listening on http://${HOST}:${PORT}`);
+ console.log(`Tracker is listening on ws://${HOST}:${PORT}`);
});
-server.listen(PORT);
+try {
+ server.listen(port, HOST, () => {
+ console.log(`Tracker server started on ws://${HOST}:${PORT}`);
+ });
+} catch (err) {
+ console.error(`Failed to start tracker server: ${err.message}`);
+}
\ No newline at end of file
diff --git a/views/index.ejs b/views/index.ejs
index ae8095f..3ad294f 100644
--- a/views/index.ejs
+++ b/views/index.ejs
@@ -29,13 +29,12 @@