refactoring, socket.io update

more refactoring, code standards, updated socket.io to 1.7.4, increment
release to 0.1.1
This commit is contained in:
billchurch 2017-06-03 16:48:45 -04:00
parent 9dace2501c
commit 7cbfed20e9
21 changed files with 286 additions and 211 deletions

3
.gitignore vendored
View file

@ -3,6 +3,9 @@ logs
*.log
npm-debug.log*
# Editor preference files
*.sublime-*
# Runtime data
pids
*.pid

View file

@ -1,4 +1,18 @@
# Change Log
## [0.1.1] 2017-06-03
### Added
- `serverlog.client` and `serverlog.server` options added to `config.json` to enable logging of client commands to server log (only client portion implemented at this time)
- morgan express middleware for logging
### Changed
- Updated socket.io to 1.7.4
- continued refactoring, breaking up `index.js`
- revised error handling methods
- revised session termination methods
### Fixed
### Removed
- color console decorations from `util/index.js`
- SanatizeHeaders function from `util/index.js`
## [0.1.0] 2017-05-27
### Added
- This ChangeLog.md file
@ -6,7 +20,7 @@
- Snyk, Bithound, Travis CI
- Cross platform improvements (path mappings)
- Session fixup between Express and Socket.io
- Session secret settings in config.json
- 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
and events in the application (not including the ssh2 module debug)
@ -19,7 +33,7 @@ and events in the application (not including the ssh2 module debug)
- error 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)
- moved SSH algorithms to `config.json` and defined as a session variable (..session.ssh.algorithms)
-- prep for future feature to define algorithms in header or some other method to enable separate ciphers per host
- minified and combined all js files to a single js in `./public/webssh2.min.js` also included a sourcemap `./public/webssh2.min.js` which maps to `./public/webssh2.js` for easier troubleshooting.
- combined all css files to a single css in `./public/webssh2.css`
@ -28,7 +42,7 @@ and events in the application (not including the ssh2 module debug)
- sourcemaps of all minified code (in /public/src and /public/src/js)
- renamed `client.htm` to `client-full.htm`
- created `client-min.htm` to serve minified javascript
- if header.text is null in config.json and header is not defined as a get parameter the Header will not be displayed. Both of these must be null / undefined and not specified as get parameters.
- if header.text is null in `config.json` and header is not defined as a get parameter the Header will not be displayed. Both of these must be null / undefined and not specified as get parameters.
### Fixed
- Multiple errors may overwrite 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

@ -1,7 +1,7 @@
# WebSSH2 [![GitHub version](https://badge.fury.io/gh/billchurch%2FWebSSH2.svg)](https://badge.fury.io/gh/billchurch%2FWebSSH2) [![Build Status](https://travis-ci.org/billchurch/WebSSH2.svg?branch=master)](https://travis-ci.org/billchurch/WebSSH2) [![Known Vulnerabilities](https://snyk.io/test/github/billchurch/webssh2/badge.svg)](https://snyk.io/test/github/billchurch/webssh2) [![bitHound Overall Score](https://www.bithound.io/github/billchurch/WebSSH2/badges/score.svg)](https://www.bithound.io/github/billchurch/WebSSH2) [![bitHound Dependencies](https://www.bithound.io/github/billchurch/WebSSH2/badges/dependencies.svg)](https://www.bithound.io/github/billchurch/WebSSH2/master/dependencies/npm) [![NSP Status](https://nodesecurity.io/orgs/billchurch/projects/b0a0d9df-1340-43ef-9736-ef983c057764/badge)](https://nodesecurity.io/orgs/billchurch/projects/b0a0d9df-1340-43ef-9736-ef983c057764)
Web SSH Client using ssh2, socket.io, xterm.js, and express
Bare bones example of using SSH2 as a client on a host to proxy a Websocket / Socket.io connection to a SSH2 server.
Bare bones example of using SSH2 as a client on a host to proxy a Websocket / Socket.io connection to a SSH2 server.
<img width="1044" alt="Screenshot 2017-03-23 18.13.59" src="https://cloud.githubusercontent.com/assets/1668075/24272639/8ad4fef0-0ff4-11e7-8dd0-72b26605e467.png">
@ -29,7 +29,7 @@ You will be prompted for credentials to use on the SSH server via HTTP Basic aut
* **header=** - _string_ - optional header to display on page
* **headerBackground=** - _string_ - optional background color of header to display on page
* **headerBackground=** - _string_ - optional background color of header to display on page
## Headers
@ -62,7 +62,7 @@ You will be prompted for credentials to use on the SSH server via HTTP Basic aut
* **session.secret** - _string_ - Secret key for cookie encryption. You should change this in production.
* **options.challengeButton** - _boolean_ - 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.
* **options.challengeButton** - _boolean_ - 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.
* **algorithms** - _object_ - This option allows you to explicitly override the default transport layer algorithms used for the connection. Each value must be an array of valid algorithms for that category. The order of the algorithms in the arrays are important, with the most favorable being first. Valid keys:
@ -152,6 +152,11 @@ You will be prompted for credentials to use on the SSH server via HTTP Basic aut
* zlib@openssh.com
* zlib
* **serverlog.client** - _boolean_ - Enables client command logging on server log (console.log). Very simple at this point, buffers data from client until it receives a line-feed then dumps buffer to console.log with session information for tracking. Will capture anything send from client, including passwords, so use for testing only... Default: false. Example:
* _serverlog.client: GcZDThwA4UahDiKO2gkMYd7YPIfVAEFW/mnf0NUugLMFRHhsWAAAA host: 192.168.99.80 command: ls -lat_
* **serverlog.server** - _boolean_ - not implemented, default: false.
# Experimental client-side logging
Clicking `Start logging` on the status bar will log all data to the client. A `Download log` option will appear after starting the logging. You may download at any time to the client. You may stop logging at any time my pressing the `Logging - STOP LOG`. Note that clicking the `Start logging` option again will cause the current log to be overwritten, so be sure to download first.
@ -162,4 +167,4 @@ http://localhost:2222/ssh/host/192.168.1.1?port=2244&header=My%20Header&color=re
# Tips
* If you want to add custom JavaScript to the browser client you can either modify `./public/client-(full|min).html` and add a **<script>** element or check out `Gulpfile.js` and add your custom javascript file to the concat task
* BIG-IP Acess Policy Manager (APM) doesn't always care for minified javascript when run in portal mode. Be sure to Set `useminified` option in `config.json` to `false` for these environments
* Set `useminified` option in `config.json` to `true` to utilize minified javascript
* Set `useminified` option in `config.json` to `true` to utilize minified javascript

80
app.js Normal file
View file

@ -0,0 +1,80 @@
// app.js
var path = require('path')
var config = require('read-config')(path.join(__dirname, 'config.json'))
var express = require('express')
var logger = require('morgan')
var session = require('express-session')({
secret: config.session.secret,
name: config.session.name,
resave: true,
saveUninitialized: false,
unset: 'destroy'
})
var app = express()
var server = require('http').Server(app)
var myutil = require('./util')
var validator = require('validator')
var io = require('socket.io')(server, { serveClient: false })
var socket = require('./socket')
var expressOptions = require('./expressOptions')
// express
app.use(session)
app.use(myutil.basicAuth)
if (config.accesslog) app.use(logger('common'))
app.disable('x-powered-by')
// static files
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', (config.useminified)
? 'client-min.htm' : 'client-full.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,
header: {
name: req.query.header || config.header.text,
background: req.query.headerBackground || config.header.background
},
algorithms: config.algorithms,
term: (/^(([a-z]|[A-Z]|[0-9]|[!^(){}\-_~])+)?\w$/.test(req.query.sshterm) &&
req.query.sshterm) || config.ssh.term,
allowreplay: validator.isBoolean(req.headers.allowreplay + '') || false,
serverlog: {
client: config.serverlog.client || false,
server: config.serverlog.server || false
}
}
req.session.ssh.header.name && validator.escape(req.session.ssh.header.name)
req.session.ssh.header.background &&
validator.escape(req.session.ssh.header.background)
})
// express error handling
app.use(function (req, res, next) {
res.status(404).send("Sorry can't find that!")
})
app.use(function (err, req, res, next) {
console.error(err.stack)
res.status(500).send('Something broke!')
})
// socket.io
// expose express session with socket.request.session
io.use(function (socket, next) {
(socket.request.res) ? session(socket.request, socket.request.res, next)
: next()
})
// bring up socket
io.on('connection', socket)
module.exports = {server: server, config: config}

