From 2394a9f6ee65cf26952985c10d7563a7f2d99adc Mon Sep 17 00:00:00 2001 From: Kumi Date: Mon, 24 Jun 2024 10:40:32 +0200 Subject: [PATCH] feat: improve security and error handling in app - Added `helmet` middleware for enhanced security with CSP. - Integrated `dotenv` for configuration management. - Added validation and error handling for mnemonic and infoHash. - Improved error handling in TURN credentials generation. - Enhanced notification and progress feedback for file sharing. - Added tracker server config validation and error handling. - Updated dependencies to include `helmet` and `dotenv`. These changes improve the app's security, robustness, and user experience. --- app.js | 75 ++++++++++--- package-lock.json | 43 ++++++-- package.json | 4 +- public/js/index.js | 264 +++++++++++++++++++++++++-------------------- tracker.js | 18 +++- views/index.ejs | 10 +- 6 files changed, 261 insertions(+), 153 deletions(-) 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 @@

Share File

- +
@@ -43,7 +42,7 @@

Receive File

- +
0%
@@ -71,10 +70,7 @@

- +