From 5ca7606b6c42ea7032d01875f37f65e498a006de Mon Sep 17 00:00:00 2001 From: Keep Creating Online <53631862+PR0M3TH3AN@users.noreply.github.com> Date: Sat, 22 Feb 2025 19:53:16 -0500 Subject: [PATCH] update --- public/components/profile-modal.html | 10 - public/index.html | 199 ++++------- public/js/nostr.js | 33 ++ server.js | 502 ++++++++++++++++++--------- 4 files changed, 435 insertions(+), 309 deletions(-) create mode 100644 public/js/nostr.js diff --git a/public/components/profile-modal.html b/public/components/profile-modal.html index dcedb83..471df0d 100644 --- a/public/components/profile-modal.html +++ b/public/components/profile-modal.html @@ -3,21 +3,15 @@ class="hidden fixed inset-0 z-50" style="background: transparent" > - - - - - - +
- - - diff --git a/public/js/nostr.js b/public/js/nostr.js new file mode 100644 index 0000000..94bc9ab --- /dev/null +++ b/public/js/nostr.js @@ -0,0 +1,33 @@ +// public/js/nostr.js +import { nip19, SimplePool } from "https://esm.sh/nostr-tools@1.8.3"; + +class NostrClient { + constructor() { + this.pool = new SimplePool(); + this.pubkey = null; + this.relays = [ + "wss://relay.damus.io", + "wss://nos.lol", + "wss://relay.snort.social", + "wss://relay.primal.net", + "wss://relay.nostr.band", + ]; + } + + async login() { + if (!window.nostr) { + throw new Error("Please install a Nostr extension (e.g. Alby)."); + } + const pubkey = await window.nostr.getPublicKey(); + this.pubkey = pubkey; + return pubkey; + } + + logout() { + this.pubkey = null; + } +} + +export const nostrClient = new NostrClient(); +window.nostrClient = nostrClient; +window.NostrTools = { nip19, SimplePool }; diff --git a/server.js b/server.js index 4ce1126..c300772 100644 --- a/server.js +++ b/server.js @@ -1,95 +1,57 @@ -import { EventEmitter } from 'events'; +// server.js +import { EventEmitter } from "events"; EventEmitter.defaultMaxListeners = 50; -import express from 'express'; -import WebTorrent from 'webtorrent'; -import fs from 'fs'; -import { promises as fsPromises } from 'fs'; -import path from 'path'; -import { fileURLToPath } from 'url'; -import rateLimit from 'express-rate-limit'; -import winston from 'winston'; +import express from "express"; +import WebTorrent from "webtorrent"; +import fs from "fs"; +import { promises as fsPromises } from "fs"; +import path from "path"; +import { fileURLToPath } from "url"; +import rateLimit from "express-rate-limit"; +import winston from "winston"; +import { exec } from "child_process"; -// Set up Winston logger +// Set up Winston logger (debug level) const logger = winston.createLogger({ - level: 'info', + level: "debug", format: winston.format.combine( winston.format.timestamp(), winston.format.printf( ({ timestamp, level, message }) => `${timestamp} ${level}: ${message}` ) ), - transports: [new winston.transports.Console()] + transports: [new winston.transports.Console()], }); const app = express(); const port = process.env.PORT || 3000; const client = new WebTorrent(); - -// Increase max listeners for our client client.setMaxListeners(50); // ES module __dirname workaround const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); -// Rate limiting middleware (100 requests per minute per IP) +// Rate limiting middleware const limiter = rateLimit({ windowMs: 60 * 1000, - max: 100 + max: 100, }); app.use(limiter); - -// Serve static files and parse JSON bodies -app.use(express.static('public')); app.use(express.json()); -// Load configuration from config.json -const CONFIG_FILE = path.join(__dirname, 'config.json'); -let config = { storageDir: path.join(__dirname, 'downloads') }; +// --- Global Variables and Helper Functions --- -try { - if (fs.existsSync(CONFIG_FILE)) { - const configData = fs.readFileSync(CONFIG_FILE, 'utf-8'); - config = JSON.parse(configData); - // Resolve storageDir relative to __dirname - config.storageDir = path.resolve(__dirname, config.storageDir); - } else { - fs.writeFileSync(CONFIG_FILE, JSON.stringify({ storageDir: './downloads' }, null, 2)); - } -} catch (err) { - logger.error('Error reading config file, using default config: ' + err); -} +// Configuration +const CONFIG_FILE = path.join(__dirname, "config.json"); +let config = { storageDir: path.join(__dirname, "downloads") }; -let STORAGE_DIR = config.storageDir; -if (!fs.existsSync(STORAGE_DIR)) { - fs.mkdirSync(STORAGE_DIR, { recursive: true }); -} - -// JSON file to store torrent metadata -const TORRENTS_FILE = path.join(__dirname, 'torrents.json'); +// Torrents data +const TORRENTS_FILE = path.join(__dirname, "torrents.json"); let savedTorrents = []; -try { - if (fs.existsSync(TORRENTS_FILE)) { - const data = fs.readFileSync(TORRENTS_FILE, 'utf-8'); - savedTorrents = JSON.parse(data); - logger.info(`Loaded saved torrents: ${JSON.stringify(savedTorrents)}`); - } else { - logger.info('No torrents.json file found. Starting with an empty list.'); - } -} catch (err) { - logger.error('Error loading torrents file: ' + err); -} -function saveTorrents() { - try { - fs.writeFileSync(TORRENTS_FILE, JSON.stringify(savedTorrents, null, 2)); - } catch (err) { - logger.error('Error saving torrents file: ' + err); - } -} - -// Helper: Check if a path exists asynchronously +// Helper: Check if a path exists async function pathExists(p) { try { await fsPromises.access(p); @@ -99,124 +61,223 @@ async function pathExists(p) { } } -// GET /torrents - Return both active and paused torrents -app.get('/torrents', (req, res) => { +// Load configuration from file +async function loadConfig() { try { - // Active torrents: those in the client - const active = client.torrents.map(t => ({ + if (await pathExists(CONFIG_FILE)) { + const data = await fsPromises.readFile(CONFIG_FILE, "utf-8"); + config = JSON.parse(data); + config.storageDir = path.resolve(__dirname, config.storageDir); + } else { + await fsPromises.writeFile( + CONFIG_FILE, + JSON.stringify({ storageDir: "./downloads" }, null, 2) + ); + } + } catch (err) { + logger.error("Error reading config file, using default config: " + err); + } +} +await loadConfig(); + +// Load saved torrents from file +async function loadSavedTorrentsFromFile() { + try { + if (fs.existsSync(TORRENTS_FILE)) { + const data = await fsPromises.readFile(TORRENTS_FILE, "utf-8"); + savedTorrents = JSON.parse(data); + logger.info(`Loaded saved torrents: ${JSON.stringify(savedTorrents)}`); + } else { + logger.info("No torrents.json file found. Starting with an empty list."); + } + } catch (err) { + logger.error("Error loading torrents file: " + err); + } +} +await loadSavedTorrentsFromFile(); + +// Save torrents to file +async function saveTorrents() { + try { + await fsPromises.writeFile( + TORRENTS_FILE, + JSON.stringify(savedTorrents, null, 2) + ); + } catch (err) { + logger.error("Error saving torrents file: " + err); + } +} + +// Ensure storage directory exists +let STORAGE_DIR = config.storageDir; +if (!fs.existsSync(STORAGE_DIR)) { + fs.mkdirSync(STORAGE_DIR, { recursive: true }); +} + +// Attach torrent debug events +function attachTorrentDebugEvents(torrent) { + torrent.on("wire", (wire, addr) => { + logger.debug(`Torrent ${torrent.infoHash} connected to peer: ${addr}`); + }); + torrent.on("upload", (bytes) => { + logger.debug(`Torrent ${torrent.infoHash} uploaded ${bytes} bytes`); + }); + torrent.on("download", (bytes) => { + logger.debug(`Torrent ${torrent.infoHash} downloaded ${bytes} bytes`); + }); +} + +// --- Resume Active Torrents on Startup --- +// For each saved torrent that is not paused, add (or seed) it. +async function resumeActiveTorrents() { + logger.info("Resuming active torrents..."); + for (const torrent of savedTorrents) { + if (!torrent.paused) { + try { + const torrentPath = path.join(STORAGE_DIR, torrent.name); + const exists = await pathExists(torrentPath); + if (exists) { + client.seed( + torrentPath, + { name: torrent.name, keepSeeding: true }, + (torrentInstance) => { + attachTorrentDebugEvents(torrentInstance); + logger.info( + `Resumed seeding torrent: ${torrentInstance.infoHash}` + ); + } + ); + } else { + client.add( + torrent.magnet, + { path: STORAGE_DIR, keepSeeding: true }, + (torrentInstance) => { + attachTorrentDebugEvents(torrentInstance); + logger.info( + `Resumed downloading torrent: ${torrentInstance.infoHash}` + ); + } + ); + } + } catch (err) { + logger.error(`Error resuming torrent ${torrent.name}: ${err}`); + } + } + } +} +await resumeActiveTorrents(); + +// --- API Routes --- +// GET /config - Return current configuration as JSON +app.get("/config", async (req, res) => { + try { + res.json({ storageDir: config.storageDir }); + } catch (err) { + logger.error("Error getting config: " + err); + res.status(500).json({ error: "Error reading config" }); + } +}); + +// GET /torrents - Return both active and paused torrents +app.get("/torrents", (req, res) => { + try { + const active = client.torrents.map((t) => ({ infoHash: t.infoHash, name: t.name, progress: t.progress, numPeers: t.numPeers, downloaded: t.downloaded, uploaded: t.uploaded, - paused: false + paused: false, })); - // Paused torrents: those saved with paused: true const paused = savedTorrents - .filter(s => s.paused) - .map(s => ({ + .filter((s) => s.paused) + .map((s) => ({ infoHash: s.infoHash, name: s.name, progress: 1, numPeers: 0, downloaded: 0, uploaded: 0, - paused: true + paused: true, })); const torrents = [...active, ...paused]; - logger.info('GET /torrents returning: ' + JSON.stringify(torrents)); + logger.info("GET /torrents returning: " + JSON.stringify(torrents)); res.json(torrents); } catch (err) { - logger.error('Error listing torrents: ' + err); - res.status(500).json({ error: 'Error listing torrents' }); + logger.error("Error listing torrents: " + err); + res.status(500).json({ error: "Error listing torrents" }); } }); -// POST /pause/:infoHash - Pause a torrent (remove it from the active client and mark as paused) -app.post('/pause/:infoHash', (req, res) => { +// POST /pause/:infoHash - Pause a torrent +app.post("/pause/:infoHash", async (req, res) => { const infoHash = req.params.infoHash; const torrent = client.get(infoHash); if (!torrent) { - return res.status(400).json({ error: 'Torrent not active or already paused' }); + return res + .status(400) + .json({ error: "Torrent not active or already paused" }); } - client.remove(infoHash, { destroyStore: false }, err => { + client.remove(infoHash, { destroyStore: false }, async (err) => { if (err) { - logger.error('Error pausing torrent: ' + err); - return res.status(500).json({ error: 'Error pausing torrent' }); + logger.error("Error pausing torrent: " + err); + return res.status(500).json({ error: "Error pausing torrent" }); } - // Update savedTorrents to mark this torrent as paused. - const entry = savedTorrents.find(s => s.infoHash === infoHash); + const entry = savedTorrents.find((s) => s.infoHash === infoHash); if (entry) entry.paused = true; - saveTorrents(); + await saveTorrents(); logger.info(`Torrent paused: ${infoHash}`); - res.json({ message: 'Torrent paused' }); + res.json({ message: "Torrent paused" }); }); }); -// POST /resume/:infoHash - Resume a paused torrent (re-add it to the client) -app.post('/resume/:infoHash', async (req, res) => { +// POST /resume/:infoHash - Resume a paused torrent +app.post("/resume/:infoHash", async (req, res) => { const infoHash = req.params.infoHash; - const entry = savedTorrents.find(s => s.infoHash === infoHash); + const entry = savedTorrents.find((s) => s.infoHash === infoHash); if (!entry) { - return res.status(400).json({ error: 'Torrent not found in saved data' }); + return res.status(400).json({ error: "Torrent not found in saved data" }); } entry.paused = false; - saveTorrents(); + await saveTorrents(); const torrentPath = path.join(STORAGE_DIR, entry.name); const exists = await pathExists(torrentPath); try { if (exists) { - client.seed(torrentPath, { name: entry.name, keepSeeding: true }, torrent => { - logger.info(`Torrent resumed (seeding from file): ${torrent.infoHash}`); - res.json({ message: 'Torrent resumed' }); - }); + client.seed( + torrentPath, + { name: entry.name, keepSeeding: true }, + (torrent) => { + attachTorrentDebugEvents(torrent); + logger.info( + `Torrent resumed (seeding from file): ${torrent.infoHash}` + ); + res.json({ message: "Torrent resumed" }); + } + ); } else { - client.add(entry.magnet, { path: STORAGE_DIR, keepSeeding: true }, torrent => { - logger.info(`Torrent resumed (downloading): ${torrent.infoHash}`); - res.json({ message: 'Torrent resumed' }); - }); + client.add( + entry.magnet, + { path: STORAGE_DIR, keepSeeding: true }, + (torrent) => { + attachTorrentDebugEvents(torrent); + logger.info(`Torrent resumed (downloading): ${torrent.infoHash}`); + res.json({ message: "Torrent resumed" }); + } + ); } } catch (err) { - logger.error('Error resuming torrent: ' + err); - res.status(500).json({ error: 'Error resuming torrent' }); + logger.error("Error resuming torrent: " + err); + res.status(500).json({ error: "Error resuming torrent" }); } }); -// Load saved torrents concurrently on startup. -// For each saved torrent, if the expected file/folder exists, we seed from that location; -// otherwise, we add the torrent to download. -// (Paused torrents will have "paused": true in savedTorrents and should not be added automatically.) -async function loadSavedTorrents() { - await Promise.all( - savedTorrents.map(async torrentData => { - // Only auto-load if not marked paused. - if (torrentData.paused) return; - const torrentPath = path.join(STORAGE_DIR, torrentData.name); - const exists = await pathExists(torrentPath); - try { - if (exists) { - client.seed(torrentPath, { name: torrentData.name, keepSeeding: true }, torrent => { - logger.info(`Seeding torrent from existing files: ${torrent.infoHash}`); - }); - } else { - client.add(torrentData.magnet, { path: STORAGE_DIR, keepSeeding: true }, torrent => { - logger.info(`Downloading torrent: ${torrent.infoHash}`); - }); - } - } catch (err) { - logger.error('Error loading saved torrent on startup: ' + err); - } - }) - ); -} -loadSavedTorrents(); - -// POST /config - Update storage directory (with input validation) -app.post('/config', (req, res) => { +// POST /config - Update storage directory +app.post("/config", async (req, res) => { const { storageDir } = req.body; - if (!storageDir || typeof storageDir !== 'string') { - return res.status(400).json({ error: 'Invalid storageDir provided' }); + if (!storageDir || typeof storageDir !== "string") { + return res.status(400).json({ error: "Invalid storageDir provided" }); } try { STORAGE_DIR = path.resolve(__dirname, storageDir); @@ -224,83 +285,183 @@ app.post('/config', (req, res) => { if (!fs.existsSync(STORAGE_DIR)) { fs.mkdirSync(STORAGE_DIR, { recursive: true }); } - fs.writeFileSync(CONFIG_FILE, JSON.stringify({ storageDir }, null, 2)); - res.json({ message: 'Config updated', storageDir: STORAGE_DIR }); + await fsPromises.writeFile( + CONFIG_FILE, + JSON.stringify({ storageDir }, null, 2) + ); + res.json({ message: "Config updated", storageDir: STORAGE_DIR }); } catch (err) { - logger.error('Error updating config: ' + err); - res.status(500).json({ error: 'Error updating config' }); + logger.error("Error updating config: " + err); + res.status(500).json({ error: "Error updating config" }); } }); -// POST /add - Add a new torrent (with basic validation and duplicate check) -app.post('/add', (req, res) => { +// POST /add - Add a new torrent +app.post("/add", (req, res) => { const { magnet } = req.body; - if (!magnet || typeof magnet !== 'string') { - return res.status(400).json({ error: 'No valid magnet link provided' }); + if (!magnet || typeof magnet !== "string") { + return res.status(400).json({ error: "No valid magnet link provided" }); } - if (!magnet.startsWith('magnet:?')) { - return res.status(400).json({ error: 'Invalid magnet link format' }); + if (!magnet.startsWith("magnet:?")) { + return res.status(400).json({ error: "Invalid magnet link format" }); } - if (savedTorrents.some(t => t.magnet === magnet)) { - logger.info('Attempted to add duplicate torrent.'); - return res.status(409).json({ error: 'Torrent already added.' }); + if (savedTorrents.some((t) => t.magnet === magnet)) { + logger.info("Attempted to add duplicate torrent."); + return res.status(409).json({ error: "Torrent already added." }); } try { - client.add(magnet, { path: STORAGE_DIR, keepSeeding: true }, torrent => { + client.add(magnet, { path: STORAGE_DIR, keepSeeding: true }, (torrent) => { + attachTorrentDebugEvents(torrent); logger.info(`Torrent added: ${torrent.infoHash}`); const torrentData = { magnet, infoHash: torrent.infoHash, name: torrent.name, - paused: false + paused: false, }; savedTorrents.push(torrentData); saveTorrents(); res.json(torrentData); }); } catch (err) { - logger.error('Error adding torrent: ' + err); - res.status(500).json({ error: 'Error adding torrent' }); + logger.error("Error adding torrent: " + err); + res.status(500).json({ error: "Error adding torrent" }); } }); // DELETE /remove/:infoHash - Remove a torrent completely. -app.delete('/remove/:infoHash', (req, res) => { +app.delete("/remove/:infoHash", (req, res) => { const infoHash = req.params.infoHash; - if (!infoHash || typeof infoHash !== 'string') { - return res.status(400).json({ error: 'Invalid infoHash' }); + if (!infoHash || typeof infoHash !== "string") { + return res.status(400).json({ error: "Invalid infoHash" }); } const torrent = client.get(infoHash); - const savedData = savedTorrents.find(t => t.infoHash === infoHash); - const torrentName = (torrent && torrent.name) || (savedData && savedData.name); + const savedData = savedTorrents.find((t) => t.infoHash === infoHash); + const torrentName = + (torrent && torrent.name) || (savedData && savedData.name); if (!torrentName) { - return res.status(400).json({ error: 'Torrent name not found' }); + return res.status(400).json({ error: "Torrent name not found" }); } try { - client.remove(infoHash, { destroyStore: true }, async err => { + client.remove(infoHash, { destroyStore: true }, async (err) => { if (err) { - logger.error('Error removing torrent: ' + err); - return res.status(500).json({ error: 'Error removing torrent' }); + logger.error("Error removing torrent: " + err); + return res.status(500).json({ error: "Error removing torrent" }); } - savedTorrents = savedTorrents.filter(t => t.infoHash !== infoHash); - saveTorrents(); + savedTorrents = savedTorrents.filter((t) => t.infoHash !== infoHash); + await saveTorrents(); const torrentPath = path.join(STORAGE_DIR, torrentName); if (await pathExists(torrentPath)) { try { await fsPromises.rm(torrentPath, { recursive: true, force: true }); } catch (rmErr) { - logger.error('Error removing torrent files: ' + rmErr); + logger.error("Error removing torrent files: " + rmErr); } } logger.info(`Torrent removed: ${infoHash}`); - res.json({ message: 'Torrent removed' }); + res.json({ message: "Torrent removed" }); }); } catch (err) { - logger.error('Error processing removal: ' + err); - res.status(500).json({ error: 'Error processing removal' }); + logger.error("Error processing removal: " + err); + res.status(500).json({ error: "Error processing removal" }); } }); +// --- End API Routes --- + +// Serve static files from "public" +app.use(express.static("public")); + +// Log system limits periodically +function logSystemLimits() { + const platform = process.platform; + if (platform === "win32") { + logger.info("Running on Windows."); + const activeHandles = process._getActiveHandles + ? process._getActiveHandles().length + : "N/A"; + logger.info(`Active handles: ${activeHandles}`); + exec("netsh int tcp show global", (err, stdout, stderr) => { + if (err) { + logger.error(`Error running netsh: ${err.message}`); + } else { + logger.info(`TCP Global Settings:\n${stdout}`); + } + }); + } else if (platform === "linux") { + exec('bash -c "ulimit -n"', (err, stdout, stderr) => { + if (err) { + logger.error(`Error fetching file descriptor limit: ${err.message}`); + } else { + logger.info(`File descriptor limit: ${stdout.trim()}`); + } + }); + exec(`lsof -p ${process.pid} | wc -l`, (err, stdout, stderr) => { + if (err) { + logger.error(`Error counting open file descriptors: ${err.message}`); + } else { + logger.info(`Current open file descriptors: ${stdout.trim()}`); + } + }); + exec("cat /proc/sys/net/core/rmem_max", (err, stdout, stderr) => { + if (err) { + logger.error(`Error fetching rmem_max: ${err.message}`); + } else { + logger.info(`Receive buffer max (rmem_max): ${stdout.trim()}`); + } + }); + exec("cat /proc/sys/net/core/wmem_max", (err, stdout, stderr) => { + if (err) { + logger.error(`Error fetching wmem_max: ${err.message}`); + } else { + logger.info(`Send buffer max (wmem_max): ${stdout.trim()}`); + } + }); + } else if (platform === "darwin") { + exec("ulimit -n", (err, stdout, stderr) => { + if (err) { + logger.error(`Error fetching file descriptor limit: ${err.message}`); + } else { + logger.info(`File descriptor limit: ${stdout.trim()}`); + } + }); + exec(`lsof -p ${process.pid} | wc -l`, (err, stdout, stderr) => { + if (err) { + logger.error(`Error counting open file descriptors: ${err.message}`); + } else { + logger.info(`Current open file descriptors: ${stdout.trim()}`); + } + }); + exec("sysctl kern.ipc.maxsockbuf", (err, stdout, stderr) => { + if (err) { + logger.error(`Error fetching kern.ipc.maxsockbuf: ${err.message}`); + } else { + logger.info(`kern.ipc.maxsockbuf: ${stdout.trim()}`); + } + }); + exec("sysctl net.inet.tcp.recvspace", (err, stdout, stderr) => { + if (err) { + logger.error(`Error fetching net.inet.tcp.recvspace: ${err.message}`); + } else { + logger.info(`net.inet.tcp.recvspace: ${stdout.trim()}`); + } + }); + exec("sysctl net.inet.tcp.sendspace", (err, stdout, stderr) => { + if (err) { + logger.error(`Error fetching net.inet.tcp.sendspace: ${err.message}`); + } else { + logger.info(`net.inet.tcp.sendspace: ${stdout.trim()}`); + } + }); + } else { + logger.info( + `Platform ${platform} not specifically supported for system limits logging.` + ); + } +} +logSystemLimits(); +setInterval(logSystemLimits, 60000); + // Start the server const server = app.listen(port, () => { logger.info(`Server running on http://localhost:${port}`); @@ -308,19 +469,18 @@ const server = app.listen(port, () => { // Graceful shutdown function shutdown() { - logger.info('Shutting down server...'); + logger.info("Shutting down server..."); server.close(() => { - logger.info('HTTP server closed.'); - client.destroy(err => { + logger.info("HTTP server closed."); + client.destroy((err) => { if (err) { - logger.error('Error destroying WebTorrent client: ' + err); + logger.error("Error destroying WebTorrent client: " + err); } else { - logger.info('WebTorrent client destroyed.'); + logger.info("WebTorrent client destroyed."); } process.exit(0); }); }); } - -process.on('SIGINT', shutdown); -process.on('SIGTERM', shutdown); +process.on("SIGINT", shutdown); +process.on("SIGTERM", shutdown);