diff --git a/libraries/mobile/mobileShutdown.js b/libraries/mobile/mobileShutdown.js new file mode 100644 index 000000000..0461df8ca --- /dev/null +++ b/libraries/mobile/mobileShutdown.js @@ -0,0 +1,51 @@ +const EXPECTED_SECRET_LENGTH = 44; // string length of base-64 encoding a 32-byte secret +const BASE64_32BYTE_REGEX = /^[A-Za-z0-9+/]{43}=$/; + +/** + * To be able to shut down the server with POST /shutdown/ (e.g. in the mobile app), + * the server can be run with a secret generated by `openssl rand -base64 32`. + * This function validates that the given string is a base64-encoded 32-byte secret. + */ +function isValidShutdownSecret(secret) { + if (typeof secret !== 'string' || secret.length !== EXPECTED_SECRET_LENGTH) { + return false; + } + + if (!BASE64_32BYTE_REGEX.test(secret)) { + return false; + } + + try { + const decoded = Buffer.from(secret, 'base64'); + return decoded.length === 32; + } catch { + return false; + } +} + +/** + * Attempts to load the shutdown secret from process.argv[2]. + * Returns null if invalid or missing. It's ok to omit it, but the /shutdown/ route will be disabled. + */ +function getShutdownSecretFromArgs() { + const rawSecret = process.argv[2]; + + if (!isValidShutdownSecret(rawSecret)) { + return null; + } + + return rawSecret; +} + +/** + * Compares the incoming request token with the expected shutdown secret. + */ +function verifyShutdownRequest(req, expectedSecret) { + const token = req.headers['x-shutdown-token']; + return token === expectedSecret; +} + +module.exports = { + getShutdownSecretFromArgs, + verifyShutdownRequest, +}; diff --git a/server.js b/server.js index a1feaacf9..531c93016 100644 --- a/server.js +++ b/server.js @@ -78,6 +78,10 @@ try { const _logger = require('./logger'); const {objectsPath, beatPort, serverPort, allowSecureMode, persistToCloud} = require('./config'); const {providedServices} = require('./services'); +const { + getShutdownSecretFromArgs, + verifyShutdownRequest, +} = require('./libraries/mobile/mobileShutdown'); const os = require('os'); const {isLightweightMobile, isStandaloneMobile} = require('./isMobile.js'); @@ -2104,6 +2108,24 @@ function objectWebServer() { } }); + // If the server args includes a 32-byte base-64 encoded secret, then enable a special /shutdown/ route + const SHUTDOWN_SECRET = getShutdownSecretFromArgs(); + + if (SHUTDOWN_SECRET) { + console.info('POST /shutdown/ route is enabled, with secret', SHUTDOWN_SECRET); + webServer.post('/shutdown/', function (req, res) { + if (!verifyShutdownRequest(req, SHUTDOWN_SECRET)) { + return res.status(403).send('Forbidden: Invalid shutdown token'); + } + + console.info('Authorized shutdown request received'); + res.send('Shutting down server...'); + exit(); + }); + } else { + console.info('POST /shutdown/ route is disabled'); + } + webServer.get('/server/networkInterface/:activeInterface/', function (req, res) { services.ips.activeInterface = req.params.activeInterface; res.json(services.ips);