diff --git a/app/server/app.js b/app/server/app.js index ab8a7c1..0f86934 100644 --- a/app/server/app.js +++ b/app/server/app.js @@ -17,23 +17,23 @@ const server = require('http').Server(app); const favicon = require('serve-favicon'); const io = require('socket.io')(server, config.socketio); const session = require('express-session')(config.express); + const appSocket = require('./socket'); -const myutil = require('./util'); +const { setDefaultCredentials, basicAuth } = require('./util'); +const { webssh2debug, auditLog, logError } = require('./logging'); const { reauth, connect, notfound, handleErrors } = require('./routes'); -myutil.setDefaultCredentials(config); +setDefaultCredentials(config); // safe shutdown +let remainingSeconds = config.safeShutdownDuration; let shutdownMode = false; -let shutdownInterval = 0; +let shutdownInterval; let connectionCount = 0; // eslint-disable-next-line consistent-return function safeShutdownGuard(req, res, next) { - if (shutdownMode) { - res.status(503).end('Service unavailable: Server shutting down'); - } else { - return next(); - } + if (!shutdownMode) return next(); + res.status(503).end('Service unavailable: Server shutting down'); } // express app.use(safeShutdownGuard); @@ -42,7 +42,7 @@ if (config.accesslog) app.use(logger('common')); app.disable('x-powered-by'); app.use(favicon(path.join(publicPath, 'favicon.ico'))); app.use('/ssh', express.static(publicPath, config.express.ssh)); -app.use(myutil.basicAuth); +app.use(basicAuth); app.get('/ssh/reauth', reauth); app.get('/ssh/host/:host?', connect); app.use(notfound); @@ -52,7 +52,7 @@ app.use(handleErrors); function stopApp(reason) { shutdownMode = false; if (reason) console.info(`Stopping: ${reason}`); - if (shutdownInterval) clearInterval(shutdownInterval); + clearInterval(shutdownInterval); io.close(); server.close(); } @@ -66,39 +66,41 @@ io.use((socket, next) => { socket.request.res ? session(socket.request, socket.request.res, next) : next(next); // eslint disable-line }); -io.on('connection', (socket) => { - connectionCount += 1; +function countdownTimer() { + if (!shutdownMode) clearInterval(shutdownInterval); + remainingSeconds -= 1; + if (remainingSeconds <= 0) { + stopApp('Countdown is over'); + } else io.emit('shutdownCountdownUpdate', remainingSeconds); +} +const signals = ['SIGTERM', 'SIGINT']; +signals.forEach((signal) => + process.on(signal, () => { + if (shutdownMode) stopApp('Safe shutdown aborted, force quitting'); + if (!connectionCount > 0) stopApp('All connections ended'); + shutdownMode = true; + console.error( + `\r\n${connectionCount} client(s) are still connected.\r\nStarting a ${remainingSeconds} seconds countdown.\r\nPress Ctrl+C again to force quit` + ); + if (!shutdownInterval) shutdownInterval = setInterval(countdownTimer, 1000); + }) +); + +module.exports = { server, config }; + +const onConnection = (socket) => { + connectionCount += 1; socket.on('disconnect', () => { connectionCount -= 1; if (connectionCount <= 0 && shutdownMode) { stopApp('All clients disconnected'); } }); -}); + socket.on('geometry', (cols, rows) => { + socket.request.session.ssh.terminfo = { cols, rows }; + webssh2debug(socket, `SOCKET GEOMETRY: termCols = ${cols}, termRows = ${rows}`); + }); +}; -const signals = ['SIGTERM', 'SIGINT']; -signals.forEach((signal) => - process.on(signal, () => { - if (shutdownMode) stopApp('Safe shutdown aborted, force quitting'); - else if (connectionCount > 0) { - let remainingSeconds = config.safeShutdownDuration; - shutdownMode = true; - const message = - connectionCount === 1 ? ' client is still connected' : ' clients are still connected'; - console.error(connectionCount + message); - console.error(`Starting a ${remainingSeconds} seconds countdown`); - console.error('Press Ctrl+C again to force quit'); - - shutdownInterval = setInterval(() => { - remainingSeconds -= 1; - if (remainingSeconds <= 0) { - stopApp('Countdown is over'); - } else { - io.sockets.emit('shutdownCountdownUpdate', remainingSeconds); - } - }, 1000); - } else stopApp(); - }) -); -module.exports = { server, config }; +io.on('connection', onConnection); diff --git a/app/server/socket.js b/app/server/socket.js index ca4fd3a..5ef25b5 100644 --- a/app/server/socket.js +++ b/app/server/socket.js @@ -13,9 +13,6 @@ const dnsPromises = require('dns').promises; const util = require('util'); const { webssh2debug, auditLog, logError } = require('./logging'); -let termCols; -let termRows; - /** * parse conn errors * @param {object} socket Socket object @@ -75,11 +72,6 @@ async function checkSubnet(socket) { // public module.exports = function appSocket(socket) { let login = false; - socket.once('geometry', (cols, rows) => { - termCols = cols; - termRows = rows; - webssh2debug(socket, `SOCKET GEOMETRY: termCols = ${cols}, termRows = ${rows}`); - }); socket.once('disconnecting', (reason) => { webssh2debug(socket, `SOCKET DISCONNECTING: ${reason}`); @@ -138,74 +130,68 @@ module.exports = function appSocket(socket) { socket.emit('status', 'SSH CONNECTION ESTABLISHED'); socket.emit('statusBackground', 'green'); socket.emit('allowreplay', socket.request.session.ssh.allowreplay); - conn.shell( - { - term: socket.request.session.ssh.term, - cols: termCols, - rows: termRows, - }, - (err, stream) => { - if (err) { - logError(socket, `EXEC ERROR`, err); - conn.end(); - socket.disconnect(true); - return; - } - socket.once('disconnect', (reason) => { - webssh2debug(socket, `CLIENT SOCKET DISCONNECT: ${util.inspect(reason)}`); - conn.end(); - socket.request.session.destroy(); - }); - socket.on('error', (errMsg) => { - webssh2debug(socket, `SOCKET ERROR: ${errMsg}`); - logError(socket, 'SOCKET ERROR', errMsg); - conn.end(); - socket.disconnect(true); - }); - socket.on('control', (controlData) => { - if (controlData === 'replayCredentials' && socket.request.session.ssh.allowreplay) { - stream.write(`${socket.request.session.userpassword}\n`); - } - if (controlData === 'reauth' && socket.request.session.username && login === true) { - auditLog( - socket, - `LOGOUT user=${socket.request.session.username} from=${socket.handshake.address} host=${socket.request.session.ssh.host}:${socket.request.session.ssh.port}` - ); - login = false; - conn.end(); - socket.disconnect(true); - } - webssh2debug(socket, `SOCKET CONTROL: ${controlData}`); - }); - socket.on('resize', (data) => { - stream.setWindow(data.rows, data.cols); - webssh2debug(socket, `SOCKET RESIZE: ${JSON.stringify([data.rows, data.cols])}`); - }); - socket.on('data', (data) => { - stream.write(data); - }); - stream.on('data', (data) => { - socket.emit('data', data.toString('utf-8')); - }); - stream.on('close', (code, signal) => { - webssh2debug(socket, `STREAM CLOSE: ${util.inspect([code, signal])}`); - if (socket.request.session?.username && login === true) { - auditLog( - socket, - `LOGOUT user=${socket.request.session.username} from=${socket.handshake.address} host=${socket.request.session.ssh.host}:${socket.request.session.ssh.port}` - ); - login = false; - } - if (code !== 0 && typeof code !== 'undefined') - logError(socket, 'STREAM CLOSE', util.inspect({ message: [code, signal] })); - socket.disconnect(true); - conn.end(); - }); - stream.stderr.on('data', (data) => { - console.error(`STDERR: ${data}`); - }); + const { term, cols, rows } = socket.request.session.ssh; + conn.shell({ term, cols, rows }, (err, stream) => { + if (err) { + logError(socket, `EXEC ERROR`, err); + conn.end(); + socket.disconnect(true); + return; } - ); + socket.once('disconnect', (reason) => { + webssh2debug(socket, `CLIENT SOCKET DISCONNECT: ${util.inspect(reason)}`); + conn.end(); + socket.request.session.destroy(); + }); + socket.on('error', (errMsg) => { + webssh2debug(socket, `SOCKET ERROR: ${errMsg}`); + logError(socket, 'SOCKET ERROR', errMsg); + conn.end(); + socket.disconnect(true); + }); + socket.on('control', (controlData) => { + if (controlData === 'replayCredentials' && socket.request.session.ssh.allowreplay) { + stream.write(`${socket.request.session.userpassword}\n`); + } + if (controlData === 'reauth' && socket.request.session.username && login === true) { + auditLog( + socket, + `LOGOUT user=${socket.request.session.username} from=${socket.handshake.address} host=${socket.request.session.ssh.host}:${socket.request.session.ssh.port}` + ); + login = false; + conn.end(); + socket.disconnect(true); + } + webssh2debug(socket, `SOCKET CONTROL: ${controlData}`); + }); + socket.on('resize', (data) => { + stream.setWindow(data.rows, data.cols); + webssh2debug(socket, `SOCKET RESIZE: ${JSON.stringify([data.rows, data.cols])}`); + }); + socket.on('data', (data) => { + stream.write(data); + }); + stream.on('data', (data) => { + socket.emit('data', data.toString('utf-8')); + }); + stream.on('close', (code, signal) => { + webssh2debug(socket, `STREAM CLOSE: ${util.inspect([code, signal])}`); + if (socket.request.session?.username && login === true) { + auditLog( + socket, + `LOGOUT user=${socket.request.session.username} from=${socket.handshake.address} host=${socket.request.session.ssh.host}:${socket.request.session.ssh.port}` + ); + login = false; + } + if (code !== 0 && typeof code !== 'undefined') + logError(socket, 'STREAM CLOSE', util.inspect({ message: [code, signal] })); + socket.disconnect(true); + conn.end(); + }); + stream.stderr.on('data', (data) => { + console.error(`STDERR: ${data}`); + }); + }); }); conn.on('end', (err) => {