Skip to content

Commit d3565e9

Browse files
lakshmisravya123lakshmisravya123
authored andcommitted
fix: validate roomId in WebRTC signaling handlers (#106)
Add roomId validation to offer, answer, and ice-candidate socket handlers. Before forwarding signaling messages, the server now verifies that the roomId exists in activeStreams and that the socket belongs to the room, emitting an error event back to the client on failure.
1 parent 3ced13a commit d3565e9

2 files changed

Lines changed: 154 additions & 2 deletions

File tree

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,8 @@
3737
"express": "^5.2.1",
3838
"jsonwebtoken": "^9.0.3",
3939
"mongoose": "^9.1.5",
40-
"morgan": "^1.10.1"
40+
"morgan": "^1.10.1",
41+
"socket.io": "^4.8.3"
4142
},
4243
"devDependencies": {
4344
"@babel/eslint-parser": "^7.28.6",

server.js

Lines changed: 152 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,26 @@ import { fileURLToPath } from 'url';
88
import { dirname, join } from 'path';
99
import * as AuthControllers from './src/controllers/AuthControllers.js';
1010
import { auth, authorizeRole } from './src/middleware/auth.js';
11+
import { createServer } from 'http';
12+
import { Server } from 'socket.io';
1113

1214
const app = express();
1315
app.use(morgan('dev'));
1416
const PORT = process.env.PORT || 3000;
1517
const MONGODB_URI = process.env.MONGODB_URI;
1618

19+
// Create HTTP server and attach Socket.IO
20+
const httpServer = createServer(app);
21+
const io = new Server(httpServer, {
22+
cors: {
23+
origin: process.env.CLIENT_URL
24+
? [process.env.CLIENT_URL]
25+
: ['http://localhost:5173', 'http://localhost:3000'],
26+
methods: ['GET', 'POST'],
27+
credentials: true,
28+
},
29+
});
30+
1731
// Recreate __dirname since it is not available in ES Modules by default
1832
const __filename = fileURLToPath(import.meta.url);
1933
const __dirname = dirname(__filename);
@@ -132,16 +146,153 @@ app.get('/health', (req,res)=>{
132146
});
133147
});
134148

