From c92c85b3f0645f04beb7f2c28853d4bb96b38ed9 Mon Sep 17 00:00:00 2001 From: "Max W." Date: Sat, 12 Apr 2025 01:24:09 +0200 Subject: [PATCH] Update server.js - Add rate limiting - Add paste size log --- server.js | 93 ++++++++++++++++++++++++++++++------------------------- 1 file changed, 50 insertions(+), 43 deletions(-) diff --git a/server.js b/server.js index 297cc23..8796d09 100644 --- a/server.js +++ b/server.js @@ -1,58 +1,59 @@ -// Import necessary modules const express = require('express'); const path = require('path'); -// --- Configuration --- -const PORT = process.env.PORT || 3000; // Use environment variable or default to 3000 -const PASTE_TTL_HOURS = 2; -const PASTE_TTL_MS = PASTE_TTL_HOURS * 60 * 60 * 1000; // Time-to-live in milliseconds +const PORT = process.env.PORT || 3000; +const PASTE_TTL_HOURS = 3; +const PASTE_TTL_MS = PASTE_TTL_HOURS * 60 * 60 * 1000; -// --- In-Memory Storage --- -// Using a Map for efficient key-based storage and deletion -const pastes = new Map(); // Stores { id: content } +const pastes = new Map(); +const ipLastPaste = new Map(); // Tracks the last paste time for each IP +const RATE_LIMIT_MS = 10 * 1000; // 10 seconds rate limit +const IP_CLEANUP_INTERVAL = 30 * 60 * 1000; // Clean up IP records every 30 minutes -// --- Express App Setup --- const app = express(); -// Middleware to parse plain text request bodies (like Hastebin) -// Limit set to 1MB, adjust as needed -app.use(express.text({ limit: '1mb', type: 'text/plain' })); -app.use(express.json()); // For JSON API responses +app.use(express.text({ limit: '10mb', type: 'text/plain' })); +app.use(express.json()); +app.use(express.static('static')); -// Serve static files from the 'public' directory -app.use(express.static('public')); - -// Function to generate a unique ID without crypto function generateUniqueId() { const timestamp = Date.now().toString(36); const randomPart = Math.random().toString(36).substring(2, 10); return `${timestamp}-${randomPart}`; } -// --- API Routes --- - -/** - * POST /api/pastes - * Creates a new paste. - * Expects the raw text content in the request body. - * Responds with the generated ID for the paste. - */ app.post('/api/pastes', (req, res) => { const content = req.body; + const clientIp = req.ip; + const now = Date.now(); + + // Check rate limit + if (ipLastPaste.has(clientIp)) { + const lastPasteTime = ipLastPaste.get(clientIp); + const timeElapsed = now - lastPasteTime; + + if (timeElapsed < RATE_LIMIT_MS) { + return res.status(429).json({ + error: 'Rate limit exceeded. Please wait before creating another paste.', + retryAfter: Math.ceil((RATE_LIMIT_MS - timeElapsed) / 1000) + }); + } + } - // Basic validation: Ensure content is present and is a string if (!content || typeof content !== 'string' || content.trim() === '') { return res.status(400).json({ error: 'Paste content cannot be empty.' }); } - // Generate a unique ID without using crypto + // Calculate paste size in bytes + const pasteSize = Buffer.byteLength(content, 'utf8'); + const sizeInKB = (pasteSize / 1024).toFixed(2); + const id = generateUniqueId(); - - // Store the paste content in the map pastes.set(id, content); - console.log(`[${new Date().toISOString()}] Paste created with ID: ${id}`); + ipLastPaste.set(clientIp, now); + + console.log(`[${new Date().toISOString()}] Paste created with ID: ${id} | Size: ${sizeInKB} KB (${pasteSize} bytes)`); - // Schedule the paste for deletion after the TTL setTimeout(() => { if (pastes.has(id)) { pastes.delete(id); @@ -60,21 +61,14 @@ app.post('/api/pastes', (req, res) => { } }, PASTE_TTL_MS); - // Respond with the ID res.status(201).json({ id: id }); }); -/** - * GET /api/pastes/:id - * Retrieves the content of a specific paste by its ID. - * Responds with the raw text content or 404 if not found. - */ app.get('/api/pastes/:id', (req, res) => { const id = req.params.id; if (pastes.has(id)) { const content = pastes.get(id); - // Send raw text content res.setHeader('Content-Type', 'text/plain; charset=utf-8'); res.status(200).send(content); } else { @@ -82,22 +76,35 @@ app.get('/api/pastes/:id', (req, res) => { } }); -// Serve the main HTML page for all other routes app.get('/', (req, res) => { res.sendFile(path.join(__dirname, 'public', 'index.html')); }); -// --- Start Server --- +// Periodically clean up the IP tracking map to prevent memory leak +setInterval(() => { + const now = Date.now(); + let cleanupCount = 0; + + // Remove IPs that haven't posted in the last hour (much longer than rate limit) + ipLastPaste.forEach((timestamp, ip) => { + if (now - timestamp > RATE_LIMIT_MS * 360) { // 1 hour = 360 * 10 seconds + ipLastPaste.delete(ip); + cleanupCount++; + } + }); + + if (cleanupCount > 0) { + console.log(`[${new Date().toISOString()}] Cleaned up ${cleanupCount} IP records from rate limiter`); + } +}, IP_CLEANUP_INTERVAL); + app.listen(PORT, () => { console.log(`Paste server listening on port ${PORT}`); console.log(`Pastes will be stored for ${PASTE_TTL_HOURS} hours.`); console.log(`Open http://localhost:${PORT} in your browser to use the application`); }); -// --- Graceful Shutdown (Optional but Recommended) --- -// Handles CTRL+C in the terminal process.on('SIGINT', () => { console.log('\nShutting down server...'); - // Perform any cleanup here if needed (though in-memory storage is lost anyway) process.exit(0); }); \ No newline at end of file