Refactoring, Session maintenance

- start of some refactoring
- linking socket.io and express sessions
- cleaning up some potential error conditions

todo:
- re-work status updates on client side for unexpected websocket
disconnects while not overwriting ssh server errors un termination
This commit is contained in:
billchurch 2017-05-19 19:49:56 -04:00
parent b466d100ae
commit 7f55613de8
7 changed files with 227 additions and 105 deletions

22
.snyk Normal file
View file

@ -0,0 +1,22 @@
# Snyk (https://snyk.io) policy file, patches or ignores known vulnerabilities.
version: v1.7.1
# ignores vulnerabilities until expiry date; change duration by modifying expiry date
ignore:
'npm:ms:20170412':
- socket.io > socket.io-adapter > socket.io-parser > debug > ms:
reason: no patch avail
expires: '2017-06-18T14:21:30.266Z'
- standard > eslint-plugin-import > eslint-module-utils > debug > ms:
reason: no patch avail
expires: '2017-06-18T14:21:30.266Z'
- socket.io > socket.io-adapter > debug > ms:
reason: no patch avail
expires: '2017-06-18T14:21:30.267Z'
- socket.io > socket.io-client > debug > ms:
reason: no patch avail
expires: '2017-06-18T14:21:30.267Z'
'npm:shelljs:20140723':
- standard > eslint > shelljs:
reason: no patch avail
expires: '2017-06-18T14:21:30.267Z'
patch: {}

View file

@ -33,31 +33,37 @@ headerBackground= - optional background color of header to display on page
config.json contains several options which may be specified to customize to your needs, vs editing the javascript direclty. This is JSON format so mind your spacing, brackets, etc... config.json contains several options which may be specified to customize to your needs, vs editing the javascript direclty. This is JSON format so mind your spacing, brackets, etc...
`listen.ip` default `127.0.0.1` `listen.ip` default `127.0.0.1`
* IP address node should listen on for client connections * IP address node should listen on for client connections.
`listen.port` default `2222` `listen.port` default `2222`
* Port node should listen on for client connections * Port node should listen on for client connections.
`user.name` default `null` `user.name` default `null`
* Specify user name to authenticate with * Specify user name to authenticate with.
`user.password` default `null` `user.password` default `null`
* Specify password to authenticate with * Specify password to authenticate with.
`ssh.host` default `null` `ssh.host` default `null`
* Specify host to connect to * Specify host to connect to.
`ssh.port` default `22` `ssh.port` default `22`
* Specify SSH port to connect to * Specify SSH port to connect to.
`ssh.term` default `xterm-color` `ssh.term` default `xterm-color`
* Specify terminal emulation to use * Specify terminal emulation to use.
`header.text` `header.text`
* Specify header text, defaults to `My Header` but may also be set to `null` * Specify header text, defaults to `My Header` but may also be set to `null`.
`header.background` `header.background`
* Header background, defaults to `green` * Header background, defaults to `green`.
`session.name`
* Name of session ID cookie. it's not a horrible idea to make this something unique.
`session.secret`
* Secret key for cookie encryption. You should change this in production.
`options.challengeButton` `options.challengeButton`
* Challenge button. This option, which is still under development, allows the user to resend the password to the server (in cases of step-up authentication for things like `sudo` or a router `enable` command. * Challenge button. This option, which is still under development, allows the user to resend the password to the server (in cases of step-up authentication for things like `sudo` or a router `enable` command.

View file

@ -16,6 +16,10 @@
"text": "My Header", "text": "My Header",
"background": "green" "background": "green"
}, },
"session": {
"name": "WebSSH2id",
"secret": "mysecret"
},
"options": { "options": {
"challengeButton": true "challengeButton": true
} }

218
index.js
View file