View file

@ -52,5 +52,10 @@
"zlib@openssh.com",
"zlib"
]
}
}
},
"serverlog": {
"client": false, // proof-of-concept to log commands from client to server
"server": false // not yet implemented
},
"accesslog": false // http style access logging to console.log
}

11
expressOptions.js Normal file
View file

@ -0,0 +1,11 @@
module.exports = {
dotfiles: 'ignore',
etag: false,
extensions: ['htm', 'html'],
index: false,
maxAge: '1s',
redirect: false,
setHeaders: function (res, path, stat) {
res.set('x-timestamp', Date.now())
}
}

View file

@ -1,97 +1,24 @@
// server.js
/*
* WebSSH2 - Web to SSH2 gateway
* Bill Church - https://github.com/billchurch/WebSSH2 - May 2017
*
*/
var express = require('express')
var app = express()
var server = require('http').Server(app)
var io = require('socket.io')(server)
var path = require('path')
var config = require('read-config')(path.join(__dirname, 'config.json'))
var myutil = require('./util')
var socket = require('./socket/index.js')
var validator = require('validator')
var session = require('express-session')({
secret: config.session.secret,
name: config.session.name,
resave: true,
saveUninitialized: false,
unset: 'destroy'
})
// express
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())
}
}
var config = require('./app').config
var server = require('./app').server
app.use(session)
app.use(myutil.basicAuth)
app.disable('x-powered-by')
app.get('/ssh/host/:host?', function (req, res, next) {
res.sendFile(path.join(path.join(__dirname, 'public', (config.useminified) ? 'client-min.htm' : 'client-full.htm')))
// capture and assign 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,
header: {
name: req.query.header || config.header.text,
background: req.query.headerBackground || config.header.background
},
algorithms: config.algorithms,
term: (/^(([a-z]|[A-Z]|[0-9]|[!^(){}\-_~])+)?\w$/.test(req.query.sshterm) && req.query.sshterm) || config.ssh.term,
allowreplay: validator.isBoolean(req.headers.allowreplay + '') || false
}
req.session.ssh.header.name && validator.escape(req.session.ssh.header.name)
req.session.ssh.header.background && validator.escape(req.session.ssh.header.background)
})
// static files
app.use(express.static(path.join(__dirname, 'public'), expressOptions))
// express error handling
app.use(function (req, res, next) {
res.status(404).send("Sorry can't find that!")
})
app.use(function (err, req, res, next) {
console.error(err.stack)
res.status(500).send('Something broke!')
})
// socket.io
// expose express session with socket.request.session
io.use(function (socket, next) {
(socket.request.res) ? session(socket.request, socket.request.res, next) : next()
})
// bring up socket
io.on('connection', socket)
// server
server.listen({
host: config.listen.ip,
port: config.listen.port
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)
console.warn('WebSSH2 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)
console.log('WebSSH2 server.listen ERROR: ' + err.code)
}
})

View file

@ -1,6 +1,6 @@
{
"name": "WebSSH2",
"version": "0.1.0",
"version": "0.1.1",
"ignore": [
".gitignore"
],
@ -32,8 +32,9 @@
"debug": "^2.6.8",
"express": "^4.15.3",
"express-session": "^1.15.3",
"morgan": "^1.8.2",
"read-config": "^1.6.0",
"socket.io": "^1.6.0",
"socket.io": "^1.7.4",
"ssh2": "^0.5.4",
"validator": "^7.0.0"
},

View file

