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.
This commit is contained in:
Kumi 2024-06-24 10:40:32 +02:00
parent f87ec52173
commit 2394a9f6ee
Signed by: kumi
GPG key ID: ECBCC9082395383F
6 changed files with 261 additions and 153 deletions

75
app.js
View file

@ -3,6 +3,10 @@ import path from "path";
import { fileURLToPath } from "url"; import { fileURLToPath } from "url";
import bip39 from "bip39"; import bip39 from "bip39";
import crypto from "crypto"; import crypto from "crypto";
import helmet from "helmet";
import dotenv from "dotenv";
dotenv.config();
const __filename = fileURLToPath(import.meta.url); const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename); const __dirname = path.dirname(__filename);
@ -17,16 +21,51 @@ const turnTTL = 86400;
app.set("view engine", "ejs"); app.set("view engine", "ejs");
app.use(express.static(path.join(__dirname, "public"))); 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) => { app.get("/generate-mnemonic/:infoHash", (req, res) => {
const infoHash = req.params.infoHash; 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) => { app.get("/get-infohash/:mnemonic", (req, res) => {
const mnemonic = req.params.mnemonic; 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) => { app.get("/turn-credentials", (req, res) => {
@ -37,24 +76,30 @@ app.get("/turn-credentials", (req, res) => {
} }
if (turnServerUrl && turnSecret) { if (turnServerUrl && turnSecret) {
const unixTimeStamp = Math.floor(Date.now() / 1000) + turnTTL; try {
const username = `${unixTimeStamp}:transfercoffee`; const unixTimeStamp = Math.floor(Date.now() / 1000) + turnTTL;
const hmac = crypto.createHmac("sha1", turnSecret); const username = `${unixTimeStamp}:transfercoffee`;
hmac.update(username); const hmac = crypto.createHmac("sha1", turnSecret);
const credential = hmac.digest("base64"); hmac.update(username);
const credential = hmac.digest("base64");
iceServers.push({ iceServers.push({
urls: turnServerUrl, urls: turnServerUrl,
username: username, username: username,
credential: credential, credential: credential,
}); });
} catch (error) {
return res
.status(500)
.json({ error: "Failed to generate TURN credentials" });
}
} }
res.json({ iceServers }); res.json({ iceServers });
}); });
app.get("/:mnemonic?", (req, res) => { app.get("/:mnemonic?", (req, res) => {
var mnemonic = req.params.mnemonic || ""; let mnemonic = req.params.mnemonic || "";
if (mnemonic) { if (mnemonic) {
mnemonic = mnemonic.replaceAll(".", " ").trim(); mnemonic = mnemonic.replaceAll(".", " ").trim();

43
package-lock.json generated
View file

@ -11,8 +11,10 @@
"dependencies": { "dependencies": {
"bip39": "^3.1.0", "bip39": "^3.1.0",
"bittorrent-tracker": "^11.1.0", "bittorrent-tracker": "^11.1.0",
"dotenv": "^16.4.5",
"ejs": "^3.1.10", "ejs": "^3.1.10",
"express": "^4.19.2" "express": "^4.19.2",
"helmet": "^7.1.0"
} }
}, },
"node_modules/@noble/hashes": { "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": { "node_modules/bittorrent-tracker/node_modules/ms": {
"version": "2.1.2", "version": "2.1.2",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
@ -624,6 +621,18 @@
"node": ">=8" "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": { "node_modules/ee-first": {
"version": "1.1.1", "version": "1.1.1",
"resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
@ -950,6 +959,15 @@
"node": ">= 0.4" "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": { "node_modules/http-errors": {
"version": "2.0.0", "version": "2.0.0",
"resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", "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", "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz",
"integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==" "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": { "node_modules/ip-address": {
"version": "9.0.5", "version": "9.0.5",
"resolved": "https://registry.npmjs.org/ip-address/-/ip-address-9.0.5.tgz", "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-9.0.5.tgz",
@ -1873,9 +1897,10 @@
"integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==" "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="
}, },
"node_modules/ws": { "node_modules/ws": {
"version": "8.17.0", "version": "8.17.1",
"resolved": "https://registry.npmjs.org/ws/-/ws-8.17.0.tgz", "resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz",
"integrity": "sha512-uJq6108EgZMAl20KagGkzCKfMEjxmKvZHG7Tlq0Z6nOky7YF7aq4mOx6xK8TJ/i1LeK4Qus7INktacctDgY8Ow==", "integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==",
"license": "MIT",
"engines": { "engines": {
"node": ">=10.0.0" "node": ">=10.0.0"
}, },

View file

@ -18,7 +18,9 @@
"dependencies": { "dependencies": {
"bip39": "^3.1.0", "bip39": "^3.1.0",
"bittorrent-tracker": "^11.1.0", "bittorrent-tracker": "^11.1.0",
"dotenv": "^16.4.5",
"ejs": "^3.1.10", "ejs": "^3.1.10",
"express": "^4.19.2" "express": "^4.19.2",
"helmet": "^7.1.0"
} }
} }

View file

@ -1,12 +1,17 @@
const client = new WebTorrent(); const client = new WebTorrent();
async function getRTCIceServers() { async function getRTCIceServers() {
const response = await fetch("/turn-credentials"); try {
const data = await response.json(); const response = await fetch("/turn-credentials");
return data.iceServers; 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 fileInput = document.getElementById("fileInput");
const file = fileInput.files[0]; const file = fileInput.files[0];
const uploadStats = document.getElementById("uploadStats"); const uploadStats = document.getElementById("uploadStats");
@ -24,19 +29,21 @@ async function uploadFile() {
uploadSection.style.display = "block"; uploadSection.style.display = "block";
uploadButton.disabled = true; uploadButton.disabled = true;
const rtcConfig = { try {
iceServers: await getRTCIceServers(), const rtcConfig = {
}; iceServers: await getRTCIceServers(),
};
const opts = { const opts = {
announce: [trackerUrl], announce: [trackerUrl],
rtcConfig: rtcConfig, rtcConfig: rtcConfig,
}; };
client.seed(file, opts, async (torrent) => { client.seed(file, opts, async (torrent) => {
fetch(`/generate-mnemonic/${torrent.infoHash}`) try {
.then((response) => response.json()) const response = await fetch(`/generate-mnemonic/${torrent.infoHash}`);
.then((data) => { if (!response.ok) throw new Error("Failed to generate mnemonic");
const data = await response.json();
const uploadResult = document.getElementById("uploadResult"); const uploadResult = document.getElementById("uploadResult");
const downloadUrl = `${ const downloadUrl = `${
window.location.origin window.location.origin
@ -46,43 +53,45 @@ async function uploadFile() {
<br>Note that the file will be available for download only as long as you keep this page open.`; <br>Note that the file will be available for download only as long as you keep this page open.`;
copyButton.style.display = "inline-block"; copyButton.style.display = "inline-block";
copyButton.setAttribute("data-url", downloadUrl); copyButton.setAttribute("data-url", downloadUrl);
}); } catch (error) {
console.error("Error generating mnemonic:", error);
let totalPeers = 0; alert("Failed to generate mnemonic. Please try again.");
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); let totalPeers = 0;
uploadStats.innerHTML = `Uploaded: ${uploaded} MB to ${totalPeers} peer(s)`; const seenPeers = new Set();
}, 1000);
}); 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() { function copyToClipboard() {
const copyButton = document.getElementById("copyButton"); const copyButton = document.getElementById("copyButton");
const url = copyButton.getAttribute("data-url"); const url = copyButton.getAttribute("data-url");
navigator.clipboard navigator.clipboard.writeText(url).catch((err) => {
.writeText(url) console.error("Failed to copy: ", err);
.then(() => { alert("Failed to copy URL to clipboard. Please try again.");
alert("URL copied to clipboard"); });
})
.catch((err) => {
console.error("Failed to copy: ", err);
});
} }
async function sha256(str) { async function sha256(str) {
@ -93,10 +102,11 @@ async function sha256(str) {
.join(""); .join("");
} }
async function downloadFile() { async function downloadFile(trackerUrl) {
const mnemonicInput = document.getElementById("mnemonicInput").value; const mnemonicInput = document.getElementById("mnemonicInput").value;
const downloadProgressBar = document.getElementById("downloadProgressBar"); const downloadProgressBar = document.getElementById("downloadProgressBar");
const downloadButton = document.getElementById("downloadButton"); const downloadButton = document.getElementById("downloadButton");
const downloadResult = document.getElementById("downloadResult");
if (!mnemonicInput) { if (!mnemonicInput) {
alert("Please enter a mnemonic."); alert("Please enter a mnemonic.");
@ -109,89 +119,105 @@ async function downloadFile() {
downloadResult.innerHTML = "Preparing incoming file transfer, please wait..."; downloadResult.innerHTML = "Preparing incoming file transfer, please wait...";
const rtcConfig = { try {
iceServers: await getRTCIceServers(), const rtcConfig = {
}; iceServers: await getRTCIceServers(),
};
fetch(`/get-infohash/${mnemonicInput}`) const response = await fetch(`/get-infohash/${mnemonicInput}`);
.then((response) => response.json()) if (!response.ok) throw new Error("Failed to get infoHash");
.then((data) => { const data = await response.json();
const torrentId = data.infoHash; const torrentId = data.infoHash;
const opts = { const opts = {
announce: [trackerUrl], announce: [trackerUrl],
rtcConfig: rtcConfig, rtcConfig: rtcConfig,
}; };
client.add(torrentId, opts, (torrent) => { client.add(torrentId, opts, (torrent) => {
torrent.files[0].getBlob((err, blob) => { torrent.files[0].getBlob((err, blob) => {
if (err) { if (err) {
const downloadResult = document.getElementById("downloadResult"); downloadResult.innerHTML = `Error: ${err.message}`;
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: <strong>${torrent.files[0].name}</strong>`;
});
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:
<strong>${torrent.files[0].name}</strong>
<br>
<br><strong>Status:</strong> ${
torrent.done ? "Completed, seeding" : "Downloading"
}
<br><strong>Peers:</strong> ${torrent.numPeers}
<br>
<br><strong>Downloaded:</strong> ${(
torrent.downloaded /
(1024 * 1024)
).toFixed(2)} MB / ${(torrent.length / (1024 * 1024)).toFixed(
2
)} MB (${progress}%)
<br><strong>Speed:</strong> ${(
torrent.downloadSpeed /
(1024 * 1024)
).toFixed(2)} MB/s
<br><strong>ETA:</strong> ${torrent.timeRemaining} seconds
<br>
<br><strong>Uploaded:</strong> ${(
torrent.uploaded /
(1024 * 1024)
).toFixed(2)} MB
<br><strong>Ratio:</strong> ${torrent.ratio.toFixed(2)}`;
return; return;
} }
downloadResult.innerHTML = "File not found. Please check the mnemonic."; const url = URL.createObjectURL(blob);
}, 1000); const a = document.createElement("a");
a.href = url;
a.download = torrent.files[0].name;
a.click();
downloadResult.innerHTML = `File downloaded: <strong>${torrent.files[0].name}</strong>`;
});
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:
<strong>${torrent.files[0].name}</strong>
<br>
<br><strong>Status:</strong> ${
torrent.done ? "Completed, seeding" : "Downloading"
}
<br><strong>Peers:</strong> ${torrent.numPeers}
<br>
<br><strong>Downloaded:</strong> ${(
torrent.downloaded /
(1024 * 1024)
).toFixed(2)} MB / ${(torrent.length / (1024 * 1024)).toFixed(
2
)} MB (${progress}%)
<br><strong>Speed:</strong> ${(
torrent.downloadSpeed /
(1024 * 1024)
).toFixed(2)} MB/s
<br><strong>ETA:</strong> ${torrent.timeRemaining} seconds
<br>
<br><strong>Uploaded:</strong> ${(
torrent.uploaded /
(1024 * 1024)
).toFixed(2)} MB
<br><strong>Ratio:</strong> ${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", () => { 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) { if (mnemonic) {
const mnemonicInput = document.getElementById("mnemonicInput"); const mnemonicInput = document.getElementById("mnemonicInput");
const downloadButton = document.getElementById("downloadButton"); const downloadButton = document.getElementById("downloadButton");

View file

@ -1,8 +1,16 @@
import { Server } from "bittorrent-tracker"; import { Server } from "bittorrent-tracker";
import dotenv from "dotenv";
dotenv.config();
const PORT = process.env.TRACKER_PORT || 8106; const PORT = process.env.TRACKER_PORT || 8106;
const HOST = process.env.TRACKER_HOST || "localhost"; 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({ const server = new Server({
udp: false, udp: false,
http: false, http: false,
@ -19,7 +27,13 @@ server.on("warning", (err) => {
}); });
server.on("listening", () => { 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}`);
}

View file

@ -29,13 +29,12 @@
<div class="section" id="uploadSection"> <div class="section" id="uploadSection">
<h2>Share File</h2> <h2>Share File</h2>
<input type="file" id="fileInput" /> <input type="file" id="fileInput" />
<button id="uploadButton" onclick="uploadFile()">Share</button> <button id="uploadButton">Share</button>
<div class="result" id="uploadResult"></div> <div class="result" id="uploadResult"></div>
<div class="result" id="uploadStats"></div> <div class="result" id="uploadStats"></div>
<button <button
id="copyButton" id="copyButton"
style="display: none" style="display: none"
onclick="copyToClipboard()"
> >
Copy URL Copy URL
</button> </button>
@ -43,7 +42,7 @@
<div class="section" id="downloadSection"> <div class="section" id="downloadSection">
<h2>Receive File</h2> <h2>Receive File</h2>
<input type="text" id="mnemonicInput" placeholder="Enter mnemonic" /> <input type="text" id="mnemonicInput" placeholder="Enter mnemonic" />
<button id="downloadButton" onclick="downloadFile()">Receive</button> <button id="downloadButton">Receive</button>
<div class="progress" id="downloadProgress"> <div class="progress" id="downloadProgress">
<div class="progress-bar" id="downloadProgressBar">0%</div> <div class="progress-bar" id="downloadProgressBar">0%</div>
</div> </div>
@ -71,10 +70,7 @@
</p> </p>
</div> </div>
</div> </div>
<script> <div id="config" data-tracker-url="<%= trackerUrl %>" data-mnemonic="<%= mnemonic %>" style="display: none;"></div>
const trackerUrl = "<%= trackerUrl %>";
const mnemonic = "<%= mnemonic %>";
</script>
<script src="/js/index.js"></script> <script src="/js/index.js"></script>
</body> </body>
</html> </html>