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 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();

43
package-lock.json generated
View file

@ -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"
},

View file

@ -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"
}
}

View file

@ -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() {
<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.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: <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)}`;
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: <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", () => {
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");

View file

@ -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}`);
}

View file

@ -29,13 +29,12 @@
<div class="section" id="uploadSection">
<h2>Share File</h2>
<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="uploadStats"></div>
<button
id="copyButton"
style="display: none"
onclick="copyToClipboard()"
>
Copy URL
</button>
@ -43,7 +42,7 @@
<div class="section" id="downloadSection">
<h2>Receive File</h2>
<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-bar" id="downloadProgressBar">0%</div>
</div>
@ -71,10 +70,7 @@
</p>
</div>
</div>
<script>
const trackerUrl = "<%= trackerUrl %>";
const mnemonic = "<%= mnemonic %>";
</script>
<div id="config" data-tracker-url="<%= trackerUrl %>" data-mnemonic="<%= mnemonic %>" style="display: none;"></div>
<script src="/js/index.js"></script>
</body>
</html>