const { Server } = require('socket.io'); const express = require('express'); const cors = require('cors'); const http = require('http'); const dotenv = require('dotenv'); const helmet = require('helmet'); const rateLimit = require('express-rate-limit'); const Joi = require('joi'); const validator = require('validator'); const { v4: uuidv4 } = require('uuid'); dotenv.config(); const app = express(); const server = http.createServer(app); // Security middlewares app.use(helmet({ contentSecurityPolicy: { directives: { defaultSrc: ["'self'"], styleSrc: ["'self'", "'unsafe-inline'"], scriptSrc: ["'self'"], imgSrc: ["'self'", "data:", "https:"], connectSrc: ["'self'", "wss:", "ws:"], fontSrc: ["'self'"], objectSrc: ["'none'"], mediaSrc: ["'self'"], frameSrc: ["'none'"], }, }, crossOriginEmbedderPolicy: false })); // Rate limiting const limiter = rateLimit({ windowMs: 15 * 60 * 1000, // 15 minutos max: 100, // Límite de 100 requests por ventana de tiempo por IP message: 'Too many requests from this IP, please try again later.', standardHeaders: true, legacyHeaders: false, }); app.use(limiter); // CORS configuration const corsOptions = { origin: process.env.CLIENT_URL || ["http://localhost:3000", "https://localhost:3000"], methods: ["GET", "POST"], credentials: true }; app.use(cors(corsOptions)); app.use(express.json({ limit: '10mb' })); const io = new Server(server, { cors: corsOptions, transports: ['websocket', 'polling'], allowEIO3: true }); // Validation schemas const roomJoinSchema = Joi.object({ email: Joi.string().email().required(), room: Joi.string().pattern(/^[a-zA-Z0-9-_]+$/).min(3).max(50).required() }); const callSchema = Joi.object({ to: Joi.string().required(), offer: Joi.object().required() }); const callAcceptedSchema = Joi.object({ to: Joi.string().required(), ans: Joi.object().required() }); app.get("/", (req, res) => { res.json({ message: `${process.env.APP_NAME || 'VideoPeersJS'} Server`, status: "running", timestamp: new Date().toISOString() }); }); app.get("/health", (req, res) => { res.status(200).json({ status: "healthy" }); }); const emailToSocket = new Map(); const socketToEmail = new Map(); const rooms = new Map(); // Helper function to sanitize input function sanitizeInput(input) { if (typeof input === 'string') { return validator.escape(input.trim()); } return input; } // Helper function to validate room access function validateRoomAccess(socket, room) { const userEmail = socketToEmail.get(socket.id); if (!userEmail) return false; const roomData = rooms.get(room); return roomData && roomData.participants.has(userEmail); } io.on("connection", (socket) => { console.log(`Socket Connected: ${socket.id} at ${new Date().toISOString()}`); // Set a timeout for socket connection const connectionTimeout = setTimeout(() => { socket.disconnect(true); }, 30000); // 30 segundos socket.on("room:join", (data) => { try { // Validate input const { error, value } = roomJoinSchema.validate(data); if (error) { let errorMessage = "Invalid room join data"; if (error.details[0]?.context?.key === 'room') { errorMessage = "Room ID must contain only letters, numbers, hyphens, and underscores (3-50 characters)"; } else if (error.details[0]?.context?.key === 'email') { errorMessage = "Please provide a valid email address"; } socket.emit("error", { message: errorMessage }); return; } const { email, room } = value; // Sanitize inputs const sanitizedEmail = sanitizeInput(email); const sanitizedRoom = sanitizeInput(room); // Additional email validation if (!validator.isEmail(sanitizedEmail)) { socket.emit("error", { message: "Invalid email format" }); return; } // Clear connection timeout clearTimeout(connectionTimeout); // Remove old socket mapping if exists const oldSocketId = emailToSocket.get(sanitizedEmail); if (oldSocketId && oldSocketId !== socket.id) { socketToEmail.delete(oldSocketId); io.sockets.sockets.get(oldSocketId)?.leave(sanitizedRoom); } emailToSocket.set(sanitizedEmail, socket.id); socketToEmail.set(socket.id, sanitizedEmail); // Manage room data if (!rooms.has(sanitizedRoom)) { rooms.set(sanitizedRoom, { id: uuidv4(), participants: new Set(), createdAt: new Date() }); } const roomData = rooms.get(sanitizedRoom); roomData.participants.add(sanitizedEmail); socket.join(sanitizedRoom); // Notify other users in the room socket.to(sanitizedRoom).emit("user:joined", { email: sanitizedEmail, id: socket.id, timestamp: new Date().toISOString() }); // Confirm room join to the user socket.emit("room:join", { email: sanitizedEmail, room: sanitizedRoom, participants: Array.from(roomData.participants), timestamp: new Date().toISOString() }); } catch (error) { console.error("Error in room:join:", error); socket.emit("error", { message: "Internal server error" }); } }); socket.on("user:call", (data) => { try { const { error, value } = callSchema.validate(data); if (error) { socket.emit("error", { message: "Invalid call data" }); return; } const { to, offer } = value; // Verify that the target socket exists const targetSocket = io.sockets.sockets.get(to); if (!targetSocket) { socket.emit("error", { message: "Target user not found" }); return; } io.to(to).emit("incoming:call", { from: socket.id, offer, timestamp: new Date().toISOString() }); } catch (error) { console.error("Error in user:call:", error); socket.emit("error", { message: "Internal server error" }); } }); socket.on("call:accepted", (data) => { try { const { error, value } = callAcceptedSchema.validate(data); if (error) { socket.emit("error", { message: "Invalid call acceptance data" }); return; } const { to, ans } = value; const targetSocket = io.sockets.sockets.get(to); if (!targetSocket) { socket.emit("error", { message: "Target user not found" }); return; } io.to(to).emit("call:accepted", { from: socket.id, ans, timestamp: new Date().toISOString() }); } catch (error) { console.error("Error in call:accepted:", error); socket.emit("error", { message: "Internal server error" }); } }); socket.on("peer:nego:needed", (data) => { try { const { to, offer } = data; if (!to || !offer) { socket.emit("error", { message: "Invalid negotiation data" }); return; } const targetSocket = io.sockets.sockets.get(to); if (!targetSocket) { socket.emit("error", { message: "Target user not found" }); return; } io.to(to).emit("peer:nego:needed", { from: socket.id, offer, timestamp: new Date().toISOString() }); } catch (error) { console.error("Error in peer:nego:needed:", error); socket.emit("error", { message: "Internal server error" }); } }); socket.on("peer:nego:done", (data) => { try { const { to, ans } = data; if (!to || !ans) { socket.emit("error", { message: "Invalid negotiation data" }); return; } const targetSocket = io.sockets.sockets.get(to); if (!targetSocket) { socket.emit("error", { message: "Target user not found" }); return; } io.to(to).emit("peer:nego:final", { from: socket.id, ans, timestamp: new Date().toISOString() }); } catch (error) { console.error("Error in peer:nego:done:", error); socket.emit("error", { message: "Internal server error" }); } }); socket.on("call:end", (data) => { try { const { to } = data; if (!to) { socket.emit("error", { message: "Invalid call end data" }); return; } const targetSocket = io.sockets.sockets.get(to); if (targetSocket) { io.to(to).emit("call:end", { from: socket.id, timestamp: new Date().toISOString() }); } } catch (error) { console.error("Error in call:end:", error); socket.emit("error", { message: "Internal server error" }); } }); socket.on("call:initiated", (data) => { try { const { to } = data; if (!to) { socket.emit("error", { message: "Invalid call initiation data" }); return; } const targetSocket = io.sockets.sockets.get(to); if (targetSocket) { io.to(to).emit("call:initiated", { from: socket.id, timestamp: new Date().toISOString() }); } } catch (error) { console.error("Error in call:initiated:", error); socket.emit("error", { message: "Internal server error" }); } }); socket.on("disconnect", (reason) => { try { console.log(`Socket Disconnected: ${socket.id}, Reason: ${reason}`); const email = socketToEmail.get(socket.id); if (email) { emailToSocket.delete(email); socketToEmail.delete(socket.id); // Remove from all rooms rooms.forEach((roomData, roomName) => { if (roomData.participants.has(email)) { roomData.participants.delete(email); // If room is empty, delete it if (roomData.participants.size === 0) { rooms.delete(roomName); } else { // Notify other participants socket.to(roomName).emit("user:left", { email, timestamp: new Date().toISOString() }); } } }); } clearTimeout(connectionTimeout); } catch (error) { console.error("Error in disconnect:", error); } }); }); // Error handling middleware app.use((err, req, res, next) => { console.error('Error:', err); res.status(500).json({ message: 'Internal server error', timestamp: new Date().toISOString() }); }); const PORT = process.env.PORT || 8000; server.listen(PORT, () => { console.log(`🚀 Server running on port ${PORT}`); console.log(`🕒 Started at ${new Date().toISOString()}`); });