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:
parent
938e2fbfa4
commit
2e912dd9cc
6 changed files with 217 additions and 170 deletions
|
@ -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.
|
||||
|
|
19
config.json
19
config.json
|
@ -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
194
index.js
|
@ -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)
|
||||
}
|
||||
})
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
147
socket/index.js
147
socket/index.js
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue