Merge remote-tracking branch 'remotes/billchurch/master'

# Conflicts:
#	index.js
This commit is contained in:
Manuel Barallobre 2017-05-29 10:38:51 +02:00
commit 4eff6128a2
26 changed files with 30516 additions and 227 deletions

View file

@ -12,5 +12,8 @@
"read-config",
"socket.io"
]
}
},
"ignore": [
"public/src/**"
]
}

View file

@ -1,22 +1,44 @@
# Change Log
## [Unreleased]
## [0.1.0] 2017-05-27
### Added
- This ChangeLog.md file
- Support for UTF-8 characters (thanks @bara666)
- 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)
- using Grunt to pull js and css source files from other modules `npm run build` to rebuild these if changed or updated.
- `useminified` option in `config.json` to enable using minified client side javascript (true) defaults to false (non-minified)
- sshterm= query option to specify TERM environment variable for host, valid strings are alpha-numeric with a hypen (validated). Otherwise the default ssh.term variable from `config.json` will be used.
- validation for host (v4,v6,fqdn,hostname), port (integer 2-65535), and header (sanitized) from URL input
### Changed
- erorr handling in public/client.js
- 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)
-- 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`
- minified all css files to a single css in `./public/webssh2.min.css`
- copied all unmodified source css and js to /public/src/css and /public/src/js respectively (for troubleshooting/etc)
- 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.
### 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.
- 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.
- ensure ssh session is closed when a browser disconnects from the websocket
- if headerBackground is changed, status background is changed to the same color (typo, fixed)
## [0.0.5] - 2017-0323
### Removed
- Express Static References directly to module source directories due to concatenating and minifying js/css
## [0.0.5] - 2017-03-23
### Added
- Added experimental support for logging (see Readme)

73
Gruntfile.js Normal file
View file

@ -0,0 +1,73 @@
module.exports = function (grunt) {
// Project configuration.
grunt.initConfig({
pkg: grunt.file.readJSON('package.json'),
copy: {
main: {
files: [
{
expand: true,
flatten: true,
src: [
'node_modules/xterm/dist/xterm.css',
'src/css/style.css'
],
dest: 'public/src/css'
},
{
expand: true,
flatten: true,
src: [
'node_modules/xterm/dist/xterm.js',
'node_modules/xterm/dist/xterm.js.map',
'node_modules/xterm/dist/addons/fit/fit.js',
'node_modules/socket.io/node_modules/socket.io-client/dist/socket.io.js',
'node_modules/socket.io/node_modules/socket.io-client/dist/socket.io.js.map',
'src/js/client.js'
],
dest: 'public/src/js'
}
]
}
},
concat: {
options: {
sourceMap: true,
sourceMapName: 'public/src/webssh2.concat.map',
sourceMapStyle: 'embed'
},
css: {
src: ['public/src/css/*.css'],
dest: 'public/webssh2.css'
},
js: {
src: [
'public/src/js/xterm.js',
'public/src/js/fit.js',
'public/src/js/socket.io.js',
'public/src/js/client.js'
],
dest: 'public/webssh2.js'
}
},
uglify: {
options: {
banner: '/*! <%= pkg.name %> <%= grunt.template.today("yyyy-mm-dd") %> */\n',
sourceMap: true,
sourceMapName: 'public/src/webssh2.min.map'
},
build: {
src: ['public/src/js/xterm.js', 'public/src/js/fit.js', 'public/src/js/socket.io.js', 'public/src/js/client.js'],
dest: 'public/webssh2.min.js'
}
}
})
// Load the plugin that provides the "uglify" task.
grunt.loadNpmTasks('grunt-contrib-copy')
grunt.loadNpmTasks('grunt-contrib-concat')
grunt.loadNpmTasks('grunt-contrib-uglify')
// Default task(s).
grunt.registerTask('default', ['copy', 'concat', 'uglify'])
}

152
README.md
View file

@ -1,4 +1,4 @@
# 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)
# 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.
@ -8,7 +8,7 @@ Bare bones example of using SSH2 as a client on a host to proxy a Websocket / So
# Instructions
To install:
1. Clone to a location somewhere and `npm install`
1. Clone to a location somewhere and `npm install --production`. If you want to develop and rebuild javascript and other files utilize `npm install` instead.
2. If desired, edit config.json to change the listener to your liking. There are also some default options which may be definied for a few of the variables.
@ -21,52 +21,136 @@ http://localhost:2222/ssh/host/127.0.0.1
You will be prompted for credentials to use on the SSH server via HTTP Basic authentcaiton. This is to permit usage with some SSO systems that can replay credentials over HTTP basic.
# Options (GET request vars)
# Options
port= - port of SSH server (defaults to 22)
## GET request vars
header= - optional header to display on page
* **port=** - _integer_ - port of SSH server (defaults to 22)
headerBackground= - optional background color of header to display on page
* **header=** - _string_ - optional header to display on page
# Config File Options
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...
* **headerBackground=** - _string_ - optional background color of header to display on page
`listen.ip` default `127.0.0.1`
* IP address node should listen on for client connections.
## Headers
`listen.port` default `2222`
* Port node should listen on for client connections.
* **allowreplay** - _boolean_ - Allow use of password replay feature, example `allowreplay: true`
`user.name` default `null`
* Specify user name to authenticate with.
## Config File Options
`config.json` contains several options which may be specified to customize to your needs, vs editing the javascript directly. This is JSON format so mind your spacing, brackets, etc...
`user.password` default `null`
* Specify password to authenticate with.
* **listen.ip** - _string_ - IP address node should listen on for client connections, defaults to `127.0.0.1`
`ssh.host` default `null`
* Specify host to connect to.
* **listen.port** - _integer_ - Port node should listen on for client connections, defaults to `2222`
`ssh.port` default `22`
* Specify SSH port to connect to.
* **user.name** - _string_ - Specify user name to authenticate with. In normal cases this should be left to the default `null` setting.
`ssh.term` default `xterm-color`
* Specify terminal emulation to use.
* **user.password** - _string_ - Specify password to authenticate with. In normal cases this should be left to the default `null` setting.
`header.text`
* Specify header text, defaults to `My Header` but may also be set to `null`.
* **ssh.host** - _string_ - Specify host to connect to. May be either hostname or IP address. Defaults to `null`.
`header.background`
* Header background, defaults to `green`.
* **ssh.port** - _integer_ - Specify SSH port to connect to, defaults to `22`
`session.name`
* Name of session ID cookie. it's not a horrible idea to make this something unique.
* **ssh.term** - _string_ - Specify terminal emulation to use, defaults to `xterm-color`
`session.secret`
* Secret key for cookie encryption. You should change this in production.
* **useminified** - _boolean_ - Choose between ./public/client-full.htm (false/non-minified) or ./public/client-min.htm (true/minified js), defaults to false (non-minified version)
`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.
* **header.text** - _string_ - Specify header text, defaults to `My Header` but may also be set to `null`. When set to `null` no header bar will be displayed on the client.
* **header.background** - _string_ - Header background, defaults to `green`.
* **session.name** - _string_ - Name of session ID cookie. it's not a horrible idea to make this something unique.
* **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.
* **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:
* **kex** - _array_ - Key exchange algorithms.
* Default values:
1. ecdh-sha2-nistp256
2. ecdh-sha2-nistp384
3. ecdh-sha2-nistp521
4. diffie-hellman-group-exchange-sha256
5. diffie-hellman-group14-sha1
* Supported values:
* ecdh-sha2-nistp256
* ecdh-sha2-nistp384
* ecdh-sha2-nistp521
* diffie-hellman-group-exchange-sha256
* diffie-hellman-group14-sha1
* diffie-hellman-group-exchange-sha1
* diffie-hellman-group1-sha1
* **cipher** - _array_ - Ciphers.
* Default values:
1. aes128-ctr
2. aes192-ctr
3. aes256-ctr
4. aes128-gcm
5. aes128-gcm@openssh.com
6. aes256-gcm
7. aes256-gcm@openssh.com
8. aes256-cbc **legacy cipher for backward compatibility, should removed :+1:**
* Supported values:
* aes128-ctr
* aes192-ctr
* aes256-ctr
* aes128-gcm
* aes128-gcm@openssh.com
* aes256-gcm
* aes256-gcm@openssh.com
* aes256-cbc
* aes192-cbc
* aes128-cbc
* blowfish-cbc
* 3des-cbc
* arcfour256
* arcfour128
* cast128-cbc
* arcfour
* **hmac** - _array_ - (H)MAC algorithms.
* Default values:
1. hmac-sha2-256
2. hmac-sha2-512
3. hmac-sha1 **legacy hmac for backward compatibility, should removed :+1:**
* Supported values:
* hmac-sha2-256
* hmac-sha2-512
* hmac-sha1
* hmac-md5
* hmac-sha2-256-96
* hmac-sha2-512-96
* hmac-ripemd160
* hmac-sha1-96
* hmac-md5-96
* **compress** - _array_ - Compression algorithms.
* Default values:
1. none
2. zlib@openssh.com
3. zlib
* Supported values:
* none
* zlib@openssh.com
* zlib
# 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.
@ -75,3 +159,7 @@ Clicking `Start logging` on the status bar will log all data to the client. A `D
http://localhost:2222/ssh/host/192.168.1.1?port=2244&header=My%20Header&color=red
# 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

View file

@ -10,17 +10,47 @@
"ssh": {
"host": null,
"port": 22,
"term": "xterm-color",
"term": "xterm-color"
},
"useminified": false,
"header": {
"text": "My Header",
"text": null,
"background": "green"
},
"session": {
"name": "WebSSH2id",
"name": "WebSSH2",
"secret": "mysecret"
},
"options": {
"challengeButton": true
},
"algorithms": {
"kex": [
"ecdh-sha2-nistp256",
"ecdh-sha2-nistp384",
"ecdh-sha2-nistp521",
"diffie-hellman-group-exchange-sha256",
"diffie-hellman-group14-sha1"
],
"cipher": [
"aes128-ctr",
"aes192-ctr",
"aes256-ctr",
"aes128-gcm",
"aes128-gcm@openssh.com",
"aes256-gcm",
"aes256-gcm@openssh.com",
"aes256-cbc" // for some legacy systems
],
"hmac": [
"hmac-sha2-256",
"hmac-sha2-512",
"hmac-sha1" // for some legacy systems
],
"compress": [
"none",
"zlib@openssh.com",
"zlib"
]
}
}
}

200
index.js
View file

@ -8,40 +8,16 @@ 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 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
})
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 +34,32 @@ 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
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)
})
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')))
// 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!")
})
@ -92,124 +70,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('utf-8')) })
stream.on('close', function (code, signal) {
err = { message: ((code || signal) ? (((code) ? 'CODE: ' + code : '') + ((code && signal) ? ' ' : '') + ((signal) ? 'SIGNAL: ' + signal : '')) : undefined) }
socketutil.SSHerror('STREAM CLOSE', err)
conn.end()
})
stream.stderr.on('data', function (data) {
console.log('STDERR: ' + data)
})
})
})
conn.on('end', function (err) { socketutil.SSHerror('CONN END BY HOST', err) })
conn.on('close', function (err) { socketutil.SSHerror('CONN CLOSE', err) })
conn.on('error', function (err) { socketutil.SSHerror('CONN ERROR', err) })
conn.on('keyboard-interactive', function (name, instructions, instructionsLang, prompts, finish) {
debugWebSSH2('Connection :: keyboard-interactive')
finish([socket.request.session.userpassword])
})
if (socket.request.session.username && socket.request.session.userpassword) {
conn.connect({
host: config.ssh.host,
port: config.ssh.port,
username: socket.request.session.username,
password: socket.request.session.userpassword,
tryKeyboard: true,
// some cisco routers need the these cipher strings
algorithms: {
'cipher': ['aes128-cbc', '3des-cbc', 'aes256-cbc', 'aes128-ctr', 'aes192-ctr', 'aes256-ctr'],
'hmac': ['hmac-sha1', 'hmac-sha1-96', 'hmac-md5-96']
},
debug: debug('ssh2')
})
server.on('error', function (err) {
if (err.code === 'EADDRINUSE') {
config.listen.port++
console.warn('Address in use, retrying on port ' + config.listen.port)
setTimeout(function () {
server.listen(config.listen.port)
}, 250)
} else {
console.warn('Attempt to connect without session.username/password defined, potentially previously abandoned client session. disconnecting websocket client.\r\nHandshake information: \r\n ' + JSON.stringify(socket.handshake))
socket.emit('statusBackground', 'red')
socket.emit('status', 'WEBSOCKET ERROR - Reload and try again')
socket.disconnect(true)
console.log('server.listen ERROR: ' + err.code)
}
})

View file

@ -1,6 +1,6 @@
{
"name": "WebSSH2",
"version": "0.0.5",
"version": "0.1.0",
"ignore": [
".gitignore"
],
@ -35,14 +35,31 @@
"read-config": "^1.6.0",
"socket.io": "^1.6.0",
"ssh2": "^0.5.4",
"xterm": "^2.6.0"
"validator": "^7.0.0"
},
"scripts": {
"start": "node index",
"test": "snyk test"
"test": "snyk test",
"watch": "nodemon index.js",
"build": "grunt copy concat uglify",
"standard": "standard --verbose | snazzy"
},
"devDependencies": {
"bithound": "^1.7.0",
"snyk": "^1.30.1"
"grunt": "^1.0.1",
"grunt-contrib-concat": "^1.0.1",
"grunt-contrib-copy": "^1.0.0",
"grunt-contrib-uglify": "^3.0.1",
"nodemon": "^1.11.0",
"snazzy": "^7.0.0",
"snyk": "^1.30.1",
"standard": "^10.0.2",
"xterm": "^2.6.0"
},
"standard": {
"ignore": [
"public/webssh2.js",
"public/src/js/*.js"
]
}
}

View file

@ -1,12 +1,8 @@
<html>
<head>
<title>Web SSH</title>
<link rel="stylesheet" href="/src/xterm.css" />
<link rel="stylesheet" href="/style/style.css" />
<script src="/socket.io/socket.io.js" defer></script>
<script src="/src/xterm.js" defer></script>
<script src="/addons/fit/fit.js" defer></script>
<script src="/client.js" defer></script>
<link rel="stylesheet" href="/webssh2.css" />
<script src="/webssh2.js" defer></script>
</head>
<body>
<div class="box">
@ -21,4 +17,4 @@
</div>
</div>
</body>
</html>
</html>

20
public/client-min.htm Normal file
View file

@ -0,0 +1,20 @@
<html>
<head>
<title>Web SSH</title>
<link rel="stylesheet" href="/webssh2.css" />
<script src="/webssh2.min.js" defer></script>
</head>
<body>
<div class="box">
<div id="header"></div>
<div id="terminal-container" class="terminal"></div>
<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>
</div>
</body>
</html>

2247
public/src/css/xterm.css Normal file

File diff suppressed because it is too large Load diff

View file

@ -1,8 +1,9 @@
/* global io, Terminal, Blob */
var sessionLogEnable = false
var sessionLog, sessionFooter, logDate, currentDate, myFile, errorExists
// replay password to server, requires
function replayCredentials () {
function replayCredentials () { // eslint-disable-line
socket.emit('control', 'replayCredentials')
console.log('replaying credentials')
return false
@ -10,7 +11,7 @@ function replayCredentials () {
// Set variable to toggle log data from client/server to a varialble
// for later download
function toggleLog () {
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>'
@ -33,7 +34,7 @@ function toggleLog () {
// cross browser method to "download" an element to the local system
// used for our client-side logging feature
function downloadLog () {
function downloadLog () { // eslint-disable-line
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, '')], {
@ -58,7 +59,7 @@ var terminalContainer = document.getElementById('terminal-container')
var term = new Terminal({
cursorBlink: true
})
var socket, termid
var socket, termid // eslint-disable-line
term.open(terminalContainer, {
focus: true
})
@ -98,7 +99,7 @@ socket.on('connect', function () {
}).on('statusBackground', function (data) {
document.getElementById('status').style.backgroundColor = data
}).on('allowreplay', function (data) {
if (data === 'true') {
if (data === true) {
console.log('allowreplay: ' + data)
document.getElementById('credentials').style.display = 'inline'
} else {

86
public/src/js/fit.js Normal file
View file

@ -0,0 +1,86 @@
/**
* Fit terminal columns and rows to the dimensions of its DOM element.
*
* ## Approach
* - Rows: Truncate the division of the terminal parent element height by the terminal row height.
*
* - Columns: Truncate the division of the terminal parent element width by the terminal character
* width (apply display: inline at the terminal row and truncate its width with the current
* number of columns).
* @module xterm/addons/fit/fit
* @license MIT
*/
(function (fit) {
if (typeof exports === 'object' && typeof module === 'object') {
/*
* CommonJS environment
*/
module.exports = fit(require('../../xterm'));
} else if (typeof define == 'function') {
/*
* Require.js is available
*/
define(['../../xterm'], fit);
} else {
/*
* Plain browser environment
*/
fit(window.Terminal);
}
})(function (Xterm) {
var exports = {};
exports.proposeGeometry = function (term) {
if (!term.element.parentElement) {
return null;
}
var parentElementStyle = window.getComputedStyle(term.element.parentElement),
parentElementHeight = parseInt(parentElementStyle.getPropertyValue('height')),
parentElementWidth = Math.max(0, parseInt(parentElementStyle.getPropertyValue('width')) - 17),
elementStyle = window.getComputedStyle(term.element),
elementPaddingVer = parseInt(elementStyle.getPropertyValue('padding-top')) + parseInt(elementStyle.getPropertyValue('padding-bottom')),
elementPaddingHor = parseInt(elementStyle.getPropertyValue('padding-right')) + parseInt(elementStyle.getPropertyValue('padding-left')),
availableHeight = parentElementHeight - elementPaddingVer,
availableWidth = parentElementWidth - elementPaddingHor,
container = term.rowContainer,
subjectRow = term.rowContainer.firstElementChild,
contentBuffer = subjectRow.innerHTML,
characterHeight,
rows,
characterWidth,
cols,
geometry;
subjectRow.style.display = 'inline';
subjectRow.innerHTML = 'W'; // Common character for measuring width, although on monospace
characterWidth = subjectRow.getBoundingClientRect().width;
subjectRow.style.display = ''; // Revert style before calculating height, since they differ.
characterHeight = subjectRow.getBoundingClientRect().height;
subjectRow.innerHTML = contentBuffer;
rows = parseInt(availableHeight / characterHeight);
cols = parseInt(availableWidth / characterWidth);
geometry = {cols: cols, rows: rows};
return geometry;
};
exports.fit = function (term) {
var geometry = exports.proposeGeometry(term);
if (geometry) {
term.resize(geometry.cols, geometry.rows);
}
};
Xterm.prototype.proposeGeometry = function () {
return exports.proposeGeometry(this);
};
Xterm.prototype.fit = function () {
return exports.fit(this);
};
return exports;
});

8201
public/src/js/socket.io.js Normal file

File diff suppressed because it is too large Load diff

File diff suppressed because one or more lines are too long

4219
public/src/js/xterm.js Normal file

File diff suppressed because it is too large Load diff

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

2374
public/webssh2.css Normal file

File diff suppressed because it is too large Load diff

12633
public/webssh2.js Normal file

File diff suppressed because it is too large Load diff

4
public/webssh2.min.js vendored Normal file

File diff suppressed because one or more lines are too long

View file

@ -1,12 +1,142 @@
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.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)
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', 'green')
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('utf-8')) })
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)
}
}

124
src/css/style.css Normal file
View file

@ -0,0 +1,124 @@
body {
font-family: helvetica, sans-serif, arial;
font-size: 1em;
color: #111;
background-color: rgb(0, 0, 0);
color: rgb(240, 240, 240);
height: 100%;
margin: 0;
}
#header {
color: rgb(240, 240, 240);
background-color: rgb(0, 128, 0);
width: 100%;
border-color: white;
border-style: none none solid none;
border-width: 1px;
text-align: center;
flex: 0 1 auto;
z-index: 99;
}
.box {
display: flex;
flex-flow: column;
height: 100%;
}
#terminal-container {
flex: 1 1 auto;
width: 100%;
margin: 0 auto;
padding: 2px;
}
#terminal-container .terminal {
background-color: #000000;
color: #fafafa;
padding: 2px;
}
#terminal-container .terminal:focus .terminal-cursor {
background-color: #fafafa;
}
#bottomdiv {
width: 100%;
background-color: rgb(50, 50, 50);
border-color: white;
border-style: solid none none none;
border-width: 1px;
z-index: 99;
flex: 0 1 30px;
}
#footer {
display: inline-block;
color: rgb(240, 240, 240);
background-color: rgb(50, 50, 50);
padding-left: 10px;
padding-right: 10px;
text-align: left;
}
#status {
display: inline-block;
color: rgb(240, 240, 240);
background-color: rgb(50, 50, 50);
padding-left: 10px;
padding-right: 10px;
border-color: white;
border-style: none solid none solid;
border-width: 1px;
text-align: left;
z-index: 100;
}
#credentials {
display: inline-block;
color: rgb(51, 51, 51);
background-color: rgb(255, 127, 0);
padding-left: 10px;
padding-right: 10px;
border-color: white;
border-style: none solid none none;
border-width: 1px;
text-align: left;
z-index: 100;
}
a.credentials {
color: rgb(51, 51, 51);
text-decoration: none;
}
#downloadLog {
display: inline-block;
color: rgb(240, 240, 240);
background-color: rgb(255, 127, 0);
padding-left: 10px;
padding-right: 10px;
border-color: white;
border-style: none solid none none;
border-width: 1px;
text-align: left;
z-index: 100;
}
a.downloadLog {
color: rgb(240, 240, 240);
text-decoration: none;
}
#toggleLog {
display: inline-block;
color: rgb(240, 240, 240);
background-color: rgb(0, 127, 0);
padding-left: 10px;
padding-right: 10px;
border-color: white;
border-style: none solid none none;
border-width: 1px;
text-align: left;
z-index: 100;
}
a.toggleLog {
color: rgb(240, 240, 240);
text-decoration: none;
}

125
src/js/client.js Normal file
View file

@ -0,0 +1,125 @@
/* global io, Terminal, Blob */
var sessionLogEnable = false
var sessionLog, sessionFooter, logDate, currentDate, myFile, errorExists
// replay password to server, requires
function replayCredentials () { // eslint-disable-line
socket.emit('control', 'replayCredentials')
console.log('replaying credentials')
return false
}
// Set variable to toggle log data from client/server to a varialble
// for later download
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>'
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'
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('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'
logDate = currentDate
return false
}
}
// 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'
// 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'
})
if (window.navigator.msSaveOrOpenBlob) {
window.navigator.msSaveBlob(blob, myFile)
} else {
var elem = window.document.createElement('a')
elem.href = window.URL.createObjectURL(blob)
elem.download = myFile
document.body.appendChild(elem)
elem.click()
document.body.removeChild(elem)
}
}
document.getElementById('downloadLog').style.display = 'none'
document.getElementById('credentials').style.display = 'none'
var terminalContainer = document.getElementById('terminal-container')
var term = new Terminal({
cursorBlink: true
})
var socket, termid // eslint-disable-line
term.open(terminalContainer, {
focus: true
})
term.fit()
if (document.location.pathname) {
var parts = document.location.pathname.split('/')
var base = parts.slice(0, parts.length - 1).join('/') + '/'
var resource = base.substring(1) + 'socket.io'
socket = io.connect(null, {
resource: resource
})
} else {
socket = io.connect()
}
socket.on('connect', function () {
socket.emit('geometry', term.cols, term.rows)
term.on('data', function (data) {
socket.emit('data', data)
})
socket.on('title', function (data) {
document.title = data
}).on('status', function (data) {
document.getElementById('status').innerHTML = data
}).on('ssherror', function (data) {
document.getElementById('status').innerHTML = data
document.getElementById('status').style.backgroundColor = 'red'
errorExists = true
}).on('headerBackground', function (data) {
document.getElementById('header').style.backgroundColor = data
}).on('header', function (data) {
document.getElementById('header').innerHTML = data
}).on('footer', function (data) {
sessionFooter = data
document.getElementById('footer').innerHTML = data
}).on('statusBackground', function (data) {
document.getElementById('status').style.backgroundColor = data
}).on('allowreplay', function (data) {
if (data === true) {
console.log('allowreplay: ' + data)
document.getElementById('credentials').style.display = 'inline'
} else {
document.getElementById('credentials').style.display = 'none'
}
}).on('data', function (data) {
term.write(data)
if (sessionLogEnable) {
sessionLog = sessionLog + data
}
}).on('disconnect', function (err) {
if (!errorExists) {
document.getElementById('status').style.backgroundColor = 'red'
document.getElementById('status').innerHTML = 'WEBSOCKET SERVER DISCONNECTED: ' + err
}
socket.io.reconnection(false)
}).on('error', function (err) {
if (!errorExists) {
document.getElementById('status').style.backgroundColor = 'red'
document.getElementById('status').innerHTML = 'ERROR: ' + err
}
})
})

View file

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