383 líneas
12 KiB
JavaScript
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()}`);
|
|
}); |