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
|
// app.js
|
||||||
|
|
||||||
var path = require('path')
|
var path = require('path')
|
||||||
var fs = require('fs')
|
|
||||||
var nodeRoot = path.dirname(require.main.filename)
|
var nodeRoot = path.dirname(require.main.filename)
|
||||||
var configPath = path.join(nodeRoot, 'config.json')
|
|
||||||
var publicPath = path.join(nodeRoot, 'client', 'public')
|
var publicPath = path.join(nodeRoot, 'client', 'public')
|
||||||
console.log('WebSSH2 service reading config from: ' + configPath)
|
var config = require('./config')
|
||||||
var express = require('express')
|
var express = require('express')
|
||||||
var logger = require('morgan')
|
var logger = require('morgan')
|
||||||
|
var app = express()
|
||||||
// sane defaults if config.json or parts are missing
|
var compression = require('compression')
|
||||||
let config = {
|
var server = require('http').Server(app)
|
||||||
listen: {
|
var myutil = require('./util')
|
||||||
ip: '0.0.0.0',
|
var io = require('socket.io')(server, { serveClient: false })
|
||||||
port: 2222
|
var socket = require('./socket')
|
||||||
},
|
var expressOptions = require('./expressOptions')
|
||||||
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 session = require('express-session')({
|
var session = require('express-session')({
|
||||||
secret: config.session.secret,
|
secret: config.session.secret,
|
||||||
name: config.session.name,
|
name: config.session.name,
|
||||||
|
|
@ -109,15 +22,6 @@ var session = require('express-session')({
|
||||||
saveUninitialized: false,
|
saveUninitialized: false,
|
||||||
unset: 'destroy'
|
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
|
// express
|
||||||
app.use(compression({ level: 9 }))
|
app.use(compression({ level: 9 }))
|
||||||
|
|
@ -137,44 +41,6 @@ app.get('/reauth', function (req, res, next) {
|
||||||
// eslint-disable-next-line complexity
|
// eslint-disable-next-line complexity
|
||||||
app.get('/ssh/host/:host?', function (req, res, next) {
|
app.get('/ssh/host/:host?', function (req, res, next) {
|
||||||
res.sendFile(path.join(path.join(publicPath, 'client.htm')))
|
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
|
// express error handling
|
||||||
|
|
|
||||||
|
|
@ -4,32 +4,200 @@
|
||||||
// socket.js
|
// socket.js
|
||||||
|
|
||||||
// private
|
// private
|
||||||
|
var config = require('./config')
|
||||||
|
var validator = require('validator')
|
||||||
var debug = require('debug')
|
var debug = require('debug')
|
||||||
|
var myutil = require('./util')
|
||||||
var debugWebSSH2 = require('debug')('WebSSH2')
|
var debugWebSSH2 = require('debug')('WebSSH2')
|
||||||
var SSH = require('ssh2').Client
|
var SSH = require('ssh2').Client
|
||||||
var CIDRMatcher = require('cidr-matcher');
|
var CIDRMatcher = require('cidr-matcher')
|
||||||
// var fs = require('fs')
|
// var fs = require('fs')
|
||||||
// var hostkeys = JSON.parse(fs.readFileSync('./hostkeyhashes.json', 'utf8'))
|
// var hostkeys = JSON.parse(fs.readFileSync('./hostkeyhashes.json', 'utf8'))
|
||||||
var termCols, termRows
|
var termCols, termRows
|
||||||
var menuData = '<a id="logBtn"><i class="fas fa-clipboard fa-fw"></i> Start Log</a>' +
|
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>'
|
'<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
|
// public
|
||||||
module.exports = function socket (socket) {
|
module.exports = function socket (socket) {
|
||||||
// if websocket connection arrives without an express session, kill it
|
// create new config by merging config object from disk with config object from the request
|
||||||
if (!socket.request.session) {
|
const socketConfig = Object.assign({}, baseSocketConfig, getValidatedRequestConfig(socket.handshake.query))
|
||||||
socket.emit('401 UNAUTHORIZED')
|
const credentials = getCredentials(socket.request.session)
|
||||||
debugWebSSH2('SOCKET: No Express Session / REJECTED')
|
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)
|
socket.disconnect(true)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// If configured, check that requsted host is in a permitted subnet
|
// 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 ) ) {
|
if (socketConfig.allowedSubnets.length > 0) {
|
||||||
var matcher = new CIDRMatcher(socket.request.session.ssh.allowedSubnets);
|
const matcher = new CIDRMatcher(socketConfig.allowedSubnets)
|
||||||
if (!matcher.contains(socket.request.session.ssh.host)) {
|
if (!matcher.contains(socketConfig.host)) {
|
||||||
console.log('WebSSH2 ' + 'error: Requested host outside configured subnets / REJECTED'.red.bold +
|
console.log('WebSSH2 ' + 'error: Requested host outside configured subnets / REJECTED'.red.bold +
|
||||||
' user=' + socket.request.session.username.yellow.bold.underline +
|
' user=' + credentials.username.yellow.bold.underline +
|
||||||
' from=' + socket.handshake.address.yellow.bold.underline)
|
' from=' + socket.handshake.address.yellow.bold.underline)
|
||||||
socket.emit('ssherror', '401 UNAUTHORIZED')
|
socket.emit('ssherror', '401 UNAUTHORIZED')
|
||||||
socket.disconnect(true)
|
socket.disconnect(true)
|
||||||
|
|
@ -37,11 +205,13 @@ module.exports = function socket (socket) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var conn = new SSH()
|
const conn = new SSH()
|
||||||
|
|
||||||
socket.on('geometry', function socketOnGeometry (cols, rows) {
|
socket.on('geometry', function socketOnGeometry (cols, rows) {
|
||||||
termCols = cols
|
termCols = cols
|
||||||
termRows = rows
|
termRows = rows
|
||||||
})
|
})
|
||||||
|
|
||||||
conn.on('banner', function connOnBanner (data) {
|
conn.on('banner', function connOnBanner (data) {
|
||||||
// need to convert to cr/lf for proper formatting
|
// need to convert to cr/lf for proper formatting
|
||||||
data = data.replace(/\r?\n/g, '\r\n')
|
data = data.replace(/\r?\n/g, '\r\n')
|
||||||
|
|
@ -49,35 +219,45 @@ module.exports = function socket (socket) {
|
||||||
})
|
})
|
||||||
|
|
||||||
conn.on('ready', function connOnReady () {
|
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('menu', menuData)
|
||||||
socket.emit('allowreauth', socket.request.session.ssh.allowreauth)
|
socket.emit('allowreauth', socketConfig.allowreauth)
|
||||||
socket.emit('setTerminalOpts', socket.request.session.ssh.terminal)
|
socket.emit('setTerminalOpts', socketConfig.terminal)
|
||||||
socket.emit('title', 'ssh://' + socket.request.session.ssh.host)
|
socket.emit('title', 'ssh://' + socketConfig.host)
|
||||||
if (socket.request.session.ssh.header.background) socket.emit('headerBackground', socket.request.session.ssh.header.background)
|
if (socketConfig.header.background) socket.emit('headerBackground', socketConfig.header.background)
|
||||||
if (socket.request.session.ssh.header.name) socket.emit('header', socket.request.session.ssh.header.name)
|
if (socketConfig.header.name) socket.emit('header', socketConfig.header.name)
|
||||||
socket.emit('footer', 'ssh://' + socket.request.session.username + '@' + socket.request.session.ssh.host + ':' + socket.request.session.ssh.port)
|
socket.emit('footer', 'ssh://' + credentials.username + '@' + socketConfig.host + ':' + socketConfig.port)
|
||||||
socket.emit('status', 'SSH CONNECTION ESTABLISHED')
|
socket.emit('status', 'SSH CONNECTION ESTABLISHED')
|
||||||
socket.emit('statusBackground', 'green')
|
socket.emit('statusBackground', 'green')
|
||||||
socket.emit('allowreplay', socket.request.session.ssh.allowreplay)
|
socket.emit('allowreplay', socketConfig.allowreplay)
|
||||||
|
|
||||||
conn.shell({
|
conn.shell({
|
||||||
term: socket.request.session.ssh.term,
|
term: socketConfig.term,
|
||||||
cols: termCols,
|
cols: termCols,
|
||||||
rows: termRows
|
rows: termRows
|
||||||
}, function connShell (err, stream) {
|
}, function connShell (err, stream) {
|
||||||
if (err) {
|
if (err) {
|
||||||
SSHerror('EXEC ERROR' + err)
|
SSHError('EXEC ERROR', err, errorContext)
|
||||||
conn.end()
|
conn.end()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
// poc to log commands from client
|
// 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) {
|
socket.on('data', function socketOnData (data) {
|
||||||
stream.write(data)
|
stream.write(data)
|
||||||
// poc to log commands from client
|
// poc to log commands from client
|
||||||
if (socket.request.session.ssh.serverlog.client) {
|
if (socketConfig.serverlog.client) {
|
||||||
if (data === '\r') {
|
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
|
dataBuffer = undefined
|
||||||
} else {
|
} else {
|
||||||
dataBuffer = (dataBuffer) ? dataBuffer + data : data
|
dataBuffer = (dataBuffer) ? dataBuffer + data : data
|
||||||
|
|
@ -87,8 +267,8 @@ module.exports = function socket (socket) {
|
||||||
socket.on('control', function socketOnControl (controlData) {
|
socket.on('control', function socketOnControl (controlData) {
|
||||||
switch (controlData) {
|
switch (controlData) {
|
||||||
case 'replayCredentials':
|
case 'replayCredentials':
|
||||||
if (socket.request.session.ssh.allowreplay) {
|
if (socketConfig.allowreplay) {
|
||||||
stream.write(socket.request.session.userpassword + '\n')
|
stream.write(credentials.userpassword + '\n')
|
||||||
}
|
}
|
||||||
/* falls through */
|
/* falls through */
|
||||||
default:
|
default:
|
||||||
|
|
@ -102,19 +282,19 @@ module.exports = function socket (socket) {
|
||||||
socket.on('disconnect', function socketOnDisconnect (reason) {
|
socket.on('disconnect', function socketOnDisconnect (reason) {
|
||||||
debugWebSSH2('SOCKET DISCONNECT: ' + reason)
|
debugWebSSH2('SOCKET DISCONNECT: ' + reason)
|
||||||
err = { message: reason }
|
err = { message: reason }
|
||||||
SSHerror('CLIENT SOCKET DISCONNECT', err)
|
SSHError('CLIENT SOCKET DISCONNECT', err, errorContext)
|
||||||
conn.end()
|
conn.end()
|
||||||
// socket.request.session.destroy()
|
// socket.request.session.destroy()
|
||||||
})
|
})
|
||||||
socket.on('error', function socketOnError (err) {
|
socket.on('error', function socketOnError (err) {
|
||||||
SSHerror('SOCKET ERROR', err)
|
SSHError('SOCKET ERROR', err, errorContext)
|
||||||
conn.end()
|
conn.end()
|
||||||
})
|
})
|
||||||
|
|
||||||
stream.on('data', function streamOnData (data) { socket.emit('data', data.toString('utf-8')) })
|
stream.on('data', function streamOnData (data) { socket.emit('data', data.toString('utf-8')) })
|
||||||
stream.on('close', function streamOnClose (code, signal) {
|
stream.on('close', function streamOnClose (code, signal) {
|
||||||
err = { message: ((code || signal) ? (((code) ? 'CODE: ' + code : '') + ((code && signal) ? ' ' : '') + ((signal) ? 'SIGNAL: ' + signal : '')) : undefined) }
|
err = { message: ((code || signal) ? (((code) ? 'CODE: ' + code : '') + ((code && signal) ? ' ' : '') + ((signal) ? 'SIGNAL: ' + signal : '')) : undefined) }
|
||||||
SSHerror('STREAM CLOSE', err)
|
SSHError('STREAM CLOSE', err, errorContext)
|
||||||
conn.end()
|
conn.end()
|
||||||
})
|
})
|
||||||
stream.stderr.on('data', function streamStderrOnData (data) {
|
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('end', function connOnEnd (err) { SSHError('CONN END BY HOST', err, errorContext) })
|
||||||
conn.on('close', function connOnClose (err) { SSHerror('CONN CLOSE', err) })
|
conn.on('close', function connOnClose (err) { SSHError('CONN CLOSE', err, errorContext) })
|
||||||
conn.on('error', function connOnError (err) { SSHerror('CONN ERROR', err) })
|
conn.on('error', function connOnError (err) { SSHError('CONN ERROR', err, errorContext) })
|
||||||
conn.on('keyboard-interactive', function connOnKeyboardInteractive (name, instructions, instructionsLang, prompts, finish) {
|
conn.on('keyboard-interactive', function connOnKeyboardInteractive (name, instructions, instructionsLang, prompts, finish) {
|
||||||
debugWebSSH2('conn.on(\'keyboard-interactive\')')
|
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])
|
// console.log('hostkeys: ' + hostkeys[0].[0])
|
||||||
conn.connect({
|
conn.connect({
|
||||||
host: socket.request.session.ssh.host,
|
host: socketConfig.host,
|
||||||
port: socket.request.session.ssh.port,
|
port: socketConfig.port,
|
||||||
localAddress: socket.request.session.ssh.localAddress,
|
localAddress: socketConfig.localAddress,
|
||||||
localPort: socket.request.session.ssh.localPort,
|
localPort: socketConfig.localPort,
|
||||||
username: socket.request.session.username,
|
username: credentials.username,
|
||||||
password: socket.request.session.userpassword,
|
password: credentials.userpassword,
|
||||||
privateKey: socket.request.session.privatekey,
|
privateKey: credentials.privatekey,
|
||||||
tryKeyboard: true,
|
tryKeyboard: true,
|
||||||
algorithms: socket.request.session.ssh.algorithms,
|
algorithms: socketConfig.algorithms,
|
||||||
readyTimeout: socket.request.session.ssh.readyTimeout,
|
readyTimeout: socketConfig.readyTimeout,
|
||||||
keepaliveInterval: socket.request.session.ssh.keepaliveInterval,
|
keepaliveInterval: socketConfig.keepaliveInterval,
|
||||||
keepaliveCountMax: socket.request.session.ssh.keepaliveCountMax,
|
keepaliveCountMax: socketConfig.keepaliveCountMax,
|
||||||
debug: debug('ssh2')
|
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)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,37 +4,36 @@
|
||||||
|
|
||||||
// private
|
// private
|
||||||
require('colors') // allow for color property extensions in log messages
|
require('colors') // allow for color property extensions in log messages
|
||||||
|
var config = require('./config')
|
||||||
var debug = require('debug')('WebSSH2')
|
var debug = require('debug')('WebSSH2')
|
||||||
var Auth = require('basic-auth')
|
var Auth = require('basic-auth')
|
||||||
|
|
||||||
let defaultCredentials = {username: null, password: null, privatekey: null};
|
exports.defaultCredentials = {
|
||||||
|
username: config.user.name,
|
||||||
exports.setDefaultCredentials = function (username, password, privatekey) {
|
userpassword: config.user.password,
|
||||||
defaultCredentials.username = username
|
privatekey: config.user.privatekey,
|
||||||
defaultCredentials.password = password
|
|
||||||
defaultCredentials.privatekey = privatekey
|
|
||||||
}
|
}
|
||||||
|
|
||||||
exports.basicAuth = function basicAuth (req, res, next) {
|
exports.basicAuth = function basicAuth (req, res, next) {
|
||||||
var myAuth = Auth(req)
|
var myAuth = Auth(req)
|
||||||
|
let password = exports.defaultCredentials.userpassword
|
||||||
|
|
||||||
if (myAuth && myAuth.pass !== '') {
|
if (myAuth && myAuth.pass !== '') {
|
||||||
req.session.username = myAuth.name
|
req.session.username = myAuth.name
|
||||||
req.session.userpassword = myAuth.pass
|
req.session.userpassword = password = myAuth.pass
|
||||||
debug('myAuth.name: ' + myAuth.name.yellow.bold.underline +
|
debug('myAuth.name: ' + myAuth.name.yellow.bold.underline +
|
||||||
' and password ' + ((myAuth.pass) ? 'exists'.yellow.bold.underline
|
' and password ' + ((myAuth.pass) ? 'exists'.yellow.bold.underline
|
||||||
: 'is blank'.underline.red.bold))
|
: '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
|
res.statusCode = 401
|
||||||
debug('basicAuth credential request (401)')
|
debug('basicAuth credential request (401)')
|
||||||
res.setHeader('WWW-Authenticate', 'Basic realm="WebSSH"')
|
res.setHeader('WWW-Authenticate', 'Basic realm="WebSSH"')
|
||||||
res.end('Username and password required for web SSH service.')
|
res.end('Username and password required for web SSH service.')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
next()
|
next()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue