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...
`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`
* Port node should listen on for client connections
* Port node should listen on for client connections.
`user.name` default `null`
* Specify user name to authenticate with
* Specify user name to authenticate with.
`user.password` default `null`
* Specify password to authenticate with
* Specify password to authenticate with.
`ssh.host` default `null`
* Specify host to connect to
* Specify host to connect to.
`ssh.port` default `22`
* Specify SSH port to connect to
* Specify SSH port to connect to.
`ssh.term` default `xterm-color`
* Specify terminal emulation to use
* Specify terminal emulation to use.
`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, 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`
* 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",
"background": "green"
},
"session": {
"name": "WebSSH2id",
"secret": "mysecret"
},
"options": {
"challengeButton": true
}

220
index.js
View file

@ -1,73 +1,96 @@
/*
* 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'),
app = express(),
cookieParser = require('cookie-parser'),
server = require('http').Server(app),
io = require('socket.io')(server),
path = require('path'),
basicAuth = require('basic-auth'),
SSH = require('ssh2').Client,
readConfig = require('read-config'),
config = readConfig(path.join(__dirname, 'config.json')),
myError = ' - ',
termCols,
termRows
var express = require('express')
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 dataBuffer = ''
var util = require('./util')
var session = require('express-session')({
secret: config.session.secret,
name: config.session.name,
resave: true,
saveUninitialized: false
})
var LogPrefix, termCols, termRows, myError
// function logErrors (err, req, res, next) {
// console.error(err.stack)
// next(err)
// }
var expressOptions = {
dotfiles: 'ignore',
etag: false,
extensions: ['htm', 'html'],
index: false,
maxAge: '1s',
redirect: false,
setHeaders: function (res, path, stat) {
res.set('x-timestamp', Date.now())
}
}
server.listen({
host: config.listen.ip,
port: config.listen.port
}).on('error', function (err) {
})
server.on('error', function (err) {
if (err.code === 'EADDRINUSE') {
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 () {
server.listen(config.listen.port)
}, 250)
} else {
console.log('server.listen ERROR: ' + err.code)
}
})
app.use(express.static(path.join(__dirname, 'public'))).use(function (req, res, next) {
var myAuth = basicAuth(req)
if (myAuth === undefined) {
res.statusCode = 401
res.setHeader('WWW-Authenticate', 'Basic realm="WebSSH"')
res.end('Username and password required for web SSH service.')
} else if (myAuth.name === '') {
res.statusCode = 401
res.setHeader('WWW-Authenticate', 'Basic realm="WebSSH"')
res.end('Username and password required for web SSH service.')
app.use(session)
app.use(util.basicAuth)
io.use(function (socket, next) {
if (socket.request.res) {
session(socket.request, socket.request.res, next)
} else {
config.user.name = myAuth.name
config.user.password = myAuth.pass
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')))
config.ssh.host = req.params.host
if (typeof req.query.port !== 'undefined' && req.query.port !== null) {
config.ssh.port = req.query.port
}
if (typeof req.query.header !== 'undefined' && req.query.header !== null) {
config.header.text = req.query.header
}
if (typeof req.query.headerBackground !== 'undefined' && req.query.headerBackground !== null) {
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))
// 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)
LogPrefix = req.session.username + '@' + req.ip + ' ssh://' + config.ssh.host + ':' + config.ssh.port + '/' + req.sessionID
// console.log('Headers: ' + JSON.stringify(req.headers))
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) {
// if websocket connection arrives without an express session, kill it
if (!socket.request.session) {
socket.disconnect(true)
return
}
var conn = new SSH()
socket.on('geometry', function (cols, rows) {
termCols = cols
@ -78,15 +101,17 @@ io.on('connection', function (socket) {
// need to convert to cr/lf for proper formatting
d = d.replace(/\r?\n/g, '\r\n')
socket.emit('data', d.toString('binary'))
}).on('ready', function () {
})
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://' + 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('statusBackground', 'green')
socket.emit('statusBackground', config.header.background)
socket.emit('allowreplay', config.options.allowreplay)
conn.shell({
term: config.ssh.term,
cols: termCols,
@ -94,16 +119,26 @@ io.on('connection', function (socket) {
}, function (err, stream) {
if (err) {
console.log(err.message)
myError = myError + err.message
return socket.emit('status', 'SSH EXEC ERROR: ' + err.message).emit('statusBackground', 'red')
myError = err.message
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) {
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(config.user.password + '\n')
stream.write(socket.request.session.userpassword + '\n')
/* falls through */
default:
console.log('controlData: ' + controlData)
@ -112,40 +147,61 @@ io.on('connection', function (socket) {
stream.on('data', function (d) {
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)
conn.end()
socket.disconnect()
}).stderr.on('data', function (data) {
})
stream.stderr.on('data', function (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": {
"basic-auth": "^1.1.0",
"cookie-parser": "^1.4.3",
"express": "^4.14.1",
"colors": "^1.1.2",
"debug": "^2.6.7",
"express": "^4.15.3",
"express-session": "^1.15.3",
"read-config": "^1.6.0",
"socket.io": "^1.6.0",
"ssh2": "^0.5.4",
"strip-ansi": "^3.0.1",
"xterm": "^2.4.0"
"xterm": "^2.6.0"
},
"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,
termid
term.open(terminalContainer)
term.open(terminalContainer, {
focus: true
})
term.fit()
if (document.location.pathname) {
@ -106,12 +108,12 @@ socket.on('connect', function () {
if (sessionLogEnable) {
sessionLog = sessionLog + data
}
}).on('disconnect', function (err) {
document.getElementById('status').style.backgroundColor = 'red'
document.getElementById('status').innerHTML = 'WEBSOCKET SERVER DISCONNECTED' + err
socket.io.reconnection(false)
}).on('error', function (err) {
document.getElementById('status').style.backgroundColor = 'red'
document.getElementById('status').innerHTML = 'ERROR ' + err
})
})// .on('disconnect', function (err) {
// document.getElementById('status').style.backgroundColor = 'red'
// document.getElementById('status').innerHTML = 'WEBSOCKET SERVER DISCONNECTED' + err
// socket.io.reconnection(false)
// })//.on('error', function (err) {
// document.getElementById('status').style.backgroundColor = 'red'
// 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.')
}
}