@ -1,73 +1,96 @@
/* /*
* WebSSH2 - Web to SSH2 gateway * WebSSH2 - Web to SSH2 gateway
* Bill Church - https://github.com/billchurch - April 2016 * Bill Church - https://github.com/billchurch/WebSSH2 - May 2017
* *
*/ */
var express = require('express'), var express = require('express')
app = express(), var app = express()
cookieParser = require('cookie-parser'), var server = require('http').Server(app)
server = require('http').Server(app), var io = require('socket.io')(server)
io = require('socket.io')(server), var path = require('path')
path = require('path'), var SSH = require('ssh2').Client
basicAuth = require('basic-auth'), var config = require('read-config')(path.join(__dirname, 'config.json'))
SSH = require('ssh2').Client, var debug = require('debug')
readConfig = require('read-config'), var dataBuffer = ''
config = readConfig(path.join(__dirname, 'config.json')), var util = require('./util')
myError = ' - ', var session = require('express-session')({
termCols, secret: config.session.secret,
termRows name: config.session.name,
resave: true,
saveUninitialized: false
})
var LogPrefix, termCols, termRows, myError
// function logErrors (err, req, res, next) { var expressOptions = {
// console.error(err.stack) dotfiles: 'ignore',
// next(err) etag: false,
// } extensions: ['htm', 'html'],
index: false,
maxAge: '1s',
redirect: false,
setHeaders: function (res, path, stat) {
res.set('x-timestamp', Date.now())
}
}
server.listen({ server.listen({
host: config.listen.ip, host: config.listen.ip,
port: config.listen.port port: config.listen.port
}).on('error', function (err) { })
server.on('error', function (err) {
if (err.code === 'EADDRINUSE') { if (err.code === 'EADDRINUSE') {
config.listen.port++ config.listen.port++
console.log('Address in use, retrying on port ' + config.listen.port) console.warn('Address in use, retrying on port ' + config.listen.port)
setTimeout(function () { setTimeout(function () {
server.listen(config.listen.port) server.listen(config.listen.port)
}, 250) }, 250)
} else {
console.log('server.listen ERROR: ' + err.code)
} }
}) })
app.use(express.static(path.join(__dirname, 'public'))).use(function (req, res, next) { app.use(session)
var myAuth = basicAuth(req) app.use(util.basicAuth)
if (myAuth === undefined) {
res.statusCode = 401 io.use(function (socket, next) {
res.setHeader('WWW-Authenticate', 'Basic realm="WebSSH"') if (socket.request.res) {
res.end('Username and password required for web SSH service.') session(socket.request, socket.request.res, next)
} else if (myAuth.name === '') {
res.statusCode = 401
res.setHeader('WWW-Authenticate', 'Basic realm="WebSSH"')
res.end('Username and password required for web SSH service.')
} else { } else {
config.user.name = myAuth.name
config.user.password = myAuth.pass
next() next()
} }
}).use(cookieParser()).get('/ssh/host/:host?', function (req, res) { })
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'))) res.sendFile(path.join(path.join(__dirname, 'public', 'client.htm')))
config.ssh.host = req.params.host // capture url variables if defined
if (typeof req.query.port !== 'undefined' && req.query.port !== null) { config.ssh.host = req.params.host || config.ssh.host
config.ssh.port = req.query.port config.ssh.port = req.query.port || config.ssh.port
} config.header.text = req.query.header || config.header.text
if (typeof req.query.header !== 'undefined' && req.query.header !== null) { config.header.background = req.query.headerBackground || config.header.background
config.header.text = req.query.header 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)
} LogPrefix = req.session.username + '@' + req.ip + ' ssh://' + config.ssh.host + ':' + config.ssh.port + '/' + req.sessionID
if (typeof req.query.headerBackground !== 'undefined' && req.query.headerBackground !== null) { // console.log('Headers: ' + JSON.stringify(req.headers))
config.header.background = req.query.headerBackground
}
console.log('webssh2 Login: user=' + config.user.name + ' from=' + req.ip + ' host=' + config.ssh.host + ' port=' + config.ssh.port + ' sessionID=' + req.headers.sessionid + ' allowreplay=' + req.headers.allowreplay)
console.log('Headers: ' + JSON.stringify(req.headers))
config.options.allowreplay = req.headers.allowreplay config.options.allowreplay = req.headers.allowreplay
}).use('/style', express.static(path.join(__dirname, 'public'))).use('/src', express.static(path.join(__dirname, 'node_modules', 'xterm', 'dist'))).use('/addons', express.static(path.join(__dirname, 'node_modules', 'xterm', 'dist', 'addons'))) })
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')))
io.on('connection', function (socket) { io.on('connection', function (socket) {
// if websocket connection arrives without an express session, kill it
if (!socket.request.session) {
socket.disconnect(true)
return
}
var conn = new SSH() var conn = new SSH()
socket.on('geometry', function (cols, rows) { socket.on('geometry', function (cols, rows) {
termCols = cols termCols = cols
@ -78,13 +101,15 @@ io.on('connection', function (socket) {
// need to convert to cr/lf for proper formatting // need to convert to cr/lf for proper formatting
d = d.replace(/\r?\n/g, '\r\n') d = d.replace(/\r?\n/g, '\r\n')
socket.emit('data', d.toString('binary')) socket.emit('data', d.toString('binary'))
}).on('ready', function () { })
conn.on('ready', function () {
socket.emit('title', 'ssh://' + config.ssh.host) socket.emit('title', 'ssh://' + config.ssh.host)
socket.emit('headerBackground', config.header.background) socket.emit('headerBackground', config.header.background)
socket.emit('header', config.header.text) socket.emit('header', config.header.text)
socket.emit('footer', 'ssh://' + config.user.name + '@' + config.ssh.host + ':' + config.ssh.port) socket.emit('footer', 'ssh://' + socket.request.session.username + '@' + config.ssh.host + ':' + config.ssh.port)
socket.emit('status', 'SSH CONNECTION ESTABLISHED') socket.emit('status', 'SSH CONNECTION ESTABLISHED')
socket.emit('statusBackground', 'green') socket.emit('statusBackground', config.header.background)
socket.emit('allowreplay', config.options.allowreplay) socket.emit('allowreplay', config.options.allowreplay)
conn.shell({ conn.shell({
@ -94,16 +119,26 @@ io.on('connection', function (socket) {
}, function (err, stream) { }, function (err, stream) {
if (err) { if (err) {
console.log(err.message) console.log(err.message)
myError = myError + err.message myError = err.message
return socket.emit('status', 'SSH EXEC ERROR: ' + err.message).emit('statusBackground', 'red') socket.emit('status', 'SSH EXEC ERROR: ' + err.message)
socket.emit('statusBackground', 'red')
console.log('conn.shell err: ' + err.message)
return socket.close(true)
} }
socket.on('data', function (data) { socket.on('data', function (data) {
stream.write(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) { socket.on('control', function (controlData) {
switch (controlData) { switch (controlData) {
case 'replayCredentials': case 'replayCredentials':
stream.write(config.user.password + '\n') stream.write(socket.request.session.userpassword + '\n')
/* falls through */ /* falls through */
default: default:
console.log('controlData: ' + controlData) console.log('controlData: ' + controlData)
@ -112,40 +147,61 @@ io.on('connection', function (socket) {
stream.on('data', function (d) { stream.on('data', function (d) {
socket.emit('data', d.toString('binary')) socket.emit('data', d.toString('binary'))
}).on('close', function (code, signal) { })
stream.on('close', function (code, signal) {
console.log('Stream :: close :: code: ' + code + ', signal: ' + signal) console.log('Stream :: close :: code: ' + code + ', signal: ' + signal)
conn.end() conn.end()
socket.disconnect() socket.disconnect()
}).stderr.on('data', function (data) { })
stream.stderr.on('data', function (data) {
console.log('STDERR: ' + data) console.log('STDERR: ' + data)
}) })
}) })
}).on('end', function () {
socket.emit('status', 'SSH CONNECTION CLOSED BY HOST' + myError)
socket.emit('statusBackground', 'red')
socket.disconnect()
}).on('close', function () {
socket.emit('status', 'SSH CONNECTION CLOSE' + myError)
socket.emit('statusBackground', 'red')
socket.disconnect()
}).on('error', function (err) {
myError = myError + err
socket.emit('status', 'SSH CONNECTION ERROR' + myError)
socket.emit('statusBackground', 'red')
console.log('on.error' + myError)
}).on('keyboard-interactive', function (name, instructions, instructionsLang, prompts, finish) {
console.log('Connection :: keyboard-interactive')
finish([config.user.password])
}).connect({
host: config.ssh.host,
port: config.ssh.port,
username: config.user.name,
password: config.user.password,
tryKeyboard: true,
// some cisco routers need the these cipher strings
algorithms: {
'cipher': ['aes128-cbc', '3des-cbc', 'aes256-cbc'],
'hmac': ['hmac-sha1', 'hmac-sha1-96', 'hmac-md5-96']
}
}) })
conn.on('end', function () {
socket.emit('status', 'SSH CONNECTION CLOSED BY HOST ' + myError)
socket.emit('statusBackground', 'red')
socket.disconnect()
})
conn.on('close', function () {
socket.emit('status', 'SSH CONNECTION CLOSE ' + myError)
socket.emit('statusBackground', 'red')
socket.disconnect()
})
conn.on('error', function (err) {
myError = err
socket.emit('status', 'SSH CONNECTION ERROR ' + myError)
socket.emit('statusBackground', 'red')
console.error('conn.on(\'error\'): ' + myError)
})
conn.on('keyboard-interactive', function (name, instructions, instructionsLang, prompts, finish) {
console.log('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('WebSSH2:debug')
})
} 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

@ -28,15 +28,20 @@
}, },
"dependencies": { "dependencies": {
"basic-auth": "^1.1.0", "basic-auth": "^1.1.0",
"cookie-parser": "^1.4.3", "colors": "^1.1.2",
"express": "^4.14.1", "debug": "^2.6.7",
"express": "^4.15.3",
"express-session": "^1.15.3",
"read-config": "^1.6.0", "read-config": "^1.6.0",
"socket.io": "^1.6.0", "socket.io": "^1.6.0",
"ssh2": "^0.5.4", "ssh2": "^0.5.4",
"strip-ansi": "^3.0.1", "xterm": "^2.6.0"
"xterm": "^2.4.0"
}, },
"scripts": { "scripts": {
"start": "node index" "start": "node index",
"test": "snyk test"
},
"devDependencies": {
"snyk": "^1.30.1"
} }
} }

View file

@ -62,7 +62,9 @@ var terminalContainer = document.getElementById('terminal-container'),
}), }),
socket, socket,
termid termid
term.open(terminalContainer) term.open(terminalContainer, {
focus: true
})
term.fit() term.fit()
if (document.location.pathname) { if (document.location.pathname) {
@ -106,12 +108,12 @@ socket.on('connect', function () {
if (sessionLogEnable) { if (sessionLogEnable) {
sessionLog = sessionLog + data sessionLog = sessionLog + data
} }
}).on('disconnect', function (err) { })// .on('disconnect', function (err) {
document.getElementById('status').style.backgroundColor = 'red' // document.getElementById('status').style.backgroundColor = 'red'
document.getElementById('status').innerHTML = 'WEBSOCKET SERVER DISCONNECTED' + err // document.getElementById('status').innerHTML = 'WEBSOCKET SERVER DISCONNECTED' + err
socket.io.reconnection(false) // socket.io.reconnection(false)
}).on('error', function (err) { // })//.on('error', function (err) {
document.getElementById('status').style.backgroundColor = 'red' // document.getElementById('status').style.backgroundColor = 'red'
document.getElementById('status').innerHTML = 'ERROR ' + err // document.getElementById('status').innerHTML = 'ERROR ' + err
}) // })
}) })

27
util/index.js Normal file
View file

@ -0,0 +1,27 @@
var colors = require('colors');
var Auth = require('basic-auth')
console.warn = makeColorConsole(console.warn, 'yellow')
console.error = makeColorConsole(console.error, 'red')
function makeColorConsole(fct, color){
return function(){
for (var i in arguments)
if (arguments[i] instanceof Object)
arguments[i] = sys.inspect(arguments[i]);
fct(Array.prototype.join.call(arguments," ")[color]);
};
}
exports.basicAuth = function (req, res, next) {
var myAuth = Auth(req)
if (myAuth) {
req.session.username = myAuth.name
req.session.userpassword = myAuth.pass
next()
} else {
res.statusCode = 401
res.setHeader('WWW-Authenticate', 'Basic realm="WebSSH"')
res.end('Username and password required for web SSH service.')
}
}