refactoring

moved some events into socket/index.js to better organize code
created session.ssh property for application session variables
moved crypto algorithms to config.json and assigned to
..session.ssh.algorithms variable
This commit is contained in:
billchurch 2017-05-26 11:25:44 -04:00
parent 938e2fbfa4
commit 2e912dd9cc
6 changed files with 217 additions and 170 deletions

View file

@ -5,12 +5,17 @@
- Snyk, Bithound, Travis CI
- Cross platform improvements (path mappings)
- Session fixup between Express and Socket.io
- Session secret settings in config.json
- env variable `DEBUG=ssh2` will put the `ssh2` module into debug mode
- env variable `debug=WebSSH2` will output additional debug messages for functions
- env variable `DEBUG=WebSSH2` will output additional debug messages for functions
and events in the application (not including the ssh2 module debug)
### Changed
- erorr handling in public/client.js
- moved socket.io operations to their own file /socket/index.js, more changes like this to come (./socket/index.js)
- all session based variables are now under the req.session.ssh property or socket.request.ssh (./index.js)
- moved SSH algorithms to config.json and defined as a session variable (..session.ssh.algorithms)
-- prep for future feature to define algorithims in header or some other method to enable seperate ciphers per host
### Fixed
- Multiple errors may ovewrite status bar which would cause confusion as to what originally caused the error. Example, ssh server disconnects which prompts a cascade of events (conn.on('end'), socket.on('disconnect'), conn.on('close')) and the original reason (conn.on('end')) would be lost and the user would erroneously receive a WEBSOCKET error as the last event to fire would be the websocket connection closing from the app.

View file

@ -10,17 +10,32 @@
"ssh": {
"host": null,
"port": 22,
"term": "xterm-color",
"term": "xterm-color"
},
"header": {
"text": "My Header",
"background": "green"
},
"session": {
"name": "WebSSH2id",
"name": "WebSSH2",
"secret": "mysecret"
},
"options": {
"challengeButton": true
},
"algorithms": {
"cipher": [
"aes128-cbc",
"3des-cbc",
"aes256-cbc",
"aes128-ctr",
"aes192-ctr",
"aes256-ctr"
],
"hmac": [
"hmac-sha1",
"hmac-sha1-96",
"hmac-md5-96"
]
}
}

194
index.js
View file