@ -11,9 +11,12 @@
<div id="bottomdiv">
<div id="footer"></div>
<div id="status"></div>
<div id="credentials"><a class="credentials" href="javascript:void(0);" onclick="replayCredentials()">CREDENTIALS</a></div>
<div id="downloadLog"><a class="downloadLog" href="javascript:void(0);" onclick="downloadLog()">Download Log</a></div>
<div id="toggleLog"><a class="toggleLog" href="javascript:void(0);" onclick="toggleLog();">Start Log</a></div>
<div id="credentials"><a class="credentials" href="javascript:void(0);"
onclick="replayCredentials()">CREDENTIALS</a></div>
<div id="downloadLog"><a class="downloadLog" href="javascript:void(0);"
onclick="downloadLog()">Download Log</a></div>
<div id="toggleLog"><a class="toggleLog" href="javascript:void(0);"
onclick="toggleLog();">Start Log</a></div>
</div>
</div>
</body>

View file

@ -11,9 +11,12 @@
<div id="bottomdiv">
<div id="footer"></div>
<div id="status"></div>
<div id="credentials"><a class="credentials" href="javascript:void(0);" onclick="replayCredentials()">CREDENTIALS</a></div>
<div id="downloadLog"><a class="downloadLog" href="javascript:void(0);" onclick="downloadLog()">Download Log</a></div>
<div id="toggleLog"><a class="toggleLog" href="javascript:void(0);" onclick="toggleLog();">Start Log</a></div>
<div id="credentials"><a class="credentials" href="javascript:void(0);"
onclick="replayCredentials()">CREDENTIALS</a></div>
<div id="downloadLog"><a class="downloadLog" href="javascript:void(0);"
onclick="downloadLog()">Download Log</a></div>
<div id="toggleLog"><a class="toggleLog" href="javascript:void(0);"
onclick="toggleLog();">Start Log</a></div>
</div>
</div>
</body>

View file

@ -88,7 +88,7 @@ body {
}
a.credentials {
color: rgb(51, 51, 51);
text-decoration: none;
text-decoration: none;
}
#downloadLog {
display: inline-block;
@ -104,7 +104,7 @@ a.credentials {
}
a.downloadLog {
color: rgb(240, 240, 240);
text-decoration: none;
text-decoration: none;
}
#toggleLog {
display: inline-block;
@ -120,5 +120,5 @@ a.downloadLog {
}
a.toggleLog {
color: rgb(240, 240, 240);
text-decoration: none;
text-decoration: none;
}

View file

