-
Notifications
You must be signed in to change notification settings - Fork 14
Expand file tree
/
Copy pathserver.js
More file actions
727 lines (595 loc) · 24.3 KB
/
Copy pathserver.js
File metadata and controls
727 lines (595 loc) · 24.3 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
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
//VideoWhisper WebRTC Signaling Server
const SERVER_VERSION = "2025.04.26";
const SERVER_FEATURES = "WebRTC Signaling, SSL, TURN/STUN configuration for VideoWhisper HTML5 Videochat, MySQL accounts, MySQL plans, plan limitations for connections/bitrate/resolution/framerate, Account registration integration, NGINX server integration for RTMP/HLS stream management with stream pin validation and web server notification, STUN/TURN check, user pin authentication support, rate limiting, rooms, chat.";
//Configuration
require('dotenv').config();
const DEVMODE = ( process.env.NODE_ENV || "development" ) === "development"; //development mode
console.log("VideoWhisper Server", SERVER_VERSION, "\r", SERVER_FEATURES );
//Authentication
if (process.env.STATIC_TOKEN) console.log("Static token configured", DEVMODE ? process.env.STATIC_TOKEN : ''); else console.log("Static token disabled");
//Account database
var accounts = {};
let accountsLoaded = false;
//for easier access
var accountsByName = {};
//Modules
let nginxModuleInstance;
//create function to update accounts, as used in multiple places
const updateAccounts = () => {
if (process.env.DB_HOST) {
const Database = require('./modules/database.js');
const db = new Database();
db.getAccounts()
.then(accts => {
accounts = accts;
accountsLoaded = true;
// Create a reverse lookup object keyed by account name for quick access
accountsByName = {};
for (const token in accounts) {
const account = accounts[token];
if (account.name) {
accountsByName[account.name] = account;
}
}
if (DEVMODE) console.log("Loaded accounts", Object.keys(accountsByName));
if (nginxModuleInstance) nginxModuleInstance.updateAccounts(accounts, accountsByName);
})
.catch(err => {
console.error('Error loading accounts:', err);
});
} else {
console.warn("No DB_HOST configured, accounts not loaded from database");
}
// Add static account to accounts list if configured in .env
// This happens after DB load to ensure it takes precedence
if (process.env.STATIC_ACCOUNT && process.env.STATIC_TOKEN) {
const staticAccount = process.env.STATIC_ACCOUNT;
const staticToken = process.env.STATIC_TOKEN;
accounts[staticToken] = {
name: staticAccount,
token: staticToken,
properties: {
loginURL: process.env.STATIC_LOGIN || null // Store the STATIC_LOGIN URL if provided
},
plan: {
// STATIC_ACCOUNT plan with generous development limits
connections: 100, // connections at same time
totalBitrate: 100000, // Mbps total account bitrate (all streams)
bitrate: 5000, // kbps video bitrate
audioBitrate: 256, // kbps audio bitrate
width: 1920, // resolution width
height: 1080, // automatically switched limits for portrait/landscape
frameRate: 30, // frames per second
streamPlayers: 100, // NGINX HLS players (per account)
}
};
// Add to accountsByName for quick lookups
accountsByName[staticAccount] = accounts[staticToken];
accountsLoaded = true; // Mark as loaded if using only static account
if (DEVMODE) console.log("Added static account:", staticAccount);
}
if (accountsLoaded)
{
if (nginxModuleInstance) nginxModuleInstance.updateAccounts(accounts, accountsByName);
if (DEVMODE) console.log("updateAccounts", accountsLoaded);
}
};
updateAccounts();
// SERVER
const express = require('express');
const app = express();
const cors = require("cors");
// Simple rate limiting implementation that works with older Node versions
const createRateLimiter = (windowMs, maxRequests) => {
const requests = {};
return (req, res, next) => {
const ip = req.ip || req.connection.remoteAddress;
const now = Date.now();
// Clean up old requests
for (const storedIp in requests) {
if (requests[storedIp].timestamp < now - windowMs) {
delete requests[storedIp];
}
}
// Initialize or update request tracking for this IP
if (!requests[ip]) {
requests[ip] = {
count: 1,
timestamp: now
};
} else {
requests[ip].count++;
if (requests[ip].count > maxRequests) {
return res.status(429).send('Too many requests: ' + requests[ip].count + '/' + (windowMs/1000) +'s. Try again in ' + Math.ceil((windowMs - (now - requests[ip].timestamp)) / 1000) + ' seconds.');
}
}
next();
};
};
// Simple security headers middleware compatible with older Node versions
const addSecurityHeaders = (req, res, next) => {
// Basic security headers
res.setHeader('X-Content-Type-Options', 'nosniff');
res.setHeader('X-Frame-Options', 'DENY');
res.setHeader('X-XSS-Protection', '1; mode=block');
// Only add strict transport security in production
if (!DEVMODE) {
res.setHeader('Strict-Transport-Security', 'max-age=31536000; includeSubDomains; preload');
}
next();
};
// Apply the middlewares
app.use(express.json());
app.use(cors({
origin: true, // Allow all origins
methods: ['GET', 'POST'],
allowedHeaders: ['Content-Type', 'Authorization'],
credentials: true
}));
app.use(addSecurityHeaders);
const fs = require('fs');
const https = require('https');
//i.e. cPanel > SSL/TLS > Certificates > Install : get certificate and key
const options = {
key: fs.readFileSync(process.env.CERTIFICATE + '.key'),
cert: fs.readFileSync(process.env.CERTIFICATE + '.crt'),
ca: fs.readFileSync(process.env.CERTIFICATE + '.pem') // crt + intermediate if necessary
};
const server = https.createServer(options, app);
// Suppress deprecation warnings in production
if (!DEVMODE) {
process.noDeprecation = true;
}
// Configure Socket.IO with modern options
const io = require('socket.io')(server, {
cors: {
origin: '*', // Match your CORS settings from Express
methods: ['GET', 'POST'],
credentials: true
},
maxHttpBufferSize: 1e8, // 100MB for large data transmission if needed
pingTimeout: 60000, // 1 minute
pingInterval: 5000, // 5 seconds
transports: ['websocket', 'polling'] // Prefer WebSocket, fallback to polling
});
// Avoid using deprecated properties
// io.eio.pingTimeout = 60000; // This was using deprecated properties
// io.eio.pingInterval = 5000; // This was using deprecated properties
// API ENDPOINT TO DISPLAY THE CONNECTION TO THE SIGNALING SERVER
let connections = {};
let channels = {}; //webrtc channels are for streaming
let stats = {};
let accountIssues = {};
// Import modules
const webrtcModule = require('./modules/webrtc.js')(io, connections, channels, stats, DEVMODE, accounts, accountIssues);
// Pass the required dependencies to the room module
const roomModule = require('./modules/room.js')(io, webrtcModule, connections, channels, stats, DEVMODE);
//[GET] https://yourDomain:PORT/
app.get("/", async (req, res) => { // Mark function as async
let result = {
"server": "VideoWhisper WebRTC",
"version": SERVER_VERSION,
"features": SERVER_FEATURES,
"nginx-module": (nginxModuleInstance ? true : false)
};
try {
// Use the testStunTurn function from the webrtc module
const stunTurnStatus = await webrtcModule.testStunTurn();
let testResult = {
"webrtc-test": stunTurnStatus.error ? `Error: ${stunTurnStatus.error}` : "STUN/TURN check passed",
"stun": stunTurnStatus.stun,
"turn": stunTurnStatus.turn
};
// Add the timeSinceTested as webrtcTestAge if available
if (stunTurnStatus.timeSinceTested !== undefined) {
testResult.webrtcTestAge = stunTurnStatus.timeSinceTested;
}
//add these properties to the existing result object
result = { ...result, ...testResult };
if (DEVMODE) console.log("API /", result);
res.json(result);
} catch (error) {
let testResult = {
"webrtc-test": "Error while checking STUN/TURN",
"stun": false,
"turn": false
};
result = { ...result, ...testResult };
console.error("Error checking STUN/TURN:", error, result);
res.json(result);
}
});
const API_KEY = process.env.API_KEY;
if (DEVMODE || API_KEY)
{
//[GET] https://yourDomain:PORT/connections?apikey=YOUR_API_KEY
app.get("/connections", (req, res) => {
const apikey = req.query.apikey;
if (DEVMODE) console.log("API /connections", connections, API_KEY, apikey );
if (apikey != API_KEY && !DEVMODE) return res.status(401).send('Invalid API key');
else
res.json(Object.entries(connections));
});
//[GET] https://yourDomain:PORT/channels?apikey=YOUR_API_KEY
app.get("/channels", (req, res) => {
const apikey = req.query.apikey;
if (DEVMODE) console.log("API /channels", channels, API_KEY, apikey );
if (apikey != API_KEY && !DEVMODE) return res.status(401).send('Invalid API key');
else
res.json(Object.entries(channels));
});
//[GET] https://yourDomain:PORT/status?apikey=API_KEY&token=ACCOUNT_TOKEN
app.get("/status", (req, res) => {
const apikey = req.query.apikey;
const token = req.query.token;
// Validate input parameters
if (token && !/^[a-zA-Z0-9_\-\.]+$/.test(token)) {
return res.status(400).send('Invalid token format');
}
if (!token && apikey != API_KEY && !DEVMODE) return res.status(401).send('Invalid API key and no account token');
if (DEVMODE) console.log("API /status", stats, apikey, token );
let result = {};
if (token)
{
//account status
//check if account with that token exists
if (accounts[token])
{
const account = accounts[token].name;
const accountInfo = accounts[token];
result['account'] = account;
if (stats[account])
{
result['status'] = 'Active';
//add stats[account];
result['stats'] = stats[account];
//add webrtc channels
let webrtc = {};
for (let channel in connections) //for each channel
{
for (let peerID in connections[channel]) //for each connection
{
let peerAccount = connections[channel][peerID].account;
if (peerAccount == account)
{
if (!webrtc[channel]) {
webrtc[channel] = channels[channel];
// webrtc[channel]['connections'] = connections[channel] ? Object.keys(connections[channel]).length : 0;
}
break;
}
}
}
result['webrtc'] = webrtc;
//end active account
}
else result['status'] = 'Inactive';
if (accountInfo && accountInfo.plan) result['plan'] = accountInfo.plan;
res.json(result);
}
else
{
if (DEVMODE) console.warn("API /status Invalid account token", token);
return res.status(401).send('Invalid account token');
}
}
else
{
if (DEVMODE) console.warn("API /status ALL");
result['connections'] = Object.keys(connections).length;
result['stats'] = stats;
result['webrtc'] = channels;
res.json(result);
}
});
//[GET] https://yourDomain:PORT/update-accounts?apikey=YOUR_API_KEY
app.get("/update-accounts", (req, res) => {
const apikey = req.query.apikey;
if (DEVMODE) console.log("API /update-accounts", API_KEY, apikey );
if (apikey != API_KEY && !DEVMODE) return res.status(401).send('Invalid API key');
else
{
updateAccounts();
res.json({ "status": "Updating Accounts" });
}
});
}
// Define serverUpdateStats function before using it in the Nginx module
const serverUpdateStats = () => {
if (DEVMODE) console.log("serverUpdateStats");
// Use webrtc module to update stats
const accountStats = webrtcModule.updateStats();
// Update our global stats object
stats = accountStats;
return stats;
};
//Nginx RTMP/HLS module
if (process.env.NGINX_HOST) {
const NGINX_HOST = process.env.NGINX_HOST;
if (fs.existsSync('./modules/nginx.js')) {
const nginxModule = require('./modules/nginx');
nginxModuleInstance = nginxModule(app, DEVMODE, serverUpdateStats);
} else {
console.warn('Nginx module is missing. Ask https://consult.videowhisper.com for details about the nginx RTMP/HLS integration module.');
}
} else if (DEVMODE) {
console.log('Nginx module disabled');
}
// Add a simple authentication rate limiter
const authRateLimiter = createRateLimiter(
10 * 60 * 1000, // 10 minutes window
DEVMODE ? 120 : 30 // Higher limit in development
);
// Apply the authentication rate limiter to socket.io connections
// by modifying the authenticate middleware to use it
const authenticate = async (socket, next) => {
try {
// Simple rate limiting for socket authentication
const mockReq = { ip: socket.handshake.address };
const mockRes = {
status: (code) => ({
send: (message) => {
next(new Error(message));
return mockRes;
}
})
};
const mockNext = () => {
// Continue with normal authentication flow
authenticateSocket(socket, next);
};
// Apply rate limiting
authRateLimiter(mockReq, mockRes, mockNext);
} catch (error) {
console.error('Rate limiting error:', error);
next(new Error(DEVMODE ? `ERROR: ${error.message}` : "ERROR: Authentication error"));
}
};
// Move the actual socket authentication logic to a separate function
const authenticateSocket = async (socket, next) => {
try {
// Get authentication parameters from handshake
const token = socket.handshake.auth.token;
const account = socket.handshake.auth.account;
const user = socket.handshake.auth.user;
const pin = socket.handshake.auth.pin;
const staticToken = process.env.STATIC_TOKEN || '';
const hideDetailedErrors = process.env.EXTRA_SECURITY === 'true' && !DEVMODE;
// Validate input parameters (while providing detailed errors in DEVMODE)
if (token && typeof token !== 'string') {
const message = "Invalid token parameter";
if (DEVMODE) console.log(`Authentication error: ${message}`);
return next(new Error(DEVMODE ? `ERROR: ${message}` : "ERROR: Authentication error"));
}
if (token && !/^[a-zA-Z0-9_\-\.]+$/.test(token)) {
const message = "Invalid token format";
if (DEVMODE) console.log(`Authentication error: ${message}`);
return next(new Error(DEVMODE ? `ERROR: ${message}` : "ERROR: Authentication error"));
}
if (account && typeof account !== 'string') {
const message = "Invalid account parameter";
if (DEVMODE) console.log(`Authentication error: ${message}`);
return next(new Error(DEVMODE ? `ERROR: ${message}` : "ERROR: Authentication error"));
}
if (account && !/^[a-zA-Z0-9_\-\.]+$/.test(account)) {
const message = "Invalid account format";
if (DEVMODE) console.log(`Authentication error: ${message}`);
return next(new Error(DEVMODE ? `ERROR: ${message}` : "ERROR: Authentication error"));
}
// Static token authentication (highest priority)
if (staticToken && token === staticToken) {
socket.account = '_static';
socket.token = staticToken;
if (DEVMODE) console.log("Authenticated with STATIC_TOKEN #", socket.id);
return next();
}
// Account/user/pin authentication
if (account && user && pin) {
// Check if the account exists
if (!accountsByName[account]) {
const errorMsg = hideDetailedErrors ? "Authentication failed" : "Account not found";
return next(new Error(`ERROR: ${errorMsg}`));
}
// Check if the account supports loginURL authentication
if (!accountsByName[account].properties || !accountsByName[account].properties.loginURL) {
const errorMsg = hideDetailedErrors ? "Authentication failed" : "This account does not support user/pin authentication";
return next(new Error(`ERROR: ${errorMsg}`));
}
try {
// Use the account's login URL and token
const loginURL = accountsByName[account].properties.loginURL;
const tokenToUse = accountsByName[account].token || '';
// Authenticate the user with the pin
const authResult = await authenticateUserPin(account, user, pin, loginURL, tokenToUse);
if (authResult.login === true) {
// Authentication successful
socket.account = account;
socket.token = accountsByName[account].token;
socket.user = user;
if (DEVMODE) console.log(`Authenticated with user/pin for account ${account} user ${user} #`, socket.id);
// Check account limits
const accountInfo = accountsByName[account];
const limitError = checkAccountLimits(account, accountInfo);
if (limitError) {
if (DEVMODE) console.warn(`Limit check failed for ${account}: ${limitError}`);
const errorMsg = hideDetailedErrors ? "Authentication failed" : limitError;
return next(new Error(`ERROR: ${errorMsg}`));
}
return next();
} else {
// Authentication failed
const errorMessage = authResult.message || 'User authentication failed';
if (DEVMODE) console.warn(`Authentication failed for ${account}/${user}: ${errorMessage}`);
const errorMsg = hideDetailedErrors ? "Authentication failed" : errorMessage;
return next(new Error(`ERROR: ${errorMsg}`));
}
} catch (error) {
console.error('Error during user/pin authentication:', error);
return next(new Error('ERROR: Authentication error'));
}
}
// Traditional token-based authentication (fallback for backward compatibility)
if (!accounts) return next(new Error("ERROR: No static token configured or accounts loaded, yet"));
const accountInfo = accounts[token];
if (accountInfo) {
socket.account = accountInfo.name;
socket.token = token;
// Check account limits
const limitError = checkAccountLimits(accountInfo.name, accountInfo);
if (limitError) {
if (DEVMODE) console.warn(`Limit check failed for ${accountInfo.name}: ${limitError}`);
const errorMsg = hideDetailedErrors ? "Authentication failed" : limitError;
return next(new Error(`ERROR: ${errorMsg}`));
}
// Accept connection
if (DEVMODE) console.log(`Authenticated with token from account ${accountInfo.name} #`, socket.id);
return next();
} else {
return next(new Error("ERROR: Authentication error"));
}
} catch (error) {
console.error('Authentication error:', error);
return next(new Error(DEVMODE ? `ERROR: ${error.message}` : "ERROR: Authentication error"));
}
};
// Function to authenticate user with account, user, pin from account.loginURL integration
// loginURL and token parameters are now provided by the authenticateSocket function
const authenticateUserPin = async (account, user, pin, loginURL, token) => {
// No need to determine login URL or token - they are provided as parameters
const isStaticAccount = account === process.env.STATIC_ACCOUNT;
// If no loginURL was provided, authentication can't proceed
if (!loginURL) {
const message = 'Login URL not available';
if (DEVMODE) console.log(`Authentication failed: ${message}`);
return { login: false, message: DEVMODE ? message : 'Authentication failed' };
}
// Make the HTTP request to authenticate
try {
const fetch = require('node-fetch');
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), 5000);
const response = await fetch(loginURL, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
// account: account, //acounts are identified by token
token: token,
user: user,
pin: pin
}),
signal: controller.signal
});
clearTimeout(timeout);
if (!response.ok) {
const message = `Server responded with ${response.status}: ${response.statusText}`;
if (DEVMODE) console.log(`Authentication failed: ${message}`);
return { login: false, message: DEVMODE ? message : 'Authentication failed' };
}
const result = await response.json();
if (DEVMODE && isStaticAccount) {
console.log(`Static account authentication result for ${account}/${user}:`, result);
}
return result;
} catch (error) {
console.error(`Error authenticating with ${isStaticAccount ? 'static account' : 'user/pin'}:`, error);
return {
login: false,
message: DEVMODE ? `Authentication error: ${error.message}` : 'Authentication failed'
};
}
};
// Helper function to check account limits
// Returns null if all checks pass, or an error message if any limits are exceeded
const checkAccountLimits = (accountName, accountInfo) => {
// Check connection limit
if (accountInfo.plan.connections &&
stats[accountName] &&
stats[accountName].connections >= accountInfo.plan.connections) {
return 'Account connection limit exceeded:' + stats[accountName].connections + '/' + accountInfo.plan.connections;
}
// Check bitrate limit
if (accountInfo.plan.totalBitrate &&
stats[accountName] &&
stats[accountName].bitrate + stats[accountName].audioBitrate >= accountInfo.plan.totalBitrate) {
return 'Account bitrate limit exceeded: ' + accountInfo.plan.totalBitrate + ' kbps';
}
// Check if account is suspended
if (accountInfo.properties.suspended) {
return 'Account is suspended: ' + accountName;
}
// All checks passed
return null;
};
// Use the authentication middleware
io.use(authenticate);
// SIGNALING LOGIC
io.on("connection", (socket) => {
if (DEVMODE) console.log("Socket connected #", socket.id, "from account", socket.account);
// Setup WebRTC event handlers from module
webrtcModule.setupSocketHandlers(socket);
// Setup Room event handlers if user is authenticated with user/pin
if (socket.user) {
roomModule.setupRoomHandlers(socket);
if (DEVMODE) console.log(`Room handlers set up for user ${socket.user}`);
}
socket.on("disconnecting", () => {
//using disconnecting because socket.rooms not available on disconnect event
const channel = Array.from(socket.rooms)[1];
if (!channel || !connections[channel]){
console.log(socket.id, "has disconnected (no channel) socket.rooms:", socket.rooms);
return;
}
const disconnectingPeer = Object.values(connections[channel]).find((peer) => peer.socketId === socket.id);
if (disconnectingPeer) {
if (DEVMODE) console.log("Disconnected", socket.id, ":" , disconnectingPeer.peerID, "@", channel);
// remove disconnecting peer from connections
delete connections[channel][disconnectingPeer.peerID];
}
else {
console.log(socket.id, " disconnected (unregistered peer from", channel);
}
//update live stats after removing connection
serverUpdateStats();
});
});
//handle exceptions and exit gracefully
process.on('unhandledRejection', (reason, promise) => {
console.log('Unhandled Rejection at:', promise, 'reason:', reason);
process.exit(1);
});
// Add graceful shutdown
// Flag to track shutdown in progress to prevent double shutdown
let isShuttingDown = false;
process.on('SIGTERM', gracefulShutdown);
process.on('SIGINT', gracefulShutdown);
function gracefulShutdown() {
// Prevent multiple shutdown attempts
if (isShuttingDown) return;
isShuttingDown = true;
console.log('Received shutdown signal. Allow 10s for graceful shutdown. Closing connections...');
// Close all socket.io connections first
if (io) {
const sockets = io.sockets.sockets;
if (sockets) {
// Disconnect all Socket.IO clients
sockets.forEach(socket => {
if (socket.connected) {
socket.disconnect(true);
}
});
}
}
// Then close the server
server.close(() => {
console.log('Server closed successfully');
process.exit(0);
});
// Force close after 10 seconds if not closed gracefully
setTimeout(() => {
console.log('Forcing server shutdown after timeout');
process.exit(1);
}, 10000);
}
// START SERVER
const PORT = process.env.PORT || 3000; //port to listen on
server.listen(PORT, () => console.log(`Server listening on PORT ${PORT}`));