Files
videopeersjs/server/index.js
2025-09-16 01:54:29 +02:00

383 líneas
12 KiB
JavaScript

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().alphanum().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) {
socket.emit("error", { message: "Invalid room join data" });
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()}`);
});