initial commit

Signed-off-by: ale <ale@manalejandro.com>
Este commit está contenido en:
ale
2025-09-16 01:54:29 +02:00
padre 9d013a7c87
commit 6d1dd42e6d
Se han modificado 44 ficheros con 1719 adiciones y 11509 borrados

Ver fichero

@@ -1 +1,4 @@
PORT=8080
PORT=8000
CLIENT_URL=http://localhost:3000
NODE_ENV=development
APP_NAME=VideoPeersJS

4
server/.env.example Archivo normal
Ver fichero

@@ -0,0 +1,4 @@
PORT=8000
CLIENT_URL=http://localhost:3000
NODE_ENV=development
APP_NAME=VideoPeersJS

4
server/.gitignore vendido
Ver fichero

@@ -1 +1,3 @@
node_modules
node_modules
*.lock
*-lock.json

Ver fichero

@@ -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

La diferencia del archivo ha sido suprimido porque es demasiado grande Cargar Diff

Ver fichero

@@ -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"
}
}

Ver fichero

@@ -1,11 +0,0 @@
{
"version": 2,
"builds": [{
"src": "*.js",
"use": "@vercel/node"
}],
"routes": [{
"src": "/(.*)",
"dest": "/"
}]
}