@@ -1 +1,4 @@
|
||||
PORT=8080
|
||||
PORT=8000
|
||||
CLIENT_URL=http://localhost:3000
|
||||
NODE_ENV=development
|
||||
APP_NAME=VideoPeersJS
|
||||
4
server/.env.example
Archivo normal
4
server/.env.example
Archivo normal
@@ -0,0 +1,4 @@
|
||||
PORT=8000
|
||||
CLIENT_URL=http://localhost:3000
|
||||
NODE_ENV=development
|
||||
APP_NAME=VideoPeersJS
|
||||
4
server/.gitignore
vendido
4
server/.gitignore
vendido
@@ -1 +1,3 @@
|
||||
node_modules
|
||||
node_modules
|
||||
*.lock
|
||||
*-lock.json
|
||||
376
server/index.js
376
server/index.js
@@ -3,63 +3,381 @@ 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);
|
||||
const io = new Server(server, {
|
||||
cors: true
|
||||
|
||||
// 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(cors());
|
||||
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.send("Hello World");
|
||||
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}`);
|
||||
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 => {
|
||||
const { email, room } = data;
|
||||
emailToSocket.set(email, socket.id);
|
||||
socketToEmail.set(socket.id, email);
|
||||
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;
|
||||
}
|
||||
|
||||
socket.join(room);
|
||||
io.to(room).emit("user:joined", { email, id: socket.id });
|
||||
const { email, room } = value;
|
||||
|
||||
// Sanitize inputs
|
||||
const sanitizedEmail = sanitizeInput(email);
|
||||
const sanitizedRoom = sanitizeInput(room);
|
||||
|
||||
// emits a 'room:joined' event back to the client
|
||||
// that just joined the room.
|
||||
io.to(socket.id).emit("room:join", data);
|
||||
// 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", ({ to, offer }) => {
|
||||
io.to(to).emit("incoming:call", { from: socket.id, offer });
|
||||
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", ({ to, ans }) => {
|
||||
io.to(to).emit("call:accepted", { from: socket.id, ans });
|
||||
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", ({ to, offer }) => {
|
||||
io.to(to).emit("peer:nego:needed", { from: socket.id, offer });
|
||||
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", ({ to, ans }) => {
|
||||
io.to(to).emit("peer:nego:final", { from: socket.id, ans });
|
||||
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", ({ to }) => {
|
||||
io.to(to).emit("call:end", { from: socket.id });
|
||||
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", ({ to }) => {
|
||||
io.to(to).emit("call:initiated", { from: socket.id });
|
||||
});
|
||||
})
|
||||
socket.on("call:initiated", (data) => {
|
||||
try {
|
||||
const { to } = data;
|
||||
if (!to) {
|
||||
socket.emit("error", { message: "Invalid call initiation data" });
|
||||
return;
|
||||
}
|
||||
|
||||
server.listen(process.env.PORT, () => console.log(`Server has started.`));
|
||||
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()}`);
|
||||
});
|
||||
2257
server/package-lock.json
generado
2257
server/package-lock.json
generado
La diferencia del archivo ha sido suprimido porque es demasiado grande
Cargar Diff
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "server",
|
||||
"name": "videopeersjs-server",
|
||||
"version": "1.0.0",
|
||||
"description": "",
|
||||
"description": "VideoPeersJS - Real-time P2P video chat server",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"test": "echo \"Error: no test specified\" && exit 1",
|
||||
@@ -12,12 +12,17 @@
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"devDependencies": {
|
||||
"nodemon": "^3.0.1"
|
||||
"nodemon": "^3.1.4"
|
||||
},
|
||||
"dependencies": {
|
||||
"cors": "^2.8.5",
|
||||
"dotenv": "^16.3.1",
|
||||
"express": "^4.18.2",
|
||||
"socket.io": "^4.7.2"
|
||||
"dotenv": "^16.4.5",
|
||||
"express": "^4.19.2",
|
||||
"express-rate-limit": "^7.4.0",
|
||||
"helmet": "^7.1.0",
|
||||
"joi": "^17.13.3",
|
||||
"socket.io": "^4.7.5",
|
||||
"uuid": "^10.0.0",
|
||||
"validator": "^13.12.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +0,0 @@
|
||||
{
|
||||
"version": 2,
|
||||
"builds": [{
|
||||
"src": "*.js",
|
||||
"use": "@vercel/node"
|
||||
}],
|
||||
"routes": [{
|
||||
"src": "/(.*)",
|
||||
"dest": "/"
|
||||
}]
|
||||
}
|
||||
Referencia en una nueva incidencia
Block a user