-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathserver.js
More file actions
275 lines (226 loc) · 11.4 KB
/
server.js
File metadata and controls
275 lines (226 loc) · 11.4 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
const express = require('express');
const http = require('http');
const socketIo = require('socket.io');
const crypto = require('crypto');
const bcrypt = require('bcrypt');
const rateLimit = require('express-rate-limit');
const app = express();
app.set('trust proxy', 1);
const server = http.createServer(app);
// CRITICAL: Socket.IO CORS must allow ngrok domain
const io = socketIo(server, {
cors: {
origin: "*", // ngrok URLs change, so we allow all but use token auth
methods: ["GET", "POST"],
credentials: true
},
maxHttpBufferSize: 1e6 // 1MB limit
});
app.use(express.json({ limit: '10kb' }));
app.use(express.static('public'));
// CRITICAL: Rate limiting to prevent attacks
const createRoomLimiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 5,
message: 'Too many rooms created, try again later',
standardHeaders: true,
legacyHeaders: false
});
const passwordLimiter = rateLimit({
windowMs: 15 * 60 * 1000,
max: 10,
message: 'Too many password attempts, try again later',
standardHeaders: true,
legacyHeaders: false
});
// Store active rooms
const rooms = new Map();
const ROOM_EXPIRY = 6 * 60 * 60 * 1000; // 6 hours (reasonable for a call with friends)
// Validation helpers
function isValidRoomId(roomId) {
return typeof roomId === 'string' && /^[a-f0-9]{32}$/.test(roomId);
}
function isValidPassword(password) {
return password &&
password.length >= 6
// /[A-Za-z]/.test(password) &&
// /[0-9]/.test(password);
}
// CRITICAL: Socket authentication middleware
io.use((socket, next) => {
const { roomId, token } = socket.handshake.auth;
if (!roomId || !isValidRoomId(roomId)) {
return next(new Error('Invalid room'));
}
const room = rooms.get(roomId);
if (!room || !room.active) {
return next(new Error('Room not found'));
}
// CRITICAL: Verify auth token
if (!token || token !== room.token) {
return next(new Error('Unauthorized'));
}
socket.roomId = roomId;
next();
});
// Clean expired rooms every hour
setInterval(() => {
const now = Date.now();
for (const [roomId, room] of rooms.entries()) {
if (now - room.createdAt > ROOM_EXPIRY) {
room.active = false;
rooms.delete(roomId);
console.log(`Expired room: ${roomId}`);
}
}
}, 60 * 60 * 1000);
// Create room with rate limiting
app.post('/api/create-room', createRoomLimiter, async (req, res) => {
const { password } = req.body;
if (!isValidPassword(password)) {
return res.status(400).json({
error: 'Password must be at least 6 characters'
});
}
try {
const roomId = crypto.randomBytes(16).toString('hex');
const hashedPassword = await bcrypt.hash(password, 12);
const token = crypto.randomBytes(32).toString('hex');
rooms.set(roomId, {
password: hashedPassword,
token: token,
participants: new Set(),
active: true,
createdAt: Date.now(),
maxParticipants: 10
});
// Support both HTTP and HTTPS (ngrok provides HTTPS)
const protocol = req.headers['x-forwarded-proto'] || req.protocol;
const host = req.get('host');
res.json({
roomId,
token, // Client needs this for socket auth
link: `${protocol}://${host}/room.html?id=${roomId}`
});
console.log(`✅ Room created: ${roomId} (expires in 6 hours)`);
} catch (error) {
console.error('Error creating room:', error);
res.status(500).json({ error: 'Server error' });
}
});
// Verify password with rate limiting
app.post('/api/verify-room', passwordLimiter, async (req, res) => {
const { roomId, password } = req.body;
if (!isValidRoomId(roomId)) {
return res.status(400).json({ error: 'Invalid room ID' });
}
const room = rooms.get(roomId);
if (!room) {
return res.status(404).json({ error: 'Room not found or expired' });
}
if (!room.active) {
return res.status(403).json({ error: 'Room has ended' });
}
// Check expiry
if (Date.now() - room.createdAt > ROOM_EXPIRY) {
room.active = false;
return res.status(403).json({ error: 'Room expired' });
}
try {
const isValid = await bcrypt.compare(password, room.password);
if (!isValid) {
console.log(`❌ Failed password attempt for room: ${roomId}`);
return res.status(401).json({ error: 'Incorrect password' });
}
console.log(`✅ Password verified for room: ${roomId}`);
res.json({
success: true,
token: room.token // Send auth token
});
} catch (error) {
console.error('Error verifying password:', error);
res.status(500).json({ error: 'Server error' });
}
});
// WebSocket signaling
io.on('connection', (socket) => {
console.log('User connected:', socket.id);
const roomId = socket.roomId; // Set by auth middleware
socket.on('join-room', ({ userId }) => {
const room = rooms.get(roomId);
if (!room || !room.active) {
socket.emit('room-error', { message: 'Room not available' });
return;
}
if (room.participants.size >= room.maxParticipants) {
socket.emit('room-error', { message: 'Room is full (max 10 people)' });
return;
}
socket.join(roomId);
socket.userId = userId;
const existingUsers = Array.from(room.participants);
room.participants.add(socket.id);
console.log(`User ${socket.id} joined room ${roomId} (${room.participants.size} total)`);
socket.emit('existing-users', existingUsers);
socket.to(roomId).emit('user-connected', socket.id);
});
// WebRTC signaling
socket.on('offer', ({ to, offer, from }) => {
io.to(to).emit('offer', { from, offer });
});
socket.on('answer', ({ to, answer, from }) => {
io.to(to).emit('answer', { from, answer });
});
socket.on('ice-candidate', ({ to, candidate, from }) => {
io.to(to).emit('ice-candidate', { from, candidate });
});
socket.on('disconnect', () => {
console.log('User disconnected:', socket.id);
if (roomId) {
const room = rooms.get(roomId);
if (room) {
room.participants.delete(socket.id);
socket.to(roomId).emit('user-disconnected', socket.id);
console.log(`Room ${roomId} now has ${room.participants.size} participants`);
if (room.participants.size === 0) {
room.active = false;
console.log(`Room ${roomId} ended (empty)`);
setTimeout(() => rooms.delete(roomId), 60000);
}
}
}
});
});
const PORT = process.env.PORT || 3001;
server.listen(PORT, () => {
console.log(`
╔════════════════════════════════════════════════════════════════════════════════════════════════════════════════╗
║ ║
║ ██████╗ ██╗██╗ ██╗ ██████╗ ██╗ ██╗████████╗ █████╗ ██╗ ██╗ ██╗ ║
║ ██╔══██╗██║██║ ██║ ██╔═══██╗██║ ██║╚══██╔══╝██╔══██╗██║ ██║ ██╔╝ ║
║ ██████╔╝██║██║ ██║ ██║ ██║██║ █╗ ██║ ██║ ███████║██║ █████╔╝ ║
║ ██╔═══╝ ██║██║ ██║ ██║ ██║██║███╗██║ ██║ ██╔══██║██║ ██╔═██╗ ║
║ ██║ ██║███████╗███████╗╚██████╔╝╚███╔███╔╝ ██║ ██║ ██║███████╗██║ ██╗ ║
║ ╚═╝ ╚═╝╚══════╝╚══════╝ ╚═════╝ ╚══╝╚══╝ ╚═╝ ╚═╝ ╚═╝╚══════╝╚═╝ ╚═╝ ║
║ ║
║ Made with ❤️ by @amiicao & @dannyl1u ║
║ ║
╟────────────────────────────────────────────────────────────────────────────────────────────────────────────────╢
║ ║
║ ☁️ Video Call Server Running ║
║ ║
║ 🌐 Local: http://localhost:${PORT.toString().padEnd(79)} ║
║ ║
╟────────────────────────────────────────────────────────────────────────────────────────────────────────────────╢
║ 🔒 Security Features ║
╟────────────────────────────────────────────────────────────────────────────────────────────────────────────────╢
║ ║
║ ✅ Socket authentication enabled ║
║ ✅ Rate limiting (5 rooms per 15 minutes) ║
║ ✅ Password validation (minimum 6 characters) ║
║ ✅ Automatic room expiry (6 hours) ║
║ ✅ Maximum 10 participants per room ║
║ ║
╚════════════════════════════════════════════════════════════════════════════════════════════════════════════════╝
`);
});