Allow websocket server to take ssh host as query parameter. This allows sockets to be opened without loading the client page first to set up the session. This simplifies use when using WebSSH2 with an alternative client.
Cleaned up the derivation of configuration from defaults, static config file, request params & session
This commit is contained in:
parent
2289036605
commit
f1e5d4a13c
3 changed files with 248 additions and 246 deletions
|
|
@ -3,105 +3,18 @@
|
|||
// app.js
|
||||
|
||||
var path = require('path')
|
||||
var fs = require('fs')
|
||||
var nodeRoot = path.dirname(require.main.filename)
|
||||
var configPath = path.join(nodeRoot, 'config.json')
|
||||
var publicPath = path.join(nodeRoot, 'client', 'public')
|
||||
console.log('WebSSH2 service reading config from: ' + configPath)
|
||||
var config = require('./config')
|
||||
var express = require('express')
|
||||
var logger = require('morgan')
|
||||
|
||||
// sane defaults if config.json or parts are missing
|
||||
let config = {
|
||||
listen: {
|
||||
ip: '0.0.0.0',
|
||||
port: 2222
|
||||
},
|
||||
user: {
|
||||
name: null,
|
||||
password: null,
|
||||
privatekey: null
|
||||
},
|
||||
ssh: {
|
||||
host: null,
|
||||
port: 22,
|
||||
term: 'xterm-color',
|
||||
readyTimeout: 20000,
|
||||
keepaliveInterval: 120000,
|
||||
keepaliveCountMax: 10,
|
||||
allowedSubnets: []
|
||||
},
|
||||
terminal: {
|
||||
cursorBlink: true,
|
||||
scrollback: 10000,
|
||||
tabStopWidth: 8,
|
||||
bellStyle: 'sound'
|
||||
},
|
||||
header: {
|
||||
text: null,
|
||||
background: 'green'
|
||||
},
|
||||
session: {
|
||||
name: 'WebSSH2',
|
||||
secret: 'mysecret'
|
||||
},
|
||||
options: {
|
||||
challengeButton: true,
|
||||
allowreauth: true
|
||||
},
|
||||
algorithms: {
|
||||
kex: [
|
||||
'ecdh-sha2-nistp256',
|
||||
'ecdh-sha2-nistp384',
|
||||
'ecdh-sha2-nistp521',
|
||||
'diffie-hellman-group-exchange-sha256',
|
||||
'diffie-hellman-group14-sha1'
|
||||
],
|
||||
cipher: [
|
||||
'aes128-ctr',
|
||||
'aes192-ctr',
|
||||
'aes256-ctr',
|
||||
'aes128-gcm',
|
||||
'aes128-gcm@openssh.com',
|
||||
'aes256-gcm',
|
||||
'aes256-gcm@openssh.com',
|
||||
'aes256-cbc'
|
||||
],
|
||||
hmac: [
|
||||
'hmac-sha2-256',
|
||||
'hmac-sha2-512',
|
||||
'hmac-sha1'
|
||||
],
|
||||
compress: [
|
||||
'none',
|
||||
'zlib@openssh.com',
|
||||
'zlib'
|
||||
]
|
||||
},
|
||||
serverlog: {
|
||||
client: false,
|
||||
server: false
|
||||
},
|
||||
accesslog: false,
|
||||
verify: false
|
||||
}
|
||||
|
||||
// test if config.json exists, if not provide error message but try to run
|
||||
// anyway
|
||||
try {
|
||||
if (fs.existsSync(configPath)) {
|
||||
console.log('ephemeral_auth service reading config from: ' + configPath)
|
||||
config = require('read-config')(configPath)
|
||||
} else {
|
||||
console.error('\n\nERROR: Missing config.json for webssh. Current config: ' + JSON.stringify(config))
|
||||
console.error('\n See config.json.sample for details\n\n')
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('\n\nERROR: Missing config.json for webssh. Current config: ' + JSON.stringify(config))
|
||||
console.error('\n See config.json.sample for details\n\n')
|
||||
console.error('ERROR:\n\n ' + err)
|
||||
}
|
||||
|
||||
var app = express()
|
||||
var compression = require('compression')
|
||||
var server = require('http').Server(app)
|
||||
var myutil = require('./util')
|
||||
var io = require('socket.io')(server, { serveClient: false })
|
||||
var socket = require('./socket')
|
||||
var expressOptions = require('./expressOptions')
|
||||
var session = require('express-session')({
|
||||
secret: config.session.secret,
|
||||
name: config.session.name,
|
||||
|
|
@ -109,15 +22,6 @@ var session = require('express-session')({
|
|||
saveUninitialized: false,
|
||||
unset: 'destroy'
|
||||
})
|
||||
var app = express()
|
||||
var compression = require('compression')
|
||||
var server = require('http').Server(app)
|
||||
var myutil = require('./util')
|
||||
myutil.setDefaultCredentials(config.user.name, config.user.password, config.user.privatekey);
|
||||
var validator = require('validator')
|
||||
var io = require('socket.io')(server, { serveClient: false })
|
||||
var socket = require('./socket')
|
||||
var expressOptions = require('./expressOptions')
|
||||
|
||||
// express
|
||||
app.use(compression({ level: 9 }))
|
||||
|
|
@ -137,44 +41,6 @@ app.get('/reauth', function (req, res, next) {
|
|||
// eslint-disable-next-line complexity
|
||||
app.get('/ssh/host/:host?', function (req, res, next) {
|
||||
res.sendFile(path.join(path.join(publicPath, 'client.htm')))
|
||||
// capture, assign, and validated variables
|
||||
req.session.ssh = {
|
||||
host: (validator.isIP(req.params.host + '') && req.params.host) ||
|
||||
(validator.isFQDN(req.params.host) && req.params.host) ||
|
||||
(/^(([a-z]|[A-Z]|[0-9]|[!^(){}\-_~])+)?\w$/.test(req.params.host) &&
|
||||
req.params.host) || config.ssh.host,
|
||||
port: (validator.isInt(req.query.port + '', { min: 1, max: 65535 }) &&
|
||||
req.query.port) || config.ssh.port,
|
||||
localAddress: config.ssh.localAddress,
|
||||
localPort: config.ssh.localPort,
|
||||
header: {
|
||||
name: req.query.header || config.header.text,
|
||||
background: req.query.headerBackground || config.header.background
|
||||
},
|
||||
algorithms: config.algorithms,
|
||||
keepaliveInterval: config.ssh.keepaliveInterval,
|
||||
keepaliveCountMax: config.ssh.keepaliveCountMax,
|
||||
allowedSubnets: config.ssh.allowedSubnets,
|
||||
term: (/^(([a-z]|[A-Z]|[0-9]|[!^(){}\-_~])+)?\w$/.test(req.query.sshterm) &&
|
||||
req.query.sshterm) || config.ssh.term,
|
||||
terminal: {
|
||||
cursorBlink: (validator.isBoolean(req.query.cursorBlink + '') ? myutil.parseBool(req.query.cursorBlink) : config.terminal.cursorBlink),
|
||||
scrollback: (validator.isInt(req.query.scrollback + '', { min: 1, max: 200000 }) && req.query.scrollback) ? req.query.scrollback : config.terminal.scrollback,
|
||||
tabStopWidth: (validator.isInt(req.query.tabStopWidth + '', { min: 1, max: 100 }) && req.query.tabStopWidth) ? req.query.tabStopWidth : config.terminal.tabStopWidth,
|
||||
bellStyle: ((req.query.bellStyle) && (['sound', 'none'].indexOf(req.query.bellStyle) > -1)) ? req.query.bellStyle : config.terminal.bellStyle
|
||||
},
|
||||
allowreplay: config.options.challengeButton || (validator.isBoolean(req.headers.allowreplay + '') ? myutil.parseBool(req.headers.allowreplay) : false),
|
||||
allowreauth: config.options.allowreauth || false,
|
||||
mrhsession: ((validator.isAlphanumeric(req.headers.mrhsession + '') && req.headers.mrhsession) ? req.headers.mrhsession : 'none'),
|
||||
serverlog: {
|
||||
client: config.serverlog.client || false,
|
||||
server: config.serverlog.server || false
|
||||
},
|
||||
readyTimeout: (validator.isInt(req.query.readyTimeout + '', { min: 1, max: 300000 }) &&
|
||||
req.query.readyTimeout) || config.ssh.readyTimeout
|
||||
}
|
||||
if (req.session.ssh.header.name) validator.escape(req.session.ssh.header.name)
|
||||
if (req.session.ssh.header.background) validator.escape(req.session.ssh.header.background)
|
||||
})
|
||||
|
||||
// express error handling
|
||||
|
|
|
|||
|
|
@ -4,44 +4,214 @@
|
|||
// socket.js
|
||||
|
||||
// private
|
||||
var config = require('./config')
|
||||
var validator = require('validator')
|
||||
var debug = require('debug')
|
||||
var myutil = require('./util')
|
||||
var debugWebSSH2 = require('debug')('WebSSH2')
|
||||
var SSH = require('ssh2').Client
|
||||
var CIDRMatcher = require('cidr-matcher');
|
||||
var CIDRMatcher = require('cidr-matcher')
|
||||
// var fs = require('fs')
|
||||
// var hostkeys = JSON.parse(fs.readFileSync('./hostkeyhashes.json', 'utf8'))
|
||||
var termCols, termRows
|
||||
var menuData = '<a id="logBtn"><i class="fas fa-clipboard fa-fw"></i> Start Log</a>' +
|
||||
'<a id="downloadLogBtn"><i class="fas fa-download fa-fw"></i> Download Log</a>'
|
||||
|
||||
// return a subset of keys from an object if they exist
|
||||
function pick (obj, keys) {
|
||||
return keys.reduce((acc, key) => {
|
||||
if (obj[key]) {
|
||||
acc[key] = obj[key]
|
||||
}
|
||||
return acc
|
||||
}, {})
|
||||
}
|
||||
|
||||
const baseSocketConfig = {
|
||||
host: config.ssh.host,
|
||||
port: config.ssh.port,
|
||||
localAddress: config.ssh.localAddress,
|
||||
localPort: config.ssh.localPort,
|
||||
term: config.ssh.term,
|
||||
readyTimeout: config.ssh.readyTimeout,
|
||||
algorithms: config.algorithms,
|
||||
keepaliveInterval: config.ssh.keepaliveInterval,
|
||||
keepaliveCountMax: config.ssh.keepaliveCountMax,
|
||||
allowedSubnets: config.ssh.allowedSubnets || [],
|
||||
header: {
|
||||
name: config.header.text,
|
||||
background: config.header.background
|
||||
},
|
||||
terminal: {
|
||||
cursorBlink: config.terminal.cursorBlink,
|
||||
scrollBack: config.terminal.scrollback,
|
||||
tabStopWidth: config.terminal.tabStopWidth,
|
||||
bellStyle: config.terminal.bellStyle
|
||||
},
|
||||
serverlog: {
|
||||
client: config.serverlog.client || false,
|
||||
server: config.serverlog.server || false
|
||||
}
|
||||
}
|
||||
|
||||
function getValidatedRequestConfig (queryParams) {
|
||||
const processedParams = {}
|
||||
const validators = {
|
||||
host: (host) => validator.isIP(host + '') || validator.isFQDN(host) || /^(([a-z]|[A-Z]|[0-9]|[!^(){}\-_~])+)?\w$/.test(host),
|
||||
port: (port) => validator.isInt(port + '', { min: 1, max: 65535 }),
|
||||
sshterm: (sshterm) => /^(([a-z]|[A-Z]|[0-9]|[!^(){}\-_~])+)?\w$/.test(sshterm),
|
||||
cursorBlink: (cursorBlink) => validator.isBoolean(cursorBlink + ''),
|
||||
scrollback: (scrollback) => validator.isInt(scrollback + '', { min: 1, max: 200000 }),
|
||||
tabStopWidth: (tabStopWidth) => validator.isInt(tabStopWidth + '', { min: 1, max: 100 }),
|
||||
bellStyle: (bellStyle) => (['sound', 'none'].indexOf(bellStyle) > -1),
|
||||
readyTimeout: (readyTimeout) => validator.isInt(readyTimeout + '', { min: 1, max: 300000 }),
|
||||
header: () => true,
|
||||
headerBackground: () => true
|
||||
}
|
||||
const transformations = {
|
||||
cursorBlink: (cursorBlink) => myutil.parseBool(cursorBlink)
|
||||
}
|
||||
const rename = {
|
||||
sshterm: 'term'
|
||||
}
|
||||
|
||||
// validate & transform and rename query parameters
|
||||
for (const key in queryParams) {
|
||||
const value = queryParams[key]
|
||||
const validator = validators[key] || (() => false)
|
||||
const transformation = transformations[key] || ((i) => i)
|
||||
const newName = rename[key] || key
|
||||
|
||||
if (value !== undefined && validator(value)) {
|
||||
processedParams[newName] = transformation(value)
|
||||
}
|
||||
}
|
||||
|
||||
// todo: address all this!!
|
||||
// const allowreplay = config.options.challengeButton || (validator.isBoolean(req.headers.allowreplay + '') ? myutil.parseBool(req.headers.allowreplay) : false)
|
||||
// const allowreauth = config.options.allowreauth || false
|
||||
// const mrhsession = ((validator.isAlphanumeric(req.headers.mrhsession + '') && req.headers.mrhsession) ? req.headers.mrhsession : 'none')
|
||||
// if (req.session.ssh.header.name) validator.escape(req.session.ssh.header.name)
|
||||
// if (req.session.ssh.header.background) validator.escape(req.session.ssh.header.background)
|
||||
// todo: do this when creating base config?
|
||||
// if (socketConfig.header.name) {
|
||||
// validator.escape(socketConfig.header.name)
|
||||
// }
|
||||
// if (socketConfig.header.background) {
|
||||
// validator.escape(socketConfig.header.background)
|
||||
// }
|
||||
|
||||
// create config object from query parameters
|
||||
const config = pick(processedParams, ['host', 'port', 'readyTimeout', 'term'])
|
||||
config.terminal = pick(processedParams, ['cursorBlink', 'scrollback', 'tabStopWidth', 'bellStyle'])
|
||||
config.header = pick(processedParams, ['header', 'headerBackground'])
|
||||
|
||||
return config
|
||||
}
|
||||
|
||||
function getCredentials (session) {
|
||||
if (session.username && session.userpassword) {
|
||||
return {
|
||||
username: session.username,
|
||||
userpassword: session.userpassword
|
||||
}
|
||||
} else {
|
||||
return myutil.defaultCredentials
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Error handling for various events. Outputs error to client, logs to
|
||||
* server, destroys session and disconnects socket.
|
||||
* @param {string} callerName Function calling this function
|
||||
* @param {object} err Error object or error message
|
||||
* @param {object} context Additional information about the state when the error occurred
|
||||
* @param {object} context.socket The socket.io socket object at the time of failure
|
||||
* @param {object} context.socketConfig The config object based on the base config and the request query parameters
|
||||
* @param {object} context.credentials The credentials used during the connection that failed
|
||||
*/
|
||||
// eslint-disable-next-line complexity
|
||||
function SSHError (callerName, err, { socket, credentials, socketConfig }) {
|
||||
var theError
|
||||
const session = socket.request.session
|
||||
|
||||
if (session) {
|
||||
// we just want the first error of the session to pass to the client
|
||||
session.error = session.error || ((err) ? err.message : undefined)
|
||||
theError = session.error ? ': ' + session.error : ''
|
||||
|
||||
// log unsuccessful login attempt
|
||||
if (err && (err.level === 'client-authentication')) {
|
||||
console.log('WebSSH2 ' + 'error: Authentication failure'.red.bold +
|
||||
' user=' + credentials.username.yellow.bold.underline +
|
||||
' from=' + socket.handshake.address.yellow.bold.underline)
|
||||
|
||||
socket.emit('allowreauth', socketConfig.allowreauth)
|
||||
socket.emit('reauth')
|
||||
} else {
|
||||
console.log('WebSSH2 Logout: user=' + credentials.username +
|
||||
' from=' + socket.handshake.address +
|
||||
' host=' + socketConfig.host +
|
||||
' port=' + socketConfig.port +
|
||||
' sessionID=' + socket.request.sessionID + '/' + socket.id +
|
||||
' allowreplay=' + socketConfig.allowreplay +
|
||||
' term=' + socketConfig.term
|
||||
)
|
||||
if (err) {
|
||||
theError = err ? ': ' + err.message : ''
|
||||
console.log('WebSSH2 error' + theError)
|
||||
}
|
||||
}
|
||||
|
||||
socket.emit('ssherror', 'SSH ' + callerName + theError)
|
||||
session.destroy()
|
||||
} else {
|
||||
theError = (err) ? ': ' + err.message : ''
|
||||
}
|
||||
|
||||
socket.disconnect(true)
|
||||
|
||||
debugWebSSH2('SSHError ' + callerName + theError)
|
||||
}
|
||||
|
||||
// public
|
||||
module.exports = function socket (socket) {
|
||||
// if websocket connection arrives without an express session, kill it
|
||||
if (!socket.request.session) {
|
||||
socket.emit('401 UNAUTHORIZED')
|
||||
debugWebSSH2('SOCKET: No Express Session / REJECTED')
|
||||
// create new config by merging config object from disk with config object from the request
|
||||
const socketConfig = Object.assign({}, baseSocketConfig, getValidatedRequestConfig(socket.handshake.query))
|
||||
const credentials = getCredentials(socket.request.session)
|
||||
const hasCredentials = credentials.username && (credentials.userpassword || credentials.privatekey)
|
||||
const errorContext = { socket, credentials, socketConfig };
|
||||
|
||||
if (!(hasCredentials && socketConfig)) {
|
||||
debugWebSSH2('Attempt to connect without session.username/password or session varialbles defined, ' +
|
||||
'potentially previously abandoned client session. disconnecting websocket client.\r\n' +
|
||||
'Handshake information: \r\n ' + JSON.stringify(socket.handshake))
|
||||
socket.emit('ssherror', 'WEBSOCKET ERROR - Refresh the browser and try again')
|
||||
socket.request.session.destroy()
|
||||
socket.disconnect(true)
|
||||
return
|
||||
}
|
||||
|
||||
// If configured, check that requsted host is in a permitted subnet
|
||||
if ( (((socket.request.session || {}).ssh || {}).allowedSubnets || {}).length && ( socket.request.session.ssh.allowedSubnets.length > 0 ) ) {
|
||||
var matcher = new CIDRMatcher(socket.request.session.ssh.allowedSubnets);
|
||||
if (!matcher.contains(socket.request.session.ssh.host)) {
|
||||
if (socketConfig.allowedSubnets.length > 0) {
|
||||
const matcher = new CIDRMatcher(socketConfig.allowedSubnets)
|
||||
if (!matcher.contains(socketConfig.host)) {
|
||||
console.log('WebSSH2 ' + 'error: Requested host outside configured subnets / REJECTED'.red.bold +
|
||||
' user=' + socket.request.session.username.yellow.bold.underline +
|
||||
' from=' + socket.handshake.address.yellow.bold.underline)
|
||||
' user=' + credentials.username.yellow.bold.underline +
|
||||
' from=' + socket.handshake.address.yellow.bold.underline)
|
||||
socket.emit('ssherror', '401 UNAUTHORIZED')
|
||||
socket.disconnect(true)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
var conn = new SSH()
|
||||
const conn = new SSH()
|
||||
|
||||
socket.on('geometry', function socketOnGeometry (cols, rows) {
|
||||
termCols = cols
|
||||
termRows = rows
|
||||
})
|
||||
|
||||
conn.on('banner', function connOnBanner (data) {
|
||||
// need to convert to cr/lf for proper formatting
|
||||
data = data.replace(/\r?\n/g, '\r\n')
|
||||
|
|
@ -49,35 +219,45 @@ module.exports = function socket (socket) {
|
|||
})
|
||||
|
||||
conn.on('ready', function connOnReady () {
|
||||
console.log('WebSSH2 Login: user=' + socket.request.session.username + ' from=' + socket.handshake.address + ' host=' + socket.request.session.ssh.host + ' port=' + socket.request.session.ssh.port + ' sessionID=' + socket.request.sessionID + '/' + socket.id + ' mrhsession=' + socket.request.session.ssh.mrhsession + ' allowreplay=' + socket.request.session.ssh.allowreplay + ' term=' + socket.request.session.ssh.term)
|
||||
console.log('WebSSH2 Login: user=' + credentials.username +
|
||||
' from=' + socket.handshake.address +
|
||||
' host=' + socketConfig.host +
|
||||
' port=' + socketConfig.port +
|
||||
' sessionID=' + socket.request.sessionID + '/' + socket.id +
|
||||
' mrhsession=' + socketConfig.mrhsession +
|
||||
' allowreplay=' + socketConfig.allowreplay +
|
||||
' term=' + socketConfig.term
|
||||
)
|
||||
|
||||
socket.emit('menu', menuData)
|
||||
socket.emit('allowreauth', socket.request.session.ssh.allowreauth)
|
||||
socket.emit('setTerminalOpts', socket.request.session.ssh.terminal)
|
||||
socket.emit('title', 'ssh://' + socket.request.session.ssh.host)
|
||||
if (socket.request.session.ssh.header.background) socket.emit('headerBackground', socket.request.session.ssh.header.background)
|
||||
if (socket.request.session.ssh.header.name) socket.emit('header', socket.request.session.ssh.header.name)
|
||||
socket.emit('footer', 'ssh://' + socket.request.session.username + '@' + socket.request.session.ssh.host + ':' + socket.request.session.ssh.port)
|
||||
socket.emit('allowreauth', socketConfig.allowreauth)
|
||||
socket.emit('setTerminalOpts', socketConfig.terminal)
|
||||
socket.emit('title', 'ssh://' + socketConfig.host)
|
||||
if (socketConfig.header.background) socket.emit('headerBackground', socketConfig.header.background)
|
||||
if (socketConfig.header.name) socket.emit('header', socketConfig.header.name)
|
||||
socket.emit('footer', 'ssh://' + credentials.username + '@' + socketConfig.host + ':' + socketConfig.port)
|
||||
socket.emit('status', 'SSH CONNECTION ESTABLISHED')
|
||||
socket.emit('statusBackground', 'green')
|
||||
socket.emit('allowreplay', socket.request.session.ssh.allowreplay)
|
||||
socket.emit('allowreplay', socketConfig.allowreplay)
|
||||
|
||||
conn.shell({
|
||||
term: socket.request.session.ssh.term,
|
||||
term: socketConfig.term,
|
||||
cols: termCols,
|
||||
rows: termRows
|
||||
}, function connShell (err, stream) {
|
||||
if (err) {
|
||||
SSHerror('EXEC ERROR' + err)
|
||||
SSHError('EXEC ERROR', err, errorContext)
|
||||
conn.end()
|
||||
return
|
||||
}
|
||||
// poc to log commands from client
|
||||
if (socket.request.session.ssh.serverlog.client) var dataBuffer
|
||||
if (socketConfig.serverlog.client) var dataBuffer
|
||||
socket.on('data', function socketOnData (data) {
|
||||
stream.write(data)
|
||||
// poc to log commands from client
|
||||
if (socket.request.session.ssh.serverlog.client) {
|
||||
if (socketConfig.serverlog.client) {
|
||||
if (data === '\r') {
|
||||
console.log('serverlog.client: ' + socket.request.session.id + '/' + socket.id + ' host: ' + socket.request.session.ssh.host + ' command: ' + dataBuffer)
|
||||
console.log('serverlog.client: ' + socket.request.session.id + '/' + socket.id + ' host: ' + socketConfig.host + ' command: ' + dataBuffer)
|
||||
dataBuffer = undefined
|
||||
} else {
|
||||
dataBuffer = (dataBuffer) ? dataBuffer + data : data
|
||||
|
|
@ -87,8 +267,8 @@ module.exports = function socket (socket) {
|
|||
socket.on('control', function socketOnControl (controlData) {
|
||||
switch (controlData) {
|
||||
case 'replayCredentials':
|
||||
if (socket.request.session.ssh.allowreplay) {
|
||||
stream.write(socket.request.session.userpassword + '\n')
|
||||
if (socketConfig.allowreplay) {
|
||||
stream.write(credentials.userpassword + '\n')
|
||||
}
|
||||
/* falls through */
|
||||
default:
|
||||
|
|
@ -102,19 +282,19 @@ module.exports = function socket (socket) {
|
|||
socket.on('disconnect', function socketOnDisconnect (reason) {
|
||||
debugWebSSH2('SOCKET DISCONNECT: ' + reason)
|
||||
err = { message: reason }
|
||||
SSHerror('CLIENT SOCKET DISCONNECT', err)
|
||||
SSHError('CLIENT SOCKET DISCONNECT', err, errorContext)
|
||||
conn.end()
|
||||
// socket.request.session.destroy()
|
||||
})
|
||||
socket.on('error', function socketOnError (err) {
|
||||
SSHerror('SOCKET ERROR', err)
|
||||
SSHError('SOCKET ERROR', err, errorContext)
|
||||
conn.end()
|
||||
})
|
||||
|
||||
stream.on('data', function streamOnData (data) { socket.emit('data', data.toString('utf-8')) })
|
||||
stream.on('close', function streamOnClose (code, signal) {
|
||||
err = { message: ((code || signal) ? (((code) ? 'CODE: ' + code : '') + ((code && signal) ? ' ' : '') + ((signal) ? 'SIGNAL: ' + signal : '')) : undefined) }
|
||||
SSHerror('STREAM CLOSE', err)
|
||||
SSHError('STREAM CLOSE', err, errorContext)
|
||||
conn.end()
|
||||
})
|
||||
stream.stderr.on('data', function streamStderrOnData (data) {
|
||||
|
|
@ -123,71 +303,28 @@ module.exports = function socket (socket) {
|
|||
})
|
||||
})
|
||||
|
||||
conn.on('end', function connOnEnd (err) { SSHerror('CONN END BY HOST', err) })
|
||||
conn.on('close', function connOnClose (err) { SSHerror('CONN CLOSE', err) })
|
||||
conn.on('error', function connOnError (err) { SSHerror('CONN ERROR', err) })
|
||||
conn.on('end', function connOnEnd (err) { SSHError('CONN END BY HOST', err, errorContext) })
|
||||
conn.on('close', function connOnClose (err) { SSHError('CONN CLOSE', err, errorContext) })
|
||||
conn.on('error', function connOnError (err) { SSHError('CONN ERROR', err, errorContext) })
|
||||
conn.on('keyboard-interactive', function connOnKeyboardInteractive (name, instructions, instructionsLang, prompts, finish) {
|
||||
debugWebSSH2('conn.on(\'keyboard-interactive\')')
|
||||
finish([socket.request.session.userpassword])
|
||||
finish([credentials.userpassword])
|
||||
})
|
||||
if (socket.request.session.username && (socket.request.session.userpassword || socket.request.session.privatekey) && socket.request.session.ssh) {
|
||||
// console.log('hostkeys: ' + hostkeys[0].[0])
|
||||
conn.connect({
|
||||
host: socket.request.session.ssh.host,
|
||||
port: socket.request.session.ssh.port,
|
||||
localAddress: socket.request.session.ssh.localAddress,
|
||||
localPort: socket.request.session.ssh.localPort,
|
||||
username: socket.request.session.username,
|
||||
password: socket.request.session.userpassword,
|
||||
privateKey: socket.request.session.privatekey,
|
||||
tryKeyboard: true,
|
||||
algorithms: socket.request.session.ssh.algorithms,
|
||||
readyTimeout: socket.request.session.ssh.readyTimeout,
|
||||
keepaliveInterval: socket.request.session.ssh.keepaliveInterval,
|
||||
keepaliveCountMax: socket.request.session.ssh.keepaliveCountMax,
|
||||
debug: debug('ssh2')
|
||||
})
|
||||
} else {
|
||||
debugWebSSH2('Attempt to connect without session.username/password or session varialbles defined, potentially previously abandoned client session. disconnecting websocket client.\r\nHandshake information: \r\n ' + JSON.stringify(socket.handshake))
|
||||
socket.emit('ssherror', 'WEBSOCKET ERROR - Refresh the browser and try again')
|
||||
socket.request.session.destroy()
|
||||
socket.disconnect(true)
|
||||
}
|
||||
|
||||
/**
|
||||
* Error handling for various events. Outputs error to client, logs to
|
||||
* server, destroys session and disconnects socket.
|
||||
* @param {string} myFunc Function calling this function
|
||||
* @param {object} err error object or error message
|
||||
*/
|
||||
// eslint-disable-next-line complexity
|
||||
function SSHerror (myFunc, err) {
|
||||
var theError
|
||||
if (socket.request.session) {
|
||||
// we just want the first error of the session to pass to the client
|
||||
socket.request.session.error = (socket.request.session.error) || ((err) ? err.message : undefined)
|
||||
theError = (socket.request.session.error) ? ': ' + socket.request.session.error : ''
|
||||
// log unsuccessful login attempt
|
||||
if (err && (err.level === 'client-authentication')) {
|
||||
console.log('WebSSH2 ' + 'error: Authentication failure'.red.bold +
|
||||
' user=' + socket.request.session.username.yellow.bold.underline +
|
||||
' from=' + socket.handshake.address.yellow.bold.underline)
|
||||
socket.emit('allowreauth', socket.request.session.ssh.allowreauth)
|
||||
socket.emit('reauth')
|
||||
} else {
|
||||
console.log('WebSSH2 Logout: user=' + socket.request.session.username + ' from=' + socket.handshake.address + ' host=' + socket.request.session.ssh.host + ' port=' + socket.request.session.ssh.port + ' sessionID=' + socket.request.sessionID + '/' + socket.id + ' allowreplay=' + socket.request.session.ssh.allowreplay + ' term=' + socket.request.session.ssh.term)
|
||||
if (err) {
|
||||
theError = (err) ? ': ' + err.message : ''
|
||||
console.log('WebSSH2 error' + theError)
|
||||
}
|
||||
}
|
||||
socket.emit('ssherror', 'SSH ' + myFunc + theError)
|
||||
socket.request.session.destroy()
|
||||
socket.disconnect(true)
|
||||
} else {
|
||||
theError = (err) ? ': ' + err.message : ''
|
||||
socket.disconnect(true)
|
||||
}
|
||||
debugWebSSH2('SSHerror ' + myFunc + theError)
|
||||
}
|
||||
// console.log('hostkeys: ' + hostkeys[0].[0])
|
||||
conn.connect({
|
||||
host: socketConfig.host,
|
||||
port: socketConfig.port,
|
||||
localAddress: socketConfig.localAddress,
|
||||
localPort: socketConfig.localPort,
|
||||
username: credentials.username,
|
||||
password: credentials.userpassword,
|
||||
privateKey: credentials.privatekey,
|
||||
tryKeyboard: true,
|
||||
algorithms: socketConfig.algorithms,
|
||||
readyTimeout: socketConfig.readyTimeout,
|
||||
keepaliveInterval: socketConfig.keepaliveInterval,
|
||||
keepaliveCountMax: socketConfig.keepaliveCountMax,
|
||||
debug: debug('ssh2')
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,37 +4,36 @@
|
|||
|
||||
// private
|
||||
require('colors') // allow for color property extensions in log messages
|
||||
var config = require('./config')
|
||||
var debug = require('debug')('WebSSH2')
|
||||
var Auth = require('basic-auth')
|
||||
|
||||
let defaultCredentials = {username: null, password: null, privatekey: null};
|
||||
|
||||
exports.setDefaultCredentials = function (username, password, privatekey) {
|
||||
defaultCredentials.username = username
|
||||
defaultCredentials.password = password
|
||||
defaultCredentials.privatekey = privatekey
|
||||
exports.defaultCredentials = {
|
||||
username: config.user.name,
|
||||
userpassword: config.user.password,
|
||||
privatekey: config.user.privatekey,
|
||||
}
|
||||
|
||||
exports.basicAuth = function basicAuth (req, res, next) {
|
||||
var myAuth = Auth(req)
|
||||
let password = exports.defaultCredentials.userpassword
|
||||
|
||||
if (myAuth && myAuth.pass !== '') {
|
||||
req.session.username = myAuth.name
|
||||
req.session.userpassword = myAuth.pass
|
||||
req.session.userpassword = password = myAuth.pass
|
||||
debug('myAuth.name: ' + myAuth.name.yellow.bold.underline +
|
||||
' and password ' + ((myAuth.pass) ? 'exists'.yellow.bold.underline
|
||||
: 'is blank'.underline.red.bold))
|
||||
} else {
|
||||
req.session.username = defaultCredentials.username;
|
||||
req.session.userpassword = defaultCredentials.password;
|
||||
req.session.privatekey = defaultCredentials.privatekey;
|
||||
}
|
||||
if ( (!req.session.userpassword) && (!req.session.privatekey) ) {
|
||||
|
||||
if (!(password || exports.defaultCredentials.privatekey)) {
|
||||
res.statusCode = 401
|
||||
debug('basicAuth credential request (401)')
|
||||
res.setHeader('WWW-Authenticate', 'Basic realm="WebSSH"')
|
||||
res.end('Username and password required for web SSH service.')
|
||||
return
|
||||
}
|
||||
|
||||
next()
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue