This commit is contained in:
Baobhan Sith
2025-04-28 10:57:38 +08:00
parent 0162aed917
commit 9e47ea3746
2 changed files with 264 additions and 24 deletions
+207
View File
@@ -1341,6 +1341,16 @@
"@types/node": "*" "@types/node": "*"
} }
}, },
"node_modules/@types/cors": {
"version": "2.8.17",
"resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.17.tgz",
"integrity": "sha512-8CGDvrBj1zgo2qE+oS3pOCyYNqCPryMWY2bGfwA0dcfopWGgxs+78df0Rs3rc9THP4JkOhLsAa+15VdpAqkcUA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/node": "*"
}
},
"node_modules/@types/estree": { "node_modules/@types/estree": {
"version": "1.0.7", "version": "1.0.7",
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.7.tgz", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.7.tgz",
@@ -2047,6 +2057,10 @@
"proxy-from-env": "^1.1.0" "proxy-from-env": "^1.1.0"
} }
}, },
"node_modules/backend": {
"resolved": "packages/rdp",
"link": true
},
"node_modules/bagpipe": { "node_modules/bagpipe": {
"version": "0.3.5", "version": "0.3.5",
"resolved": "https://registry.npmjs.org/bagpipe/-/bagpipe-0.3.5.tgz", "resolved": "https://registry.npmjs.org/bagpipe/-/bagpipe-0.3.5.tgz",
@@ -2722,6 +2736,19 @@
"integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/cors": {
"version": "2.8.5",
"resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz",
"integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==",
"license": "MIT",
"dependencies": {
"object-assign": "^4",
"vary": "^1"
},
"engines": {
"node": ">= 0.10"
}
},
"node_modules/cpu-features": { "node_modules/cpu-features": {
"version": "0.0.10", "version": "0.0.10",
"resolved": "https://registry.npmjs.org/cpu-features/-/cpu-features-0.0.10.tgz", "resolved": "https://registry.npmjs.org/cpu-features/-/cpu-features-0.0.10.tgz",
@@ -3648,6 +3675,40 @@
"integrity": "sha512-zxztif3GGhKbg1RgOqwmqot8kXgv2HmHFg1EvWwd4q7UfEKvBcYZ0f+7G8HzvU+FUxF0Psqm9Kl5vCbgfrRgJg==", "integrity": "sha512-zxztif3GGhKbg1RgOqwmqot8kXgv2HmHFg1EvWwd4q7UfEKvBcYZ0f+7G8HzvU+FUxF0Psqm9Kl5vCbgfrRgJg==",
"license": "Apache 2.0" "license": "Apache 2.0"
}, },
"node_modules/guacamole-lite": {
"version": "0.7.3",
"resolved": "https://registry.npmjs.org/guacamole-lite/-/guacamole-lite-0.7.3.tgz",
"integrity": "sha512-BXq7BpleikHe+B/Z1NSJIlTzzAnEX87eFW9WCl3aZ5DoEaGcNbVD9Tzj8CUQI3FFYk1LqtZ31MxlRdVojDLVLw==",
"license": "Apache-2.0",
"dependencies": {
"deep-extend": "^0.6.0",
"moment": "^2.17.1",
"ws": "^1.1.2"
},
"engines": {
"node": ">=6"
}
},
"node_modules/guacamole-lite/node_modules/ws": {
"version": "1.1.5",
"resolved": "https://registry.npmjs.org/ws/-/ws-1.1.5.tgz",
"integrity": "sha512-o3KqipXNUdS7wpQzBHSe180lBGO60SoK0yVo3CYJgb2MkobuWuBX6dhkYP5ORCLd55y+SaflMOV5fqAB53ux4w==",
"license": "MIT",
"dependencies": {
"options": ">=0.0.5",
"ultron": "1.0.x"
}
},
"node_modules/has-flag": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz",
"integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=4"
}
},
"node_modules/has-symbols": { "node_modules/has-symbols": {
"version": "1.1.0", "version": "1.1.0",
"resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
@@ -3848,6 +3909,13 @@
"node": ">= 4" "node": ">= 4"
} }
}, },
"node_modules/ignore-by-default": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/ignore-by-default/-/ignore-by-default-1.0.1.tgz",
"integrity": "sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA==",
"dev": true,
"license": "ISC"
},
"node_modules/imurmurhash": { "node_modules/imurmurhash": {
"version": "0.1.4", "version": "0.1.4",
"resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz",
@@ -4699,6 +4767,15 @@
"pathe": "^2.0.1" "pathe": "^2.0.1"
} }
}, },
"node_modules/moment": {
"version": "2.30.1",
"resolved": "https://registry.npmjs.org/moment/-/moment-2.30.1.tgz",
"integrity": "sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how==",
"license": "MIT",
"engines": {
"node": "*"
}
},
"node_modules/monaco-editor": { "node_modules/monaco-editor": {
"version": "0.52.2", "version": "0.52.2",
"resolved": "https://registry.npmjs.org/monaco-editor/-/monaco-editor-0.52.2.tgz", "resolved": "https://registry.npmjs.org/monaco-editor/-/monaco-editor-0.52.2.tgz",
@@ -4979,6 +5056,35 @@
"node": ">=6.0.0" "node": ">=6.0.0"
} }
}, },
"node_modules/nodemon": {
"version": "3.1.10",
"resolved": "https://registry.npmjs.org/nodemon/-/nodemon-3.1.10.tgz",
"integrity": "sha512-WDjw3pJ0/0jMFmyNDp3gvY2YizjLmmOUQo6DEBY+JgdvW/yQ9mEeSw6H5ythl5Ny2ytb7f9C2nIbjSxMNzbJXw==",
"dev": true,
"license": "MIT",
"dependencies": {
"chokidar": "^3.5.2",
"debug": "^4",
"ignore-by-default": "^1.0.1",
"minimatch": "^3.1.2",
"pstree.remy": "^1.1.8",
"semver": "^7.5.3",
"simple-update-notifier": "^2.0.0",
"supports-color": "^5.5.0",
"touch": "^3.1.0",
"undefsafe": "^2.0.5"
},
"bin": {
"nodemon": "bin/nodemon.js"
},
"engines": {
"node": ">=10"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/nodemon"
}
},
"node_modules/noms": { "node_modules/noms": {
"version": "0.0.0", "version": "0.0.0",
"resolved": "https://registry.npmjs.org/noms/-/noms-0.0.0.tgz", "resolved": "https://registry.npmjs.org/noms/-/noms-0.0.0.tgz",
@@ -5131,6 +5237,14 @@
"wrappy": "1" "wrappy": "1"
} }
}, },
"node_modules/options": {
"version": "0.0.6",
"resolved": "https://registry.npmjs.org/options/-/options-0.0.6.tgz",
"integrity": "sha512-bOj3L1ypm++N+n7CEbbe473A414AB7z+amKYshRb//iuL3MpdDCLhPnw6aVTdKB9g5ZRVHIEp8eUln6L2NUStg==",
"engines": {
"node": ">=0.4.0"
}
},
"node_modules/p-limit": { "node_modules/p-limit": {
"version": "2.3.0", "version": "2.3.0",
"resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz",
@@ -5481,6 +5595,13 @@
"integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/pstree.remy": {
"version": "1.1.8",
"resolved": "https://registry.npmjs.org/pstree.remy/-/pstree.remy-1.1.8.tgz",
"integrity": "sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w==",
"dev": true,
"license": "MIT"
},
"node_modules/pump": { "node_modules/pump": {
"version": "3.0.2", "version": "3.0.2",
"resolved": "https://registry.npmjs.org/pump/-/pump-3.0.2.tgz", "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.2.tgz",
@@ -6097,6 +6218,19 @@
"simple-concat": "^1.0.0" "simple-concat": "^1.0.0"
} }
}, },
"node_modules/simple-update-notifier": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-2.0.0.tgz",
"integrity": "sha512-a2B9Y0KlNXl9u/vsW6sTIu9vGEpfKu2wRV6l1H3XEas/0gUIzGzBoP/IouTcUQbm9JWZLH3COxyn03TYlFax6w==",
"dev": true,
"license": "MIT",
"dependencies": {
"semver": "^7.5.3"
},
"engines": {
"node": ">=10"
}
},
"node_modules/slash": { "node_modules/slash": {
"version": "5.1.0", "version": "5.1.0",
"resolved": "https://registry.npmjs.org/slash/-/slash-5.1.0.tgz", "resolved": "https://registry.npmjs.org/slash/-/slash-5.1.0.tgz",
@@ -6384,6 +6518,19 @@
"node": ">=16" "node": ">=16"
} }
}, },
"node_modules/supports-color": {
"version": "5.5.0",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz",
"integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==",
"dev": true,
"license": "MIT",
"dependencies": {
"has-flag": "^3.0.0"
},
"engines": {
"node": ">=4"
}
},
"node_modules/supports-preserve-symlinks-flag": { "node_modules/supports-preserve-symlinks-flag": {
"version": "1.0.0", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz",
@@ -6585,6 +6732,16 @@
"node": ">=0.6" "node": ">=0.6"
} }
}, },
"node_modules/touch": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/touch/-/touch-3.1.1.tgz",
"integrity": "sha512-r0eojU4bI8MnHr8c5bNo7lJDdI2qXlWWJk6a9EAFG7vbhTjElYhBVS3/miuE0uOuoLdb8Mc/rVfsmm6eo5o9GA==",
"dev": true,
"license": "ISC",
"bin": {
"nodetouch": "bin/nodetouch.js"
}
},
"node_modules/tr46": { "node_modules/tr46": {
"version": "0.0.3", "version": "0.0.3",
"resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz",
@@ -6779,6 +6936,12 @@
"node": ">= 0.8" "node": ">= 0.8"
} }
}, },
"node_modules/ultron": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/ultron/-/ultron-1.0.2.tgz",
"integrity": "sha512-QMpnpVtYaWEeY+MwKDN/UdKlE/LsFZXM5lO1u7GaZzNgmIbGixHEmVMIKT+vqYOALu3m5GYQy9kz4Xu4IVn7Ow==",
"license": "MIT"
},
"node_modules/unctx": { "node_modules/unctx": {
"version": "2.4.1", "version": "2.4.1",
"resolved": "https://registry.npmjs.org/unctx/-/unctx-2.4.1.tgz", "resolved": "https://registry.npmjs.org/unctx/-/unctx-2.4.1.tgz",
@@ -6800,6 +6963,13 @@
"@types/estree": "^1.0.0" "@types/estree": "^1.0.0"
} }
}, },
"node_modules/undefsafe": {
"version": "2.0.5",
"resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.5.tgz",
"integrity": "sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA==",
"dev": true,
"license": "MIT"
},
"node_modules/undici-types": { "node_modules/undici-types": {
"version": "6.19.8", "version": "6.19.8",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz",
@@ -7494,6 +7664,43 @@
"vite": "^5.2.0", "vite": "^5.2.0",
"vue-tsc": "^2.2.8" "vue-tsc": "^2.2.8"
} }
},
"packages/rdp": {
"name": "backend",
"version": "1.0.0",
"license": "ISC",
"dependencies": {
"cors": "^2.8.5",
"express": "^5.1.0",
"guacamole-lite": "^0.7.3",
"ws": "^8.18.1"
},
"devDependencies": {
"@types/cors": "^2.8.17",
"@types/express": "^5.0.1",
"@types/node": "^22.15.2",
"@types/ws": "^8.18.1",
"nodemon": "^3.1.10",
"ts-node": "^10.9.2",
"typescript": "^5.8.3"
}
},
"packages/rdp/node_modules/@types/node": {
"version": "22.15.2",
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.15.2.tgz",
"integrity": "sha512-uKXqKN9beGoMdBfcaTY1ecwz6ctxuJAcUlwE55938g0ZJ8lRxwAZqRz2AJ4pzpt5dHdTPMB863UZ0ESiFUcP7A==",
"dev": true,
"license": "MIT",
"dependencies": {
"undici-types": "~6.21.0"
}
},
"packages/rdp/node_modules/undici-types": {
"version": "6.21.0",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
"integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
"dev": true,
"license": "MIT"
} }
} }
} }
+57 -24
View File
@@ -4,16 +4,21 @@ import express, { Request, Response } from 'express';
import http from 'http'; import http from 'http';
import crypto from 'crypto'; import crypto from 'crypto';
import cors from 'cors'; import cors from 'cors';
// --- Removed fs, path, dotenv imports ---
// --- Configuration ---
const GUAC_WS_PORT = process.env.GUAC_WS_PORT || 8081; const GUAC_WS_PORT = process.env.GUAC_WS_PORT || 8081;
const API_PORT = process.env.API_PORT || 9090; const API_PORT = process.env.API_PORT || 9090;
const GUACD_HOST = process.env.GUACD_HOST || 'localhost'; const GUACD_HOST = process.env.GUACD_HOST || 'localhost';
const GUACD_PORT = parseInt(process.env.GUACD_PORT || '4822', 10); const GUACD_PORT = parseInt(process.env.GUACD_PORT || '4822', 10);
const ENCRYPTION_KEY_STRING = process.env.ENCRYPTION_KEY || 'ThisIsASecretKeyForGuacLite123!!';
if (Buffer.byteLength(ENCRYPTION_KEY_STRING, 'utf8') !== 32) {
process.exit(1);
}
// --- Generate In-Memory Encryption Key on Startup ---
console.log("Generating new in-memory encryption key for this session...");
const ENCRYPTION_KEY_STRING = crypto.randomBytes(32).toString('hex');
const ENCRYPTION_KEY_BUFFER = Buffer.from(ENCRYPTION_KEY_STRING, 'hex');
console.log("In-memory encryption key generated.");
// --- Express App Setup ---
const app = express(); const app = express();
const apiServer = http.createServer(app); const apiServer = http.createServer(app);
@@ -36,7 +41,7 @@ const websocketOptions = {
const clientOptions = { const clientOptions = {
crypt: { crypt: {
key: ENCRYPTION_KEY_STRING key: ENCRYPTION_KEY_STRING // GuacamoleLite expects the string key
}, },
}; };
@@ -73,31 +78,47 @@ try {
process.exit(1); process.exit(1);
} }
const encryptToken = (data: string, key: string): string => { // Updated encryptToken to use the validated Buffer key and AES-GCM
const encryptToken = (data: string, keyBuffer: Buffer): string => {
try { try {
const iv = crypto.randomBytes(16); const iv = crypto.randomBytes(12); // GCM recommended IV size is 12 bytes
const cipher = crypto.createCipheriv('aes-256-cbc', key, iv); const cipher = crypto.createCipheriv('aes-256-gcm', keyBuffer, iv);
const encryptedBuffer = Buffer.concat([cipher.update(data, 'utf8'), cipher.final()]); const encryptedBuffer = Buffer.concat([cipher.update(data, 'utf8'), cipher.final()]);
const tag = cipher.getAuthTag(); // Get the authentication tag
const ivBase64 = iv.toString('base64'); // Combine IV, encrypted data, and tag for storage/transmission
const encryptedBase64 = encryptedBuffer.toString('base64'); // Using a format like IV:TAG:ENCRYPTED_DATA (Base64 encoded) is common
const combined = Buffer.concat([iv, tag, encryptedBuffer]);
const encodedObject = { return combined.toString('base64');
iv: ivBase64,
value: encryptedBase64
};
const jsonString = JSON.stringify(encodedObject);
const finalToken = Buffer.from(jsonString, 'utf8').toString('base64');
return finalToken;
} catch (e) { } catch (e) {
console.error("Token encryption failed:", e); // Log the actual error
throw new Error("Token encryption failed."); throw new Error("Token encryption failed.");
} }
}; };
// Example Decryption (if needed elsewhere, ensure consistency)
// const decryptToken = (token: string, keyBuffer: Buffer): string => {
// try {
// const combined = Buffer.from(token, 'base64');
// const iv = combined.slice(0, 12);
// const tag = combined.slice(12, 12 + 16);
// const encrypted = combined.slice(12 + 16);
//
// const decipher = crypto.createDecipheriv('aes-256-gcm', keyBuffer, iv);
// decipher.setAuthTag(tag);
//
// const decrypted = Buffer.concat([decipher.update(encrypted), decipher.final()]);
// return decrypted.toString('utf8');
// } catch (e) {
// console.error("Token decryption failed:", e);
// throw new Error("Token decryption failed.");
// }
// };
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
app.get('/api/get-token', (req: any, res: any) => { app.get('/api/get-token', (req: any, res: any) => {
const { hostname, port, username, password, security = 'any', ignoreCert = 'true' } = req.query; const { hostname, port, username, password, security = 'any', ignoreCert = 'true' } = req.query;
@@ -126,33 +147,39 @@ app.get('/api/get-token', (req: any, res: any) => {
try { try {
const tokenData = JSON.stringify(connectionParams); const tokenData = JSON.stringify(connectionParams);
const encryptedToken = encryptToken(tokenData, ENCRYPTION_KEY_STRING); // Use the validated key buffer for encryption
// Use the validated key buffer for encryption
const encryptedToken = encryptToken(tokenData, ENCRYPTION_KEY_BUFFER);
res.json({ token: encryptedToken }); res.json({ token: encryptedToken });
} catch (error) { } catch (error) {
console.error("Error in /api/get-token:", error); // Log specific error
res.status(500).json({ error: 'Failed to generate token' }); res.status(500).json({ error: 'Failed to generate token' });
} }
}); });
apiServer.listen(API_PORT, () => { apiServer.listen(API_PORT, () => {
// Startup log removed console.log(`RDP API server listening on port ${API_PORT}`); // Added startup log
console.log(`Guacamole WebSocket server expected to be running on port ${GUAC_WS_PORT}`);
}); });
const gracefulShutdown = (signal: string) => { const gracefulShutdown = (signal: string) => {
console.log(`Received ${signal}. Shutting down gracefully...`); // Added shutdown log
let guacClosed = false; let guacClosed = false;
let apiClosed = false; let apiClosed = false;
const tryExit = () => { const tryExit = () => {
if (guacClosed && apiClosed) { if (guacClosed && apiClosed) {
console.log("All servers closed. Exiting."); // Added exit log
process.exit(0); process.exit(0);
} }
}; };
apiServer.close((err) => { apiServer.close((err) => {
if (err) { if (err) {
// Minimal error handling console.error("Error closing API server:", err); // Added error log
} else { } else {
// Minimal close handling console.log("API server closed."); // Added close log
} }
apiClosed = true; apiClosed = true;
tryExit(); tryExit();
@@ -160,24 +187,30 @@ const gracefulShutdown = (signal: string) => {
// @ts-ignore - Assuming close method exists based on common patterns // @ts-ignore - Assuming close method exists based on common patterns
if (typeof guacServer !== 'undefined' && guacServer && typeof guacServer.close === 'function') { if (typeof guacServer !== 'undefined' && guacServer && typeof guacServer.close === 'function') {
console.log("Closing Guacamole server..."); // Added close log
// @ts-ignore // @ts-ignore
guacServer.close(() => { guacServer.close(() => {
console.log("Guacamole server closed."); // Added close log
guacClosed = true; guacClosed = true;
tryExit(); tryExit();
}); });
} else { } else {
console.log("Guacamole server not running or doesn't support close()."); // Added info log
guacClosed = true; guacClosed = true;
tryExit(); tryExit();
} }
// Force exit after timeout
setTimeout(() => { setTimeout(() => {
console.error("Graceful shutdown timed out. Forcing exit."); // Added timeout log
process.exit(1); process.exit(1);
}, 10000); }, 10000); // 10 seconds timeout
}; };
process.on('SIGINT', () => gracefulShutdown('SIGINT')); process.on('SIGINT', () => gracefulShutdown('SIGINT'));
process.on('SIGTERM', () => gracefulShutdown('SIGTERM')); process.on('SIGTERM', () => gracefulShutdown('SIGTERM'));
process.on('SIGUSR2', () => { process.on('SIGUSR2', () => {
// Handle nodemon restarts gracefully
gracefulShutdown('SIGUSR2 (nodemon restart)'); gracefulShutdown('SIGUSR2 (nodemon restart)');
}); });