@ -14,19 +14,27 @@ function replayCredentials () { // eslint-disable-line
function toggleLog () { // eslint-disable-line
if (sessionLogEnable === true) {
sessionLogEnable = false
document.getElementById('toggleLog').innerHTML = '<a class="toggleLog" href="javascript:void(0);" onclick="toggleLog();">Start Log</a>'
document.getElementById('toggleLog').innerHTML =
'<a class="toggleLog" href="javascript:void(0);" onclick="toggleLog();">Start Log</a>'
console.log('stopping log, ' + sessionLogEnable)
currentDate = new Date()
sessionLog = sessionLog + '\r\n\r\nLog End for ' + sessionFooter + ': ' + currentDate.getFullYear() + '/' + (currentDate.getMonth() + 1) + '/' + currentDate.getDate() + ' @ ' + currentDate.getHours() + ':' + currentDate.getMinutes() + ':' + currentDate.getSeconds() + '\r\n'
sessionLog = sessionLog + '\r\n\r\nLog End for ' + sessionFooter + ': ' +
currentDate.getFullYear() + '/' + (currentDate.getMonth() + 1) + '/' +
currentDate.getDate() + ' @ ' + currentDate.getHours() + ':' +
currentDate.getMinutes() + ':' + currentDate.getSeconds() + '\r\n'
logDate = currentDate
return false
} else {
sessionLogEnable = true
document.getElementById('toggleLog').innerHTML = '<a class="toggleLog" href="javascript:void(0)" onclick="toggleLog();">Logging - STOP LOG</a>'
document.getElementById('toggleLog').innerHTML =
'<a class="toggleLog" href="javascript:void(0)" onclick="toggleLog();">Logging - STOP LOG</a>'
document.getElementById('downloadLog').style.display = 'inline'
console.log('starting log, ' + sessionLogEnable)
currentDate = new Date()
sessionLog = 'Log Start for ' + sessionFooter + ': ' + currentDate.getFullYear() + '/' + (currentDate.getMonth() + 1) + '/' + currentDate.getDate() + ' @ ' + currentDate.getHours() + ':' + currentDate.getMinutes() + ':' + currentDate.getSeconds() + '\r\n\r\n'
sessionLog = 'Log Start for ' + sessionFooter + ': ' +
currentDate.getFullYear() + '/' + (currentDate.getMonth() + 1) + '/' +
currentDate.getDate() + ' @ ' + currentDate.getHours() + ':' +
currentDate.getMinutes() + ':' + currentDate.getSeconds() + '\r\n\r\n'
logDate = currentDate
return false
}
@ -35,7 +43,9 @@ function toggleLog () { // eslint-disable-line
// cross browser method to "download" an element to the local system
// used for our client-side logging feature
function downloadLog () { // eslint-disable-line
myFile = 'WebSSH2-' + logDate.getFullYear() + (logDate.getMonth() + 1) + logDate.getDate() + '_' + logDate.getHours() + logDate.getMinutes() + logDate.getSeconds() + '.log'
myFile = 'WebSSH2-' + logDate.getFullYear() + (logDate.getMonth() + 1) +
logDate.getDate() + '_' + logDate.getHours() + logDate.getMinutes() +
logDate.getSeconds() + '.log'
// regex should eliminate escape sequences from being logged.
var blob = new Blob([sessionLog.replace(/[\u001b\u009b][[()#;?]*(?:[0-9]{1,4}(?:;[0-9]{0,4})*)?[0-9A-ORZcf-nqry=><]/g, '')], {
type: 'text/plain'
@ -113,7 +123,8 @@ socket.on('connect', function () {
}).on('disconnect', function (err) {
if (!errorExists) {
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)
}).on('error', function (err) {

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -88,7 +88,7 @@ body {
}
a.credentials {
color: rgb(51, 51, 51);
text-decoration: none;
text-decoration: none;
}
#downloadLog {
display: inline-block;
@ -104,7 +104,7 @@ a.credentials {
}
a.downloadLog {
color: rgb(240, 240, 240);
text-decoration: none;
text-decoration: none;
}
#toggleLog {
display: inline-block;
@ -120,7 +120,7 @@ a.downloadLog {
}
a.toggleLog {
color: rgb(240, 240, 240);
text-decoration: none;
text-decoration: none;
}
/**

View file

@ -12520,19 +12520,27 @@ function replayCredentials () { // eslint-disable-line
function toggleLog () { // eslint-disable-line
if (sessionLogEnable === true) {
sessionLogEnable = false
document.getElementById('toggleLog').innerHTML = '<a class="toggleLog" href="javascript:void(0);" onclick="toggleLog();">Start Log</a>'
document.getElementById('toggleLog').innerHTML =
'<a class="toggleLog" href="javascript:void(0);" onclick="toggleLog();">Start Log</a>'
console.log('stopping log, ' + sessionLogEnable)
currentDate = new Date()
sessionLog = sessionLog + '\r\n\r\nLog End for ' + sessionFooter + ': ' + currentDate.getFullYear() + '/' + (currentDate.getMonth() + 1) + '/' + currentDate.getDate() + ' @ ' + currentDate.getHours() + ':' + currentDate.getMinutes() + ':' + currentDate.getSeconds() + '\r\n'
sessionLog = sessionLog + '\r\n\r\nLog End for ' + sessionFooter + ': ' +
currentDate.getFullYear() + '/' + (currentDate.getMonth() + 1) + '/' +
currentDate.getDate() + ' @ ' + currentDate.getHours() + ':' +
currentDate.getMinutes() + ':' + currentDate.getSeconds() + '\r\n'
logDate = currentDate
return false
} else {
sessionLogEnable = true
document.getElementById('toggleLog').innerHTML = '<a class="toggleLog" href="javascript:void(0)" onclick="toggleLog();">Logging - STOP LOG</a>'
document.getElementById('toggleLog').innerHTML =
'<a class="toggleLog" href="javascript:void(0)" onclick="toggleLog();">Logging - STOP LOG</a>'
document.getElementById('downloadLog').style.display = 'inline'
console.log('starting log, ' + sessionLogEnable)
currentDate = new Date()
sessionLog = 'Log Start for ' + sessionFooter + ': ' + currentDate.getFullYear() + '/' + (currentDate.getMonth() + 1) + '/' + currentDate.getDate() + ' @ ' + currentDate.getHours() + ':' + currentDate.getMinutes() + ':' + currentDate.getSeconds() + '\r\n\r\n'
sessionLog = 'Log Start for ' + sessionFooter + ': ' +
currentDate.getFullYear() + '/' + (currentDate.getMonth() + 1) + '/' +
currentDate.getDate() + ' @ ' + currentDate.getHours() + ':' +
currentDate.getMinutes() + ':' + currentDate.getSeconds() + '\r\n\r\n'
logDate = currentDate
return false
}
@ -12541,7 +12549,9 @@ function toggleLog () { // eslint-disable-line
// cross browser method to "download" an element to the local system
// used for our client-side logging feature
function downloadLog () { // eslint-disable-line
myFile = 'WebSSH2-' + logDate.getFullYear() + (logDate.getMonth() + 1) + logDate.getDate() + '_' + logDate.getHours() + logDate.getMinutes() + logDate.getSeconds() + '.log'
myFile = 'WebSSH2-' + logDate.getFullYear() + (logDate.getMonth() + 1) +
logDate.getDate() + '_' + logDate.getHours() + logDate.getMinutes() +
logDate.getSeconds() + '.log'
// regex should eliminate escape sequences from being logged.
var blob = new Blob([sessionLog.replace(/[\u001b\u009b][[()#;?]*(?:[0-9]{1,4}(?:;[0-9]{0,4})*)?[0-9A-ORZcf-nqry=><]/g, '')], {
type: 'text/plain'
@ -12619,7 +12629,8 @@ socket.on('connect', function () {
}).on('disconnect', function (err) {
if (!errorExists) {
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)
}).on('error', function (err) {

File diff suppressed because one or more lines are too long

View file

@ -1,3 +1,5 @@
// socket/index.js
// private
var debug = require('debug')
var debugWebSSH2 = require('debug')('WebSSH2')
@ -5,30 +7,7 @@ var SSH = require('ssh2').Client
var termCols, termRows
// 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)
}
}
module.exports = function socket (socket) {
// if websocket connection arrives without an express session, kill it
if (!socket.request.session) {
socket.emit('401 UNAUTHORIZED')
@ -37,19 +16,18 @@ module.exports = function (socket) {
return
}
var conn = new SSH()
socket.on('geometry', function (cols, rows) {
socket.on('geometry', function socketOnGeometry (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) {
conn.on('banner', function connOnBanner (data) {
// need to convert to cr/lf for proper formatting
d = d.replace(/\r?\n/g, '\r\n')
socket.emit('data', d.toString('binary'))
data = data.replace(/\r?\n/g, '\r\n')
socket.emit('data', data.toString('utf-8'))
})
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)
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 + ' allowreplay=' + socket.request.session.ssh.allowreplay + ' term=' + socket.request.session.ssh.term)
socket.emit('title', 'ssh://' + socket.request.session.ssh.host)
socket.request.session.ssh.header.background && socket.emit('headerBackground', socket.request.session.ssh.header.background)
socket.request.session.ssh.header.name && socket.emit('header', socket.request.session.ssh.header.name)
@ -61,25 +39,27 @@ module.exports = function (socket) {
term: socket.request.session.ssh.term,
cols: termCols,
rows: termRows
}, function (err, stream) {
}, function connShell (err, stream) {
if (err) {
SSHerror('EXEC ERROR' + err)
conn.end()
return
}
// poc to log commands from client
// var dataBuffer
socket.on('data', function (data) {
if (socket.request.session.ssh.serverlog.client) var dataBuffer
socket.on('data', function socketOnData (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
// }
if (socket.request.session.ssh.serverlog.client) {
if (data === '\r') {
console.log('serverlog.client: ' + socket.request.session.id + '/' + socket.id + ' host: ' + socket.request.session.ssh.host + ' command: ' + dataBuffer)
dataBuffer = undefined
} else {
dataBuffer = (dataBuffer) ? dataBuffer + data : data
}
}
})
socket.on('control', function (controlData) {
socket.on('control', function socketOnControl (controlData) {
switch (controlData) {
case 'replayCredentials':
stream.write(socket.request.session.userpassword + '\n')
@ -88,55 +68,82 @@ module.exports = function (socket) {
console.log('controlData: ' + controlData)
}
})
socket.on('disconnecting', function (reason) { debugWebSSH2('SOCKET DISCONNECTING: ' + reason) })
socket.on('disconnect', function (reason) {
socket.on('disconnecting', function socketOnDisconnecting (reason) { debugWebSSH2('SOCKET DISCONNECTING: ' + reason) })
socket.on('disconnect', function socketOnDisconnect (reason) {
debugWebSSH2('SOCKET DISCONNECT: ' + reason)
err = { message: reason }
SSHerror('CLIENT SOCKET DISCONNECT', err)
conn.end()
// socket.request.session.destroy()
})
socket.on('error', function socketOnError (err) {
SSHerror('SOCKET ERROR', err)
conn.end()
})
socket.on('error', function (error) { debugWebSSH2('SOCKET ERROR: ' + JSON.stringify(error)) })
stream.on('data', function (d) { socket.emit('data', d.toString('utf-8')) })
stream.on('close', function (code, signal) {
stream.on('data', function streamOnData (data) { socket.emit('data', data.toString('utf-8')) })
stream.on('close', function streamOnClose (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) {
stream.stderr.on('data', function streamStderrOnData (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) {
conn.on('end', function connOnEnd (err) { SSHerror('CONN END BY HOST', err) })
conn.on('close', function connOnClose (err) { SSHerror('CONN CLOSE', err) })
conn.on('error', function connOnError (err) { SSHerror('CONN ERROR', err) })
conn.on('keyboard-interactive', function connOnKeyboardInteractive (name, instructions, instructionsLang, prompts, finish) {
debugWebSSH2('conn.on(\'keyboard-interactive\')')
finish([socket.request.session.userpassword])
})
if (socket.request.session.username && socket.request.session.userpassword) {
if (socket.request.session.username && socket.request.session.userpassword && socket.request.session.ssh) {
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')
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
*/
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)
} 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)
}
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)
}
}

View file

@ -88,7 +88,7 @@ body {
}
a.credentials {
color: rgb(51, 51, 51);
text-decoration: none;
text-decoration: none;
}
#downloadLog {
display: inline-block;
@ -104,7 +104,7 @@ a.credentials {
}
a.downloadLog {
color: rgb(240, 240, 240);
text-decoration: none;
text-decoration: none;
}
#toggleLog {
display: inline-block;
@ -120,5 +120,5 @@ a.downloadLog {
}
a.toggleLog {
color: rgb(240, 240, 240);
text-decoration: none;
text-decoration: none;
}

View file

@ -14,19 +14,27 @@ function replayCredentials () { // eslint-disable-line
function toggleLog () { // eslint-disable-line
if (sessionLogEnable === true) {
sessionLogEnable = false
document.getElementById('toggleLog').innerHTML = '<a class="toggleLog" href="javascript:void(0);" onclick="toggleLog();">Start Log</a>'
document.getElementById('toggleLog').innerHTML =
'<a class="toggleLog" href="javascript:void(0);" onclick="toggleLog();">Start Log</a>'
console.log('stopping log, ' + sessionLogEnable)
currentDate = new Date()
sessionLog = sessionLog + '\r\n\r\nLog End for ' + sessionFooter + ': ' + currentDate.getFullYear() + '/' + (currentDate.getMonth() + 1) + '/' + currentDate.getDate() + ' @ ' + currentDate.getHours() + ':' + currentDate.getMinutes() + ':' + currentDate.getSeconds() + '\r\n'
sessionLog = sessionLog + '\r\n\r\nLog End for ' + sessionFooter + ': ' +
currentDate.getFullYear() + '/' + (currentDate.getMonth() + 1) + '/' +
currentDate.getDate() + ' @ ' + currentDate.getHours() + ':' +
currentDate.getMinutes() + ':' + currentDate.getSeconds() + '\r\n'
logDate = currentDate
return false
} else {
sessionLogEnable = true
document.getElementById('toggleLog').innerHTML = '<a class="toggleLog" href="javascript:void(0)" onclick="toggleLog();">Logging - STOP LOG</a>'
document.getElementById('toggleLog').innerHTML =
'<a class="toggleLog" href="javascript:void(0)" onclick="toggleLog();">Logging - STOP LOG</a>'
document.getElementById('downloadLog').style.display = 'inline'
console.log('starting log, ' + sessionLogEnable)
currentDate = new Date()
sessionLog = 'Log Start for ' + sessionFooter + ': ' + currentDate.getFullYear() + '/' + (currentDate.getMonth() + 1) + '/' + currentDate.getDate() + ' @ ' + currentDate.getHours() + ':' + currentDate.getMinutes() + ':' + currentDate.getSeconds() + '\r\n\r\n'
sessionLog = 'Log Start for ' + sessionFooter + ': ' +
currentDate.getFullYear() + '/' + (currentDate.getMonth() + 1) + '/' +
currentDate.getDate() + ' @ ' + currentDate.getHours() + ':' +
currentDate.getMinutes() + ':' + currentDate.getSeconds() + '\r\n\r\n'
logDate = currentDate
return false
}
@ -35,7 +43,9 @@ function toggleLog () { // eslint-disable-line
// cross browser method to "download" an element to the local system
// used for our client-side logging feature
function downloadLog () { // eslint-disable-line
myFile = 'WebSSH2-' + logDate.getFullYear() + (logDate.getMonth() + 1) + logDate.getDate() + '_' + logDate.getHours() + logDate.getMinutes() + logDate.getSeconds() + '.log'
myFile = 'WebSSH2-' + logDate.getFullYear() + (logDate.getMonth() + 1) +
logDate.getDate() + '_' + logDate.getHours() + logDate.getMinutes() +
logDate.getSeconds() + '.log'
// regex should eliminate escape sequences from being logged.
var blob = new Blob([sessionLog.replace(/[\u001b\u009b][[()#;?]*(?:[0-9]{1,4}(?:;[0-9]{0,4})*)?[0-9A-ORZcf-nqry=><]/g, '')], {
type: 'text/plain'
@ -113,7 +123,8 @@ socket.on('connect', function () {
}).on('disconnect', function (err) {
if (!errorExists) {
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)
}).on('error', function (err) {

View file

@ -1,28 +1,18 @@
// util/index.js
// private
require('colors') // allow for color property extensions in log messages
var debug = require('debug')('WebSSH2')
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) {
if (arguments[i] instanceof Object) { arguments[i] = util.inspect(arguments[i]) }
}
fct(Array.prototype.join.call(arguments, ' ')[color])
}
}
exports.basicAuth = function (req, res, next) {
exports.basicAuth = function basicAuth (req, res, next) {
var myAuth = Auth(req)
if (myAuth) {
req.session.username = myAuth.name
req.session.userpassword = myAuth.pass
debug('myAuth.name: ' + myAuth.name.yellow.bold.underline + ' and password ' + ((myAuth.pass) ? 'exists'.yellow.bold.underline : 'is blank'.underline.red.bold))
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
@ -31,10 +21,3 @@ 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)
}