diff --git a/app/package.json b/app/package.json
index 032725ebc..51ca5aead 100644
--- a/app/package.json
+++ b/app/package.json
@@ -21,7 +21,7 @@
}
],
"engines": {
- "node": ">= 6"
+ "node": ">= 10"
},
"bugs": {
"url": "https://github.com/billchurch/WebSSH2/issues"
diff --git a/app/scripts/webpack.common.js b/app/scripts/webpack.common.js
index 14e2f39f0..368e838d4 100644
--- a/app/scripts/webpack.common.js
+++ b/app/scripts/webpack.common.js
@@ -1,5 +1,5 @@
const path = require('path')
-const { CleanWebpackPlugin } = require('clean-webpack-plugin');
+const { CleanWebpackPlugin } = require('clean-webpack-plugin')
/* const CleanWebpackPlugin = require('clean-webpack-plugin') */
const CopyWebpackPlugin = require('copy-webpack-plugin')
const ExtractTextPlugin = require('extract-text-webpack-plugin')
diff --git a/app/scripts/webpack.prod.js b/app/scripts/webpack.prod.js
index 08e51a3ee..04567dda9 100644
--- a/app/scripts/webpack.prod.js
+++ b/app/scripts/webpack.prod.js
@@ -1,4 +1,4 @@
-const TerserPlugin = require('terser-webpack-plugin');
+const TerserPlugin = require('terser-webpack-plugin')
const merge = require('webpack-merge')
const common = require('./webpack.common.js')
@@ -9,8 +9,8 @@ module.exports = merge(common, {
terserOptions: {
parallel: 4,
ie8: false,
- safari10: false
+ safari10: false
}
- })],
+ })]
}
-})
\ No newline at end of file
+})
diff --git a/app/server/app.js b/app/server/app.js
index 5f0b49061..64240d2e4 100644
--- a/app/server/app.js
+++ b/app/server/app.js
@@ -2,104 +2,28 @@
/* jshint esversion: 6, asi: true, node: true */
// app.js
-var path = require('path')
-var fs = require('fs')
-var nodeRoot = path.dirname(require.main.filename)
-var configPath = path.join(nodeRoot, 'config.json')
-var publicPath = path.join(nodeRoot, 'client', 'public')
-console.log('WebSSH2 service reading config from: ' + configPath)
-var express = require('express')
-var logger = require('morgan')
-
-// sane defaults if config.json or parts are missing
-let config = {
- listen: {
- ip: '0.0.0.0',
- port: 2222
- },
- user: {
- name: null,
- password: null,
- privatekey: null
- },
- ssh: {
- host: null,
- port: 22,
- term: 'xterm-color',
- readyTimeout: 20000,
- keepaliveInterval: 120000,
- keepaliveCountMax: 10,
- allowedSubnets: []
- },
- terminal: {
- cursorBlink: true,
- scrollback: 10000,
- tabStopWidth: 8,
- bellStyle: 'sound'
- },
- header: {
- text: null,
- background: 'green'
- },
- session: {
- name: 'WebSSH2',
- secret: 'mysecret'
- },
- options: {
- challengeButton: true,
- allowreauth: 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'
- ],
- hmac: [
- 'hmac-sha2-256',
- 'hmac-sha2-512',
- 'hmac-sha1'
- ],
- compress: [
- 'none',
- 'zlib@openssh.com',
- 'zlib'
- ]
- },
- serverlog: {
- client: false,
- server: false
- },
- accesslog: false,
- verify: false
-}
+const compression = require('compression')
+const config = require('./app_config')
+const express = require('express')
+const http = require('http')
+const logger = require('morgan')
+const path = require('path')
+const socketIo = require('socket.io')
+const bodyParser = require('body-parser')
-// test if config.json exists, if not provide error message but try to run
-// anyway
-try {
- if (fs.existsSync(configPath)) {
- console.log('ephemeral_auth service reading config from: ' + configPath)
- config = require('read-config')(configPath)
- } else {
- console.error('\n\nERROR: Missing config.json for webssh. Current config: ' + JSON.stringify(config))
- console.error('\n See config.json.sample for details\n\n')
- }
-} catch (err) {
- console.error('\n\nERROR: Missing config.json for webssh. Current config: ' + JSON.stringify(config))
- console.error('\n See config.json.sample for details\n\n')
- console.error('ERROR:\n\n ' + err)
+const expressOptions = require('./expressOptions')
+const myutil = require('./util')
+const socket = require('./socket')
+const sshSessionConfig = require('./ssh_session_config')
+
+// Credential Token Store
+const DEFAULT_CC_TTL = 60 * 1000 * 5
+const cachedCredentials = {}
+function cachedCredentialSetTTL (key, ttl) {
+ setTimeout(() => {
+ console.log()
+ delete cachedCredentials[key]
+ }, ttl)
}
var session = require('express-session')({
@@ -109,72 +33,80 @@ var session = require('express-session')({
saveUninitialized: false,
unset: 'destroy'
})
-var app = express()
-var compression = require('compression')
-var server = require('http').Server(app)
-var myutil = require('./util')
-myutil.setDefaultCredentials(config.user.name, config.user.password, config.user.privatekey);
-var validator = require('validator')
-var io = require('socket.io')(server, { serveClient: false })
-var socket = require('./socket')
-var expressOptions = require('./expressOptions')
+
+myutil.setDefaultCredentials(config.user.name, config.user.password, config.user.privatekey)
// express
+const app = express()
+const server = http.Server(app)
+const io = socketIo(server, { serveClient: false })
+app.use(bodyParser.json())
app.use(compression({ level: 9 }))
app.use(session)
-app.use(myutil.basicAuth)
+// app.use(myutil.basicAuth)
if (config.accesslog) app.use(logger('common'))
app.disable('x-powered-by')
// static files
+const publicPath = path.join(path.dirname(require.main.filename), 'client', 'public')
app.use(express.static(publicPath, expressOptions))
-app.get('/reauth', function (req, res, next) {
- var r = req.headers.referer || '/'
- res.status(401).send('
')
-})
+//app.get('/reauth', function (req, res, next) {
+// var r = req.headers.referer || '/'
+// res.status(401).send('')
+//})
+
+// eslint-disable-next-line complexity
+//app.get('/ssh/host/:host?', function (req, res, next) {
+// res.sendFile(path.join(path.join(publicPath, 'client.htm')))
+//
+// // Setup the session data
+// req.session.ssh = sshSessionConfig(config, Object.assign({}, req.params, req.query, req.headers))
+//})
// eslint-disable-next-line complexity
-app.get('/ssh/host/:host?', function (req, res, next) {
- res.sendFile(path.join(path.join(publicPath, 'client.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,
- localAddress: config.ssh.localAddress,
- localPort: config.ssh.localPort,
- header: {
- name: req.query.header || config.header.text,
- background: req.query.headerBackground || config.header.background
- },
- algorithms: config.algorithms,
- keepaliveInterval: config.ssh.keepaliveInterval,
- keepaliveCountMax: config.ssh.keepaliveCountMax,
- allowedSubnets: config.ssh.allowedSubnets,
- term: (/^(([a-z]|[A-Z]|[0-9]|[!^(){}\-_~])+)?\w$/.test(req.query.sshterm) &&
- req.query.sshterm) || config.ssh.term,
- terminal: {
- cursorBlink: (validator.isBoolean(req.query.cursorBlink + '') ? myutil.parseBool(req.query.cursorBlink) : config.terminal.cursorBlink),
- scrollback: (validator.isInt(req.query.scrollback + '', { min: 1, max: 200000 }) && req.query.scrollback) ? req.query.scrollback : config.terminal.scrollback,
- tabStopWidth: (validator.isInt(req.query.tabStopWidth + '', { min: 1, max: 100 }) && req.query.tabStopWidth) ? req.query.tabStopWidth : config.terminal.tabStopWidth,
- bellStyle: ((req.query.bellStyle) && (['sound', 'none'].indexOf(req.query.bellStyle) > -1)) ? req.query.bellStyle : config.terminal.bellStyle
- },
- allowreplay: config.options.challengeButton || (validator.isBoolean(req.headers.allowreplay + '') ? myutil.parseBool(req.headers.allowreplay) : false),
- allowreauth: config.options.allowreauth || false,
- mrhsession: ((validator.isAlphanumeric(req.headers.mrhsession + '') && req.headers.mrhsession) ? req.headers.mrhsession : 'none'),
- serverlog: {
- client: config.serverlog.client || false,
- server: config.serverlog.server || false
- },
- readyTimeout: (validator.isInt(req.query.readyTimeout + '', { min: 1, max: 300000 }) &&
- req.query.readyTimeout) || config.ssh.readyTimeout
+app.get('/ssh/token_session/:token', function (req, res, next) {
+ const token = req.params.token
+ const creds = cachedCredentials[token]
+
+ if (creds) {
+ delete cachedCredentials[token]
+ res.sendFile(path.join(path.join(publicPath, 'client.htm')))
+
+ const { username, userpassword, host, port, label } = creds
+
+ // Setup the session data
+ req.session.username = username
+ req.session.userpassword = userpassword
+ req.session.label = label
+ req.session.ssh = sshSessionConfig(config, Object.assign({}, req.query, req.headers, { host: host, port: port }))
+ console.log(`[GET] /ssh/token_session/${token} - Claimed session`)
+ } else {
+ console.log(`[GET] /ssh/token_session/${token} - 404`)
+ res.status(404).send('Session Not Found
404
')
+ }
+})
+
+app.post('/ssh/set_session_credentials', (req, res) => {
+ const data = req.body
+
+ if (!data.username || !data.userpassword || !data.host) {
+ res.status(400).send('{"Error": "Missing required field"}')
+ } else {
+ const token = myutil.uuidv4()
+ const sshConfig = {
+ username: data.username,
+ userpassword: data.userpassword,
+ label: data.label,
+ host: data.host,
+ port: data.port || config.ssh.port
+ }
+
+ console.log(`[POST] /ssh/set_session_credentials - ${data.host} | ${data.username} 200`)
+ cachedCredentials[token] = sshConfig
+ cachedCredentialSetTTL(token, DEFAULT_CC_TTL)
+ res.status(200).send(JSON.stringify({ token: token, ttl: DEFAULT_CC_TTL }))
}
- if (req.session.ssh.header.name) validator.escape(req.session.ssh.header.name)
- if (req.session.ssh.header.background) validator.escape(req.session.ssh.header.background)
})
// express error handling
diff --git a/app/server/app_config.js b/app/server/app_config.js
new file mode 100644
index 000000000..e73a3f528
--- /dev/null
+++ b/app/server/app_config.js
@@ -0,0 +1,99 @@
+const path = require('path')
+const fs = require('fs')
+
+const configPath = path.join(path.dirname(require.main.filename), 'config.json')
+
+console.log('WebSSH2 service reading config from: ' + configPath)
+
+// sane defaults if config.json or parts are missing
+let config = {
+ listen: {
+ ip: '0.0.0.0',
+ port: 2222
+ },
+ user: {
+ name: null,
+ password: null,
+ privatekey: null
+ },
+ ssh: {
+ host: null,
+ port: 22,
+ term: 'xterm-color',
+ readyTimeout: 20000,
+ keepaliveInterval: 120000,
+ keepaliveCountMax: 10,
+ allowedSubnets: []
+ },
+ terminal: {
+ cursorBlink: true,
+ scrollback: 10000,
+ tabStopWidth: 8,
+ bellStyle: 'sound'
+ },
+ header: {
+ text: null,
+ background: 'green'
+ },
+ session: {
+ name: 'WebSSH2',
+ secret: 'mysecret'
+ },
+ options: {
+ challengeButton: true,
+ allowreauth: 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'
+ ],
+ hmac: [
+ 'hmac-sha2-256',
+ 'hmac-sha2-512',
+ 'hmac-sha1'
+ ],
+ compress: [
+ 'none',
+ 'zlib@openssh.com',
+ 'zlib'
+ ]
+ },
+ serverlog: {
+ client: false,
+ server: false
+ },
+ accesslog: false,
+ verify: false
+}
+
+// test if config.json exists, if not provide error message but try to run
+// anyway
+try {
+ if (fs.existsSync(configPath)) {
+ console.log('ephemeral_auth service reading config from: ' + configPath)
+ config = require('read-config')(configPath)
+ } else {
+ console.error('\n\nERROR: Missing config.json for webssh. Current config: ' + JSON.stringify(config))
+ console.error('\n See config.json.sample for details\n\n')
+ }
+} catch (err) {
+ console.error('\n\nERROR: Missing config.json for webssh. Current config: ' + JSON.stringify(config))
+ console.error('\n See config.json.sample for details\n\n')
+ console.error('ERROR:\n\n ' + err)
+}
+
+module.exports = config
diff --git a/app/server/socket.js b/app/server/socket.js
index da880f880..26c549d07 100644
--- a/app/server/socket.js
+++ b/app/server/socket.js
@@ -7,7 +7,7 @@
var debug = require('debug')
var debugWebSSH2 = require('debug')('WebSSH2')
var SSH = require('ssh2').Client
-var CIDRMatcher = require('cidr-matcher');
+var CIDRMatcher = require('cidr-matcher')
// var fs = require('fs')
// var hostkeys = JSON.parse(fs.readFileSync('./hostkeyhashes.json', 'utf8'))
var termCols, termRows
@@ -25,8 +25,8 @@ module.exports = function socket (socket) {
}
// If configured, check that requsted host is in a permitted subnet
- if ( (((socket.request.session || {}).ssh || {}).allowedSubnets || {}).length && ( socket.request.session.ssh.allowedSubnets.length > 0 ) ) {
- var matcher = new CIDRMatcher(socket.request.session.ssh.allowedSubnets);
+ if ((((socket.request.session || {}).ssh || {}).allowedSubnets || {}).length && (socket.request.session.ssh.allowedSubnets.length > 0)) {
+ var matcher = new CIDRMatcher(socket.request.session.ssh.allowedSubnets)
if (!matcher.contains(socket.request.session.ssh.host)) {
console.log('WebSSH2 ' + 'error: Requested host outside configured subnets / REJECTED'.red.bold +
' user=' + socket.request.session.username.yellow.bold.underline +
@@ -53,13 +53,13 @@ module.exports = function socket (socket) {
socket.emit('menu', menuData)
socket.emit('allowreauth', socket.request.session.ssh.allowreauth)
socket.emit('setTerminalOpts', socket.request.session.ssh.terminal)
- socket.emit('title', 'ssh://' + socket.request.session.ssh.host)
+ socket.emit('title', socket.request.session.label ? socket.request.session.label : 'ssh://' + socket.request.session.ssh.host)
if (socket.request.session.ssh.header.background) socket.emit('headerBackground', socket.request.session.ssh.header.background)
if (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('footer', socket.request.session.label ? socket.request.session.label : `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)
+ socket.emit('allowreplay', false);//socket.request.session.ssh.allowreplay)
conn.shell({
term: socket.request.session.ssh.term,
cols: termCols,
@@ -86,10 +86,10 @@ module.exports = function socket (socket) {
})
socket.on('control', function socketOnControl (controlData) {
switch (controlData) {
- case 'replayCredentials':
- if (socket.request.session.ssh.allowreplay) {
- stream.write(socket.request.session.userpassword + '\n')
- }
+ //case 'replayCredentials':
+ // if (socket.request.session.ssh.allowreplay) {
+ // stream.write(socket.request.session.userpassword + '\n')
+ // }
/* falls through */
default:
console.log('controlData: ' + controlData)
@@ -124,7 +124,7 @@ module.exports = function socket (socket) {
})
conn.on('end', function connOnEnd (err) { SSHerror('CONN END BY HOST', err) })
- conn.on('close', function connOnClose (err) { SSHerror('CONN CLOSE', 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\')')
@@ -132,7 +132,7 @@ module.exports = function socket (socket) {
})
if (socket.request.session.username && (socket.request.session.userpassword || socket.request.session.privatekey) && socket.request.session.ssh) {
// console.log('hostkeys: ' + hostkeys[0].[0])
- conn.connect({
+ const sshConfig = {
host: socket.request.session.ssh.host,
port: socket.request.session.ssh.port,
localAddress: socket.request.session.ssh.localAddress,
@@ -146,7 +146,9 @@ module.exports = function socket (socket) {
keepaliveInterval: socket.request.session.ssh.keepaliveInterval,
keepaliveCountMax: socket.request.session.ssh.keepaliveCountMax,
debug: debug('ssh2')
- })
+ }
+
+ conn.connect(sshConfig)
} else {
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')
diff --git a/app/server/ssh_session_config.js b/app/server/ssh_session_config.js
new file mode 100644
index 000000000..84aca234f
--- /dev/null
+++ b/app/server/ssh_session_config.js
@@ -0,0 +1,44 @@
+var validator = require('validator')
+const myutil = require('./util')
+
+module.exports = (appConfig, opts) => {
+ const sshSessionConfig = {
+ host: (validator.isIP(opts.host + '') && opts.host) ||
+ (validator.isFQDN(opts.host) && opts.host) ||
+ (/^(([a-z]|[A-Z]|[0-9]|[!^(){}\-_~])+)?\w$/.test(opts.host) &&
+ opts.host) || appConfig.ssh.host,
+ port: (validator.isInt(opts.port + '', { min: 1, max: 65535 }) &&
+ opts.port) || appConfig.ssh.port,
+ localAddress: appConfig.ssh.localAddress,
+ localPort: appConfig.ssh.localPort,
+ header: {
+ name: opts.header || appConfig.header.text,
+ background: opts.headerBackground || appConfig.header.background
+ },
+ algorithms: appConfig.algorithms,
+ keepaliveInterval: appConfig.ssh.keepaliveInterval,
+ keepaliveCountMax: appConfig.ssh.keepaliveCountMax,
+ allowedSubnets: appConfig.ssh.allowedSubnets,
+ term: (/^(([a-z]|[A-Z]|[0-9]|[!^(){}\-_~])+)?\w$/.test(opts.sshterm) &&
+ opts.sshterm) || appConfig.ssh.term,
+ terminal: {
+ cursorBlink: (validator.isBoolean(opts.cursorBlink + '') ? myutil.parseBool(opts.cursorBlink) : appConfig.terminal.cursorBlink),
+ scrollback: (validator.isInt(opts.scrollback + '', { min: 1, max: 200000 }) && opts.scrollback) ? opts.scrollback : appConfig.terminal.scrollback,
+ tabStopWidth: (validator.isInt(opts.tabStopWidth + '', { min: 1, max: 100 }) && opts.tabStopWidth) ? opts.tabStopWidth : appConfig.terminal.tabStopWidth,
+ bellStyle: ((opts.bellStyle) && (['sound', 'none'].indexOf(opts.bellStyle) > -1)) ? opts.bellStyle : appConfig.terminal.bellStyle
+ },
+ allowreplay: appConfig.options.challengeButton || (validator.isBoolean(opts.allowreplay + '') ? myutil.parseBool(opts.allowreplay) : false),
+ allowreauth: appConfig.options.allowreauth || false,
+ mrhsession: ((validator.isAlphanumeric(opts.mrhsession + '') && opts.mrhsession) ? opts.mrhsession : 'none'),
+ serverlog: {
+ client: appConfig.serverlog.client || false,
+ server: appConfig.serverlog.server || false
+ },
+ readyTimeout: (validator.isInt(opts.readyTimeout + '', { min: 1, max: 300000 }) &&
+ opts.readyTimeout) || appConfig.ssh.readyTimeout
+ }
+ if (sshSessionConfig.header.name) validator.escape(sshSessionConfig.header.name)
+ if (sshSessionConfig.header.background) validator.escape(sshSessionConfig.header.background)
+
+ return sshSessionConfig
+}
diff --git a/app/server/util.js b/app/server/util.js
index 0ca3dca01..f2fd2387d 100644
--- a/app/server/util.js
+++ b/app/server/util.js
@@ -1,18 +1,16 @@
-'use strict'
/* jshint esversion: 6, asi: true, node: true */
// util.js
-// private
require('colors') // allow for color property extensions in log messages
-var debug = require('debug')('WebSSH2')
-var Auth = require('basic-auth')
+const debug = require('debug')('WebSSH2')
+const Auth = require('basic-auth')
-let defaultCredentials = {username: null, password: null, privatekey: null};
+const defaultCredentials = { username: null, password: null, privatekey: null }
exports.setDefaultCredentials = function (username, password, privatekey) {
- defaultCredentials.username = username
- defaultCredentials.password = password
- defaultCredentials.privatekey = privatekey
+ defaultCredentials.username = username
+ defaultCredentials.password = password
+ defaultCredentials.privatekey = privatekey
}
exports.basicAuth = function basicAuth (req, res, next) {
@@ -24,11 +22,11 @@ exports.basicAuth = function basicAuth (req, res, next) {
' and password ' + ((myAuth.pass) ? 'exists'.yellow.bold.underline
: 'is blank'.underline.red.bold))
} else {
- req.session.username = defaultCredentials.username;
- req.session.userpassword = defaultCredentials.password;
- req.session.privatekey = defaultCredentials.privatekey;
+ req.session.username = defaultCredentials.username
+ req.session.userpassword = defaultCredentials.password
+ req.session.privatekey = defaultCredentials.privatekey
}
- if ( (!req.session.userpassword) && (!req.session.privatekey) ) {
+ if ((!req.session.userpassword) && (!req.session.privatekey)) {
res.statusCode = 401
debug('basicAuth credential request (401)')
res.setHeader('WWW-Authenticate', 'Basic realm="WebSSH"')
@@ -42,3 +40,10 @@ exports.basicAuth = function basicAuth (req, res, next) {
exports.parseBool = function parseBool (str) {
return (str.toLowerCase() === 'true')
}
+
+exports.uuidv4 = function uuidv4 () {
+ return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) {
+ var r = Math.random() * 16 | 0; var v = c === 'x' ? r : (r & 0x3 | 0x8)
+ return v.toString(16)
+ })
+}