149+
// ============== WEBRTC SIGNALING SERVER ==============
150+
// Track active streams: { roomId: { broadcaster: socketId, viewers: [socketId] } }
151+
const activeStreams = new Map();
152+
153+
// API: Get list of active live streams
154+
app.get('/api/live-streams', (req, res) => {
155+
const streams = [];
156+
activeStreams.forEach((value, key) => {
157+
streams.push({ roomId: key, viewerCount: value.viewers.length });
158+
});
159+
res.json(streams);
160+
});
161+
162+
/**
163+
* Validate that a roomId exists in activeStreams and that the socket
164+
* has joined the corresponding Socket.IO room.
165+
* Returns the stream object on success, or null after emitting an error.
166+
*/
167+
function validateRoom(socket, roomId, eventName) {
168+
if (!roomId || !activeStreams.has(roomId)) {
169+
socket.emit('error', {
170+
event: eventName,
171+
message: `Room "${roomId}" does not exist in active streams`,
172+
});
173+
return null;
174+
}
175+
176+
if (!socket.rooms.has(roomId)) {
177+
socket.emit('error', {
178+
event: eventName,
179+
message: `Socket is not a member of room "${roomId}"`,
180+
});
181+
return null;
182+
}
183+
184+
return activeStreams.get(roomId);
185+
}
186+
187+
io.on('connection', (socket) => {
188+
console.log(`[SOCKET] User connected: ${socket.id}`);
189+
190+
// Broadcaster starts a stream
191+
socket.on('start-stream', (roomId) => {
192+
if (activeStreams.has(roomId)) {
193+
socket.emit('error', { message: 'Room already exists' });
194+
return;
195+
}
196+
socket.join(roomId);
197+
activeStreams.set(roomId, { broadcaster: socket.id, viewers: [] });
198+
console.log(`[LIVE] Stream started: ${roomId} by ${socket.id}`);
199+
socket.emit('stream-started', { roomId });
200+
});
201+
202+
// Viewer joins a stream
203+
socket.on('join-stream', (roomId) => {
204+
const stream = activeStreams.get(roomId);
205+
if (!stream) {
206+
socket.emit('error', { message: 'Stream not found' });
207+
return;
208+
}
209+
socket.join(roomId);
210+
stream.viewers.push(socket.id);
211+
console.log(`[VIEWER] ${socket.id} joined ${roomId}`);
212+
213+
// Notify broadcaster that a new viewer joined
214+
io.to(stream.broadcaster).emit('viewer-joined', {
215+
viewerId: socket.id,
216+
});
217+
});
218+
219+
// WebRTC signaling: Offer (broadcaster -> viewer)
220+
socket.on('offer', ({ roomId, offer, viewerId }) => {
221+
const stream = validateRoom(socket, roomId, 'offer');
222+
if (!stream) return;
223+
224+
io.to(viewerId).emit('offer', { offer, broadcasterId: socket.id });
225+
});
226+
227+
// WebRTC signaling: Answer (viewer -> broadcaster)
228+
socket.on('answer', ({ roomId, answer, broadcasterId }) => {
229+
const stream = validateRoom(socket, roomId, 'answer');
230+
if (!stream) return;
231+
232+
io.to(broadcasterId).emit('answer', { answer, viewerId: socket.id });
233+
});
234+
235+
// WebRTC signaling: ICE Candidate
236+
socket.on('ice-candidate', ({ roomId, candidate, targetId }) => {
237+
const stream = validateRoom(socket, roomId, 'ice-candidate');
238+
if (!stream) return;
239+
240+
io.to(targetId).emit('ice-candidate', {
241+
candidate,
242+
senderId: socket.id,
243+
});
244+
});
245+
246+
// Stop stream
247+
socket.on('stop-stream', (roomId) => {
248+
if (activeStreams.has(roomId)) {
249+
io.to(roomId).emit('stream-ended');
250+
activeStreams.delete(roomId);
251+
console.log(`[ENDED] Stream ended: ${roomId}`);
252+
}
253+
});
254+
255+
// Disconnect handling
256+
socket.on('disconnect', () => {
257+
console.log(`[SOCKET] User disconnected: ${socket.id}`);
258+
259+
// Check all streams for this socket
260+
activeStreams.forEach((stream, roomId) => {
261+
if (stream.broadcaster === socket.id) {
262+
// Broadcaster disconnected - end the stream for all viewers
263+
io.to(roomId).emit('stream-ended', {
264+
reason: 'Broadcaster disconnected',
265+
});
266+
activeStreams.delete(roomId);
267+
console.log(
268+
`[ENDED] Stream ${roomId} ended (broadcaster disconnected)`
269+
);
270+
} else if (stream.viewers.includes(socket.id)) {
271+
// Viewer disconnected - remove from list and notify broadcaster
272+
stream.viewers = stream.viewers.filter((id) => id !== socket.id);
273+
io.to(stream.broadcaster).emit('viewer-left', {
274+
viewerId: socket.id,
275+
viewerCount: stream.viewers.length,
276+
});
277+
console.log(
278+
`[VIEWER] ${socket.id} left ${roomId}. Viewers remaining: ${stream.viewers.length}`
279+
);
280+
}
281+
});
282+
});
283+
});
284+
135285
// 404 Not Found handler (must be after all routes)
136286
app.use((req, res) => {
137287
res.status(404).json({
138288
error: 'Route not found',
139289
});
140290
});
141291

142-
app.listen(PORT, () => {
292+
httpServer.listen(PORT, () => {
143293
console.log(`\nServer successfully started!`);
144294
console.log(`Home: http://localhost:${PORT}`);
145295
console.log('\n✅ Server successfully started!');
146296
console.log(`🏠 Home: http://localhost:${PORT}`);
297+
console.log(`WebRTC Signaling: ws://localhost:${PORT}`);
147298
});

0 commit comments

Comments
 (0)