@ -8,40 +8,17 @@ var app = express()
var server = require('http').Server(app)
var io = require('socket.io')(server)
var path = require('path')
var SSH = require('ssh2').Client
var config = require('read-config')(path.join(__dirname, 'config.json'))
var debug = require('debug')
var debugWebSSH2 = debug('WebSSH2')
var util = require('./util')
var SocketUtil = require('./socket')
// var debug = require('debug')
// var debugWebSSH2 = debug('WebSSH2')
var myutil = require('./util')
var socket = require('./socket/index.js')
var session = require('express-session')({
secret: config.session.secret,
name: config.session.name,
resave: true,
saveUninitialized: false
})
var colors = require('colors/safe')
var termCols, termRows
// var LogPrefix
// var dataBuffer = ''
// server
server.listen({
host: config.listen.ip,
port: config.listen.port
})
server.on('error', function (err) {
if (err.code === 'EADDRINUSE') {
config.listen.port++
console.warn('Address in use, retrying on port ' + config.listen.port)
setTimeout(function () {
server.listen(config.listen.port)
}, 250)
} else {
console.log('server.listen ERROR: ' + err.code)
}
saveUninitialized: false,
unset: 'destroy'
})
// express
@ -58,30 +35,33 @@ var expressOptions = {
}
app.use(session)
app.use(util.basicAuth)
app.use(myutil.basicAuth)
app.disable('x-powered-by')
app.use(express.static(path.join(__dirname, 'public'), expressOptions))
app.get('/ssh/host/:host?', function (req, res, next) {
res.sendFile(path.join(path.join(__dirname, 'public', 'client.htm')))
// capture url variables if defined
config.ssh.host = req.params.host || config.ssh.host
config.ssh.port = req.query.port || config.ssh.port
config.header.text = req.query.header || config.header.text
config.header.background = req.query.headerBackground || config.header.background
console.log('webssh2 Login: user=' + req.session.username + ' from=' + req.ip + ' host=' + config.ssh.host + ' port=' + config.ssh.port + ' sessionID=' + req.sessionID + ' allowreplay=' + req.headers.allowreplay)
debugWebSSH2('Headers: ' + colors.yellow(JSON.stringify(req.headers)))
config.options.allowreplay = req.headers.allowreplay
// capture and assign variables
req.session.ssh = {
host: req.params.host || config.ssh.host,
port: req.query.port || config.ssh.port,
header: {
name: req.query.header || config.header.text,
background: req.query.headerBackground || config.header.background
},
algorithms: config.algorithms,
term: config.ssh.term,
allowreplay: req.headers.allowreplay || false
}
})
// static files
app.use(express.static(path.join(__dirname, 'public'), expressOptions))
app.use('/style', express.static(path.join(__dirname, 'public')))
app.use('/src', express.static(path.join(__dirname, 'node_modules', 'xterm', 'dist')))
app.use('/addons', express.static(path.join(__dirname, 'node_modules', 'xterm', 'dist', 'addons')))
// express error handling
app.use(function (req, res, next) {
res.status(404).send("Sorry can't find that!")
})
@ -92,124 +72,28 @@ app.use(function (err, req, res, next) {
})
// socket.io
// expose express session with socket.request.session
io.use(function (socket, next) {
if (socket.request.res) {
session(socket.request, socket.request.res, next)
} else {
next()
}
(socket.request.res) ? session(socket.request, socket.request.res, next) : next()
})
io.on('connection', function (socket) {
// if websocket connection arrives without an express session, kill it
if (!socket.request.session) {
socket.disconnect(true)
return
}
var socketutil = new SocketUtil(socket, io)
var conn = new SSH()
socket.on('geometry', function (cols, rows) {
termCols = cols
termRows = rows
})
// bring up socket
io.on('connection', socket)
conn.on('banner', function (d) {
// need to convert to cr/lf for proper formatting
d = d.replace(/\r?\n/g, '\r\n')
socket.emit('data', d.toString('binary'))
})
// server
server.listen({
host: config.listen.ip,
port: config.listen.port
})
conn.on('ready', function () {
socket.emit('title', 'ssh://' + config.ssh.host)
socket.emit('headerBackground', config.header.background)
socket.emit('header', config.header.text)
socket.emit('footer', 'ssh://' + socket.request.session.username + '@' + config.ssh.host + ':' + config.ssh.port)
socket.emit('status', 'SSH CONNECTION ESTABLISHED')
socket.emit('statusBackground', config.header.background)
socket.emit('allowreplay', config.options.allowreplay)
conn.shell({
term: config.ssh.term,
cols: termCols,
rows: termRows
}, function (err, stream) {
if (err) {
socketutil.SSHerror('EXEC ERROR' + err)
conn.end()
return
}
socket.on('data', function (data) {
stream.write(data)
// poc to log commands from client
// if (data === '\r') {
// console.log(LogPrefix + ': ' + dataBuffer)
// dataBuffer = ''
// } else {
// dataBuffer = dataBuffer + data
// }
})
socket.on('control', function (controlData) {
switch (controlData) {
case 'replayCredentials':
stream.write(socket.request.session.userpassword + '\n')
/* falls through */
default:
console.log('controlData: ' + controlData)
}
})
socket.on('disconnecting', function (reason) { debugWebSSH2('SOCKET DISCONNECTING: ' + reason) })
socket.on('disconnect', function (reason) {
debugWebSSH2('SOCKET DISCONNECT: ' + reason)
err = { message: reason }
socketutil.SSHerror('CLIENT SOCKET DISCONNECT', err)
conn.end()
})
socket.on('error', function (error) { debugWebSSH2('SOCKET ERROR: ' + JSON.stringify(error)) })
stream.on('data', function (d) { socket.emit('data', d.toString('binary')) })
stream.on('close', function (code, signal) {
err = { message: ((code || signal) ? (((code) ? 'CODE: ' + code : '') + ((code && signal) ? ' ' : '') + ((signal) ? 'SIGNAL: ' + signal : '')) : undefined) }
socketutil.SSHerror('STREAM CLOSE', err)
conn.end()
})
stream.stderr.on('data', function (data) {
console.log('STDERR: ' + data)
})
})
})
conn.on('end', function (err) { socketutil.SSHerror('CONN END BY HOST', err) })
conn.on('close', function (err) { socketutil.SSHerror('CONN CLOSE', err) })
conn.on('error', function (err) { socketutil.SSHerror('CONN ERROR', err) })
conn.on('keyboard-interactive', function (name, instructions, instructionsLang, prompts, finish) {
debugWebSSH2('Connection :: keyboard-interactive')
finish([socket.request.session.userpassword])
})
if (socket.request.session.username && socket.request.session.userpassword) {
conn.connect({
host: config.ssh.host,
port: config.ssh.port,
username: socket.request.session.username,
password: socket.request.session.userpassword,
tryKeyboard: true,
// some cisco routers need the these cipher strings
algorithms: {
'cipher': ['aes128-cbc', '3des-cbc', 'aes256-cbc', 'aes128-ctr', 'aes192-ctr', 'aes256-ctr'],
'hmac': ['hmac-sha1', 'hmac-sha1-96', 'hmac-md5-96']
},
debug: debug('ssh2')
})
server.on('error', function (err) {
if (err.code === 'EADDRINUSE') {
config.listen.port++
console.warn('Address in use, retrying on port ' + config.listen.port)
setTimeout(function () {
server.listen(config.listen.port)
}, 250)
} else {
console.warn('Attempt to connect without session.username/password defined, potentially previously abandoned client session. disconnecting websocket client.\r\nHandshake information: \r\n ' + JSON.stringify(socket.handshake))
socket.emit('statusBackground', 'red')
socket.emit('status', 'WEBSOCKET ERROR - Reload and try again')
socket.disconnect(true)
console.log('server.listen ERROR: ' + err.code)
}
})

View file

@ -39,10 +39,12 @@
},
"scripts": {
"start": "node index",
"test": "snyk test"
"test": "snyk test",
"watch": "nodemon index.js"
},
"devDependencies": {
"bithound": "^1.7.0",
"nodemon": "^1.11.0",
"snyk": "^1.30.1"
}
}

View file

@ -1,12 +1,143 @@
var debug = require('debug')('WebSSH2')
var myError
// private
var debug = require('debug')
var debugWebSSH2 = require('debug')('WebSSH2')
var SSH = require('ssh2').Client
var termCols, termRows
module.exports = function (socket, io) {
this.SSHerror = function (myFunc, err) {
myError = (myError) || ((err) ? err.message : undefined)
var thisError = (myError) ? ': ' + myError : ''
debug('SSH ' + myFunc + thisError)
socket.emit('ssherror', 'SSH ' + myFunc + thisError)
// public
module.exports = function (socket) {
function SSHerror (myFunc, err) {
socket.request.session.error = (socket.request.session.error) || ((err) ? err.message : undefined)
var 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)
}
switch (myFunc) {
case 'STREAM CLOSE':
debugWebSSH2('SSH ' + myFunc + theError.red)
socket.emit('ssherror', 'SSH ' + myFunc + theError)
socket.disconnect(true)
break
default:
debugWebSSH2('SSHerror: default'.red)
debugWebSSH2('SSH ' + myFunc + theError)
socket.emit('ssherror', 'SSH ' + myFunc + theError)
socket.disconnect(true)
}
}
// if websocket connection arrives without an express session, kill it
if (!socket.request.session) {
socket.emit('401 UNAUTHORIZED')
debugWebSSH2('SOCKET: No Express Session / REJECTED')
socket.disconnect(true)
return
}
var conn = new SSH()
socket.on('geometry', function (cols, rows) {
termCols = cols
termRows = rows
})
console.log('webssh2 ' + 'IO ON:'.cyan.bold + ' 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)
conn.on('banner', function (d) {
// need to convert to cr/lf for proper formatting
d = d.replace(/\r?\n/g, '\r\n')
socket.emit('data', d.toString('binary'))
})
conn.on('ready', function () {
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 + ' allowreplay=' + socket.request.session.ssh.allowreplay + ' term=' + socket.request.session.ssh.term)
socket.emit('title', 'ssh://' + socket.request.session.ssh.host)
socket.emit('headerBackground', socket.request.session.ssh.header.background)
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('status', 'SSH CONNECTION ESTABLISHED')
socket.emit('statusBackground', socket.request.session.ssh.header.background)
socket.emit('allowreplay', socket.request.session.ssh.allowreplay)
conn.shell({
term: socket.request.session.ssh.term,
cols: termCols,
rows: termRows
}, function (err, stream) {
if (err) {
SSHerror('EXEC ERROR' + err)
conn.end()
return
}
// poc to log commands from client
// var dataBuffer
socket.on('data', function (data) {
stream.write(data)
// poc to log commands from client
// if (data === '\r') {
// console.log(socket.request.session.id + '/' + socket.id + ' command: ' + socket.request.session.ssh.host + ': ' + dataBuffer)
// dataBuffer = undefined
// } else {
// dataBuffer = (dataBuffer) ? dataBuffer + data : data
// }
})
socket.on('control', function (controlData) {
switch (controlData) {
case 'replayCredentials':
stream.write(socket.request.session.userpassword + '\n')
/* falls through */
default:
console.log('controlData: ' + controlData)
}
})
socket.on('disconnecting', function (reason) { debugWebSSH2('SOCKET DISCONNECTING: ' + reason) })
socket.on('disconnect', function (reason) {
debugWebSSH2('SOCKET DISCONNECT: ' + reason)
err = { message: reason }
SSHerror('CLIENT SOCKET DISCONNECT', err)
conn.end()
})
socket.on('error', function (error) { debugWebSSH2('SOCKET ERROR: ' + JSON.stringify(error)) })
stream.on('data', function (d) { socket.emit('data', d.toString('binary')) })
stream.on('close', function (code, signal) {
err = { message: ((code || signal) ? (((code) ? 'CODE: ' + code : '') + ((code && signal) ? ' ' : '') + ((signal) ? 'SIGNAL: ' + signal : '')) : undefined) }
SSHerror('STREAM CLOSE', err)
conn.end()
})
stream.stderr.on('data', function (data) {
console.log('STDERR: ' + data)
})
})
})
conn.on('end', function (err) { SSHerror('CONN END BY HOST', err) })
conn.on('close', function (err) { SSHerror('CONN CLOSE', err) })
conn.on('error', function (err) { SSHerror('CONN ERROR', err) })
conn.on('keyboard-interactive', function (name, instructions, instructionsLang, prompts, finish) {
debugWebSSH2('conn.on(\'keyboard-interactive\')')
finish([socket.request.session.userpassword])
})
if (socket.request.session.username && socket.request.session.userpassword) {
conn.connect({
host: socket.request.session.ssh.host,
port: socket.request.session.ssh.port,
username: socket.request.session.username,
password: socket.request.session.userpassword,
tryKeyboard: true,
// some cisco routers need the these cipher strings
algorithms: socket.request.session.ssh.algorithms,
debug: debug('ssh2')
})
} else {
console.warn('Attempt to connect without session.username/password defined, potentially previously abandoned client session. disconnecting websocket client.\r\nHandshake information: \r\n ' + JSON.stringify(socket.handshake))
socket.emit('statusBackground', 'red')
socket.emit('status', 'WEBSOCKET ERROR - Reload and try again')
socket.disconnect(true)
}
}

View file

@ -1,10 +1,13 @@
// private
require('colors') // allow for color property extensions in log messages
var debug = require('debug')('WebSSH2')
var colors = require('colors')
var Auth = require('basic-auth')
var util = require('util')
console.warn = makeColorConsole(console.warn, 'yellow')
console.error = makeColorConsole(console.error, 'red')
// public
function makeColorConsole (fct, color) {
return function () {
for (var i in arguments) {
@ -19,7 +22,7 @@ exports.basicAuth = function (req, res, next) {
if (myAuth) {
req.session.username = myAuth.name
req.session.userpassword = myAuth.pass
debug('myAuth.name: ' + myAuth.name + ' and password ' + ((myAuth.pass) ? 'exists' : 'is blank'.underline.red))
debug('myAuth.name: ' + myAuth.name.yellow.bold.underline + ' and password ' + ((myAuth.pass) ? 'exists'.yellow.bold.underline : 'is blank'.underline.red.bold))
next()
} else {
res.statusCode = 401
@ -28,3 +31,10 @@ exports.basicAuth = function (req, res, next) {
res.end('Username and password required for web SSH service.')
}
}
// expects headers to be a JSON object, will replace authroization header with 'Sanatized//Exists'
// we don't want to log basic auth header since it contains a password...
exports.SanatizeHeaders = function (headers) {
if (headers.authorization) { headers.authorization = 'Sanitized//Exists' }
return (headers)
}