Compare commits

...

17 commits

Author SHA1 Message Date
Bill Church
8636790bb9 docs: reorganize and update 2021-05-20 16:37:12 -04:00
Bill Church
e74c40b005 docs: update changelog 2021-05-20 16:35:58 -04:00
Bill Church
2f8ba2d9e9 bump ver 2021-05-20 16:35:47 -04:00
Bill Church
c7851da84a chore: take same config options from env and override
port, listen ip, session name, session secret
2021-05-20 16:35:34 -04:00
Bill Church
f36af08d36 chore: remove auto port increment 2021-05-20 16:32:57 -04:00
Bill Church
6aca5b414f chore: modify webssh2 debug keyword 2021-05-20 13:18:59 -04:00
Bill Church
df2a896139 chore: refactor ./app/server/app.js
safeShutdown and setupSession
2021-05-20 12:47:20 -04:00
Bill Church
dc3aa3f91f chore: update config defaults and samples 2021-05-20 12:46:19 -04:00
Bill Church
039e1cfc62 chore: move linting exceptions to .eslintrc.json 2021-05-20 12:41:43 -04:00
Bill Church
b05c15c25e Merge branches 'dev' and 'dev' of github.com:billchurch/webssh2 into dev 2021-05-19 19:23:31 +00:00
Bill Church
ffc140096c feat: move authentication system to pasport.js 2021-05-19 19:23:22 +00:00
Bill Church
02181406d8
Merge branch 'master' into dev 2021-05-19 12:22:56 -04:00
Bill Church
7aad9624d6 chore: udpate dev tools 2021-05-19 16:19:46 +00:00
Bill Church
0ca24c0d22 build tools update 2021-05-19 16:18:25 +00:00
Bill Church
e7713cc972 chore: update ci 2021-05-19 16:11:30 +00:00
Bill Church
9a3c26b71f ci tools updates 2021-05-19 15:31:43 +00:00
Bill Church
1475288109 chore: ci tools updates 2021-05-19 15:31:31 +00:00
16 changed files with 520 additions and 374 deletions

View file

@ -2,3 +2,5 @@ language: node_js
node_js:
- 14
- 16
before_install:
- npm i -g snyk

203
CONFIGURATION.md Normal file
View file

@ -0,0 +1,203 @@
# CONFIGURATION
Configuration options can be set in multiple ways.
- [Environment Options](#environment-options)
- [GET POST Options](#get-post-options)
- [Headers](#headers)
- [Config File Options](#config-file-options)
All configuration variables may be set by `./app/config.js` some may be set by GET/POST vars and some by environment vars.
## Environment Options
Environment variables take priority over anything in `./app/config.json` if it exists.
* **LISTEN_IP** - _string_ - IP address node should listen on for client connections, defaults to `127.0.0.1`
* **PORT** - _integer_ - Port node should listen on for client connections, defaults to `2222`
* **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.
## GET POST Options
### /ssh/host
* **port=** - _integer_ - port of SSH server (defaults to 22)
* **header=** - _string_ - optional header to display on page
* **headerBackground=** - _string_ - optional background color of header to display on page
* **readyTimeout=** - _integer_ - How long (in milliseconds) to wait for the SSH handshake to complete. **Default:** 20000. **Enforced Values:** Min: 1, Max: 300000
* **cursorBlink** - _boolean_ - Cursor blinks (true), does not (false) **Default:** true.
* **scrollback** - _integer_ - Lines in the scrollback buffer. **Default:** 10000. **Enforced Values:** Min: 1, Max: 200000
* **tabStopWidth** - _integer_ - Tab stops at _n_ characters **Default:** 8. **Enforced Values:** Min: 1, Max: 100
* **bellStyle** - _string_ - Style of terminal bell: ("sound"|"none"). **Default:** "sound". **Enforced Values:** "sound", "none"
### /ssh/login/host
Above plus
* **username=** - _string_ - required username (either GET or POST)
* **password=** - _string_ - requied password (either GET or POST)
## Headers
* **allowreplay** - _boolean_ - Allow use of password replay feature, example `allowreplay: true`
* **mrhsession** - _string_ - Can be used to pass APM session for event correlation `mrhsession: abc123`
## 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...
* **listen.ip** - _string_ - IP address node should listen on for client connections, defaults to `127.0.0.1`
* **listen.port** - _integer_ - Port node should listen on for client connections, defaults to `2222`
* **http.origins** - _array_ - COORS origins to allow connections from to socket.io server, defaults to `localhost:2222`. Changed in 0.3.1, to enable previous, less secure, default behavior of everything use `*:*` (not recommended). Check [#240](../../issues/240)
* **user.name** - _string_ - Specify user name to authenticate with. In normal cases this should be left to the default `null` setting.
* **user.password** - _string_ - Specify password to authenticate with. In normal cases this should be left to the default `null` setting.
* **user.overridebasic** - _boolean_ - When set to `true` ignores `Authorization: Basic` header sent from client and use credentials defined in `user.name` and `user.password` instead. Defaults to `false`. [issue 242](../../issues/242) for more information.
* **ssh.host** - _string_ - Specify host to connect to. May be either hostname or IP address. Defaults to `null`.
* **ssh.port** - _integer_ - Specify SSH port to connect to, defaults to `22`
* **ssh.term** - _string_ - Specify terminal emulation to use, defaults to `xterm-color`
* **ssh.readyTimeout** - _integer_ - How long (in milliseconds) to wait for the SSH handshake to complete. **Default:** 20000.
* **ssh.keepaliveInterval** - _integer_ - How often (in milliseconds) to send SSH-level keepalive packets to the server (in a similar way as OpenSSH's ServerAliveInterval config option). Set to 0 to disable. **Default:** 120000.
* **ssh.keepaliveCountMax** - _integer_ - How many consecutive, unanswered SSH-level keepalive packets that can be sent to the server before disconnection (similar to OpenSSH's ServerAliveCountMax config option). **Default:** 10.
* **allowedSubnets** - _array_ - A list of subnets that the server is allowed to connect to via SSH. An empty array means all subnets are permitted; no restriction. **Default:** empty array.
* **terminal.cursorBlink** - _boolean_ - Cursor blinks (true), does not (false) **Default:** true.
* **terminal.scrollback** - _integer_ - Lines in the scrollback buffer. **Default:** 10000.
* **terminal.tabStopWidth** - _integer_ - Tab stops at _n_ characters **Default:** 8.
* **terminal.bellStyle** - _string_ - Style of terminal bell: (sound|none). **Default:** "sound".
* **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.
* **session.resave** - _boolean_ - Secret key for cookie encryption. You should change this in production.
* **session.saveUninitialized** - _boolean_ - Forces a session that is "uninitialized" to be saved to the store. A session is uninitialized when it is new but not modified. **Default:** `false`
* **session.unset** - _string_ - `destroy` or `keep` Control the result of unsetting req.session (through delete, setting to null, etc.). **Default:** `destroy`
* **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.allowreauth** - _boolean_ - Reauth button. This option creates an option to provide a button to create a new session with new credentials. See [issue 51](../../issues/51) and [pull 85](../../pull/85) for more detail.
* **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 **(node v0.11.14 or newer)**
2. ecdh-sha2-nistp384 **(node v0.11.14 or newer)**
3. ecdh-sha2-nistp521 **(node v0.11.14 or newer)**
4. diffie-hellman-group-exchange-sha256 **(node v0.11.12 or newer)**
5. diffie-hellman-group14-sha1
* Supported values:
* ecdh-sha2-nistp256 **(node v0.11.14 or newer)**
* ecdh-sha2-nistp384 **(node v0.11.14 or newer)**
* ecdh-sha2-nistp521 **(node v0.11.14 or newer)**
* diffie-hellman-group-exchange-sha256 **(node v0.11.12 or newer)**
* diffie-hellman-group14-sha1
* diffie-hellman-group-exchange-sha1 **(node v0.11.12 or newer)**
* diffie-hellman-group1-sha1
* **cipher** - _array_ - Ciphers.
* Default values:
1. aes128-ctr
2. aes192-ctr
3. aes256-ctr
4. aes128-gcm **(node v0.11.12 or newer)**
5. aes128-gcm@openssh.com **(node v0.11.12 or newer)**
6. aes256-gcm **(node v0.11.12 or newer)**
7. aes256-gcm@openssh.com **(node v0.11.12 or newer)**
* Supported values:
* aes128-ctr
* aes192-ctr
* aes256-ctr
* aes128-gcm **(node v0.11.12 or newer)**
* aes128-gcm@openssh.com **(node v0.11.12 or newer)**
* aes256-gcm **(node v0.11.12 or newer)**
* aes256-gcm@openssh.com **(node v0.11.12 or newer)**
* 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
* 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
* **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.
* **accesslog** - _boolean_ - http style access logging to console.log, default: false
* **safeShutdownDuration** - _integer_ - maximum delay, in seconds, given to users before the server stops when doing a safe shutdown

View file

@ -1,4 +1,17 @@
# Change Log
## 0.5.0 [TBD]
### BREAKING
- Debug environment for webssh2 changed from `WebSSH2` to `webssh2`
- Remove auto port increment from `./app/index.js` (f36af08)
### Changes
- Credentials are now retreived using passport.js (should make it easier to expand to other methods in the future) for #247
- Refactored ./app/server/app.js
- Safe shutdown feature moved to ./app/server/safeShutdown.js and re-worked
- Session setup moved out of `/ssh/host` route into ./app/server/sessionSetup.js for readability and reuse for #247
### Added
- New route `/ssh/login/host` to gather `username` and `password` credentials from either GET or POST
## 0.4.0 [20210519]
### BREAKING
- Disabled ssh.serverlog.client option, this disables the POC which allowed for logging of the data sent between the client/server to the console.log.

176
README.md
View file

@ -2,7 +2,9 @@
<ignorestart>
[![GitHub version](https://badge.fury.io/gh/billchurch%2Fwebssh2.svg)](https://badge.fury.io/gh/billchurch%2Fwebssh2)
[![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)
[![Buy Me A Coffee](https://www.buymeacoffee.com/assets/img/custom_images/orange_img.png)](https://www.buymeacoffee.com/billchurch)
@ -70,176 +72,8 @@ docker run --name webssh2 -d -p 2222:2222 -v `pwd`/app/config.json:/usr/src/conf
<ignoreend>
# Options
## GET request vars
* **port=** - _integer_ - port of SSH server (defaults to 22)
* **header=** - _string_ - optional header to display on page
* **headerBackground=** - _string_ - optional background color of header to display on page
* **readyTimeout=** - _integer_ - How long (in milliseconds) to wait for the SSH handshake to complete. **Default:** 20000. **Enforced Values:** Min: 1, Max: 300000
* **cursorBlink** - _boolean_ - Cursor blinks (true), does not (false) **Default:** true.
* **scrollback** - _integer_ - Lines in the scrollback buffer. **Default:** 10000. **Enforced Values:** Min: 1, Max: 200000
* **tabStopWidth** - _integer_ - Tab stops at _n_ characters **Default:** 8. **Enforced Values:** Min: 1, Max: 100
* **bellStyle** - _string_ - Style of terminal bell: ("sound"|"none"). **Default:** "sound". **Enforced Values:** "sound", "none"
## Headers
* **allowreplay** - _boolean_ - Allow use of password replay feature, example `allowreplay: true`
* **mrhsession** - _string_ - Can be used to pass APM session for event correlation `mrhsession: abc123`
## 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...
* **listen.ip** - _string_ - IP address node should listen on for client connections, defaults to `127.0.0.1`
* **listen.port** - _integer_ - Port node should listen on for client connections, defaults to `2222`
* **http.origins** - _array_ - COORS origins to allow connections from to socket.io server, defaults to `localhost:2222`. Changed in 0.3.1, to enable previous, less secure, default behavior of everything use `*:*` (not recommended). Check [#240](../../issues/240)
* **user.name** - _string_ - Specify user name to authenticate with. In normal cases this should be left to the default `null` setting.
* **user.password** - _string_ - Specify password to authenticate with. In normal cases this should be left to the default `null` setting.
* **user.overridebasic** - _boolean_ - When set to `true` ignores `Authorization: Basic` header sent from client and use credentials defined in `user.name` and `user.password` instead. Defaults to `false`. [issue 242](../../issues/242) for more information.
* **ssh.host** - _string_ - Specify host to connect to. May be either hostname or IP address. Defaults to `null`.
* **ssh.port** - _integer_ - Specify SSH port to connect to, defaults to `22`
* **ssh.term** - _string_ - Specify terminal emulation to use, defaults to `xterm-color`
* **ssh.readyTimeout** - _integer_ - How long (in milliseconds) to wait for the SSH handshake to complete. **Default:** 20000.
* **ssh.keepaliveInterval** - _integer_ - How often (in milliseconds) to send SSH-level keepalive packets to the server (in a similar way as OpenSSH's ServerAliveInterval config option). Set to 0 to disable. **Default:** 120000.
* **ssh.keepaliveCountMax** - _integer_ - How many consecutive, unanswered SSH-level keepalive packets that can be sent to the server before disconnection (similar to OpenSSH's ServerAliveCountMax config option). **Default:** 10.
* **allowedSubnets** - _array_ - A list of subnets that the server is allowed to connect to via SSH. An empty array means all subnets are permitted; no restriction. **Default:** empty array.
* **terminal.cursorBlink** - _boolean_ - Cursor blinks (true), does not (false) **Default:** true.
* **terminal.scrollback** - _integer_ - Lines in the scrollback buffer. **Default:** 10000.
* **terminal.tabStopWidth** - _integer_ - Tab stops at _n_ characters **Default:** 8.
* **terminal.bellStyle** - _string_ - Style of terminal bell: (sound|none). **Default:** "sound".
* **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.
* **options.allowreauth** - _boolean_ - Reauth button. This option creates an option to provide a button to create a new session with new credentials. See [issue 51](../../issues/51) and [pull 85](../../pull/85) for more detail.
* **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 **(node v0.11.14 or newer)**
2. ecdh-sha2-nistp384 **(node v0.11.14 or newer)**
3. ecdh-sha2-nistp521 **(node v0.11.14 or newer)**
4. diffie-hellman-group-exchange-sha256 **(node v0.11.12 or newer)**
5. diffie-hellman-group14-sha1
* Supported values:
* ecdh-sha2-nistp256 **(node v0.11.14 or newer)**
* ecdh-sha2-nistp384 **(node v0.11.14 or newer)**
* ecdh-sha2-nistp521 **(node v0.11.14 or newer)**
* diffie-hellman-group-exchange-sha256 **(node v0.11.12 or newer)**
* diffie-hellman-group14-sha1
* diffie-hellman-group-exchange-sha1 **(node v0.11.12 or newer)**
* diffie-hellman-group1-sha1
* **cipher** - _array_ - Ciphers.
* Default values:
1. aes128-ctr
2. aes192-ctr
3. aes256-ctr
4. aes128-gcm **(node v0.11.12 or newer)**
5. aes128-gcm@openssh.com **(node v0.11.12 or newer)**
6. aes256-gcm **(node v0.11.12 or newer)**
7. aes256-gcm@openssh.com **(node v0.11.12 or newer)**
* Supported values:
* aes128-ctr
* aes192-ctr
* aes256-ctr
* aes128-gcm **(node v0.11.12 or newer)**
* aes128-gcm@openssh.com **(node v0.11.12 or newer)**
* aes256-gcm **(node v0.11.12 or newer)**
* aes256-gcm@openssh.com **(node v0.11.12 or newer)**
* 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
* 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
* **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.
* **accesslog** - _boolean_ - http style access logging to console.log, default: false
* **safeShutdownDuration** - _integer_ - maximum delay, in seconds, given to users before the server stops when doing a safe shutdown
# Configuration
see [CONFIGURATION](configuration.md)
# 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.

View file

@ -19,6 +19,8 @@
"prettier"
],
"rules": {
"prettier/prettier": ["error"]
"prettier/prettier": ["error"],
"no-unused-expressions": ["error", { "allowShortCircuit": true, "allowTernary": true }],
"no-console": ["error", { "allow": ["warn", "error"] }]
}
}

View file

@ -35,7 +35,11 @@
},
"session": {
"name": "WebSSH2",
"secret": "mysecret"
"secret": "mysecret",
"resave": false,
"saveUninitialized": false,
"unset": "destroy"
},
},
"options": {
"challengeButton": true,

View file

@ -1,5 +1,3 @@
/* eslint no-console: ["error", { allow: ["warn", "error"] }] */
/* jshint esversion: 6, asi: true, node: true */
/*
* index.js
*
@ -17,14 +15,5 @@ server.listen({ host: config.listen.ip, port: config.listen.port });
console.log(`WebSSH2 service listening on ${config.listen.ip}:${config.listen.port}`);
server.on('error', (err) => {
if (err.code === 'EADDRINUSE') {
config.listen.port += 1;
console.warn(`WebSSH2 Address in use, retrying on port ${config.listen.port}`);
setTimeout(() => {
server.listen(config.listen.port);
}, 250);
} else {
// eslint-disable-next-line no-console
console.log(`WebSSH2 server.listen ERROR: ${err.code}`);
}
console.error(`WebSSH2 server.listen ERROR: ${err.code}`);
});

45
app/package-lock.json generated
View file

@ -1,6 +1,6 @@
{
"name": "webssh2",
"version": "0.4.0",
"version": "0.5.0-dev-1",
"lockfileVersion": 1,
"requires": true,
"dependencies": {
@ -6332,6 +6332,44 @@
"resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
"integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ=="
},
"passport": {
"version": "0.4.1",
"resolved": "https://registry.npmjs.org/passport/-/passport-0.4.1.tgz",
"integrity": "sha512-IxXgZZs8d7uFSt3eqNjM9NQ3g3uQCW5avD8mRNoXV99Yig50vjuaez6dQK2qC0kVWPRTujxY0dWgGfT09adjYg==",
"requires": {
"passport-strategy": "1.x.x",
"pause": "0.0.1"
}
},
"passport-custom": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/passport-custom/-/passport-custom-1.1.1.tgz",
"integrity": "sha512-/2m7jUGxmCYvoqenLB9UrmkCgPt64h8ZtV+UtuQklZ/Tn1NpKBeOorCYkB/8lMRoiZ5hUrCoMmDtxCS/d38mlg==",
"requires": {
"passport-strategy": "1.x.x"
}
},
"passport-http": {
"version": "0.3.0",
"resolved": "https://registry.npmjs.org/passport-http/-/passport-http-0.3.0.tgz",
"integrity": "sha1-juU9Q4C+nGDfIVGSUCmCb3cRVgM=",
"requires": {
"passport-strategy": "1.x.x"
}
},
"passport-local": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/passport-local/-/passport-local-1.0.0.tgz",
"integrity": "sha1-H+YyaMkudWBmJkN+O5BmYsFbpu4=",
"requires": {
"passport-strategy": "1.x.x"
}
},
"passport-strategy": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/passport-strategy/-/passport-strategy-1.0.0.tgz",
"integrity": "sha1-tVOaqPwiWj0a0XlHbd8ja0QPUuQ="
},
"path-exists": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz",
@ -6393,6 +6431,11 @@
"integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==",
"dev": true
},
"pause": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/pause/-/pause-0.0.1.tgz",
"integrity": "sha1-HUCLP9t2kjuVQ9lvtMnf1TXZy10="
},
"peek-stream": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/peek-stream/-/peek-stream-1.1.3.tgz",

View file

@ -1,6 +1,6 @@
{
"name": "webssh2",
"version": "0.4.0",
"version": "0.5.0-dev-1",
"ignore": [
".gitignore"
],
@ -39,6 +39,10 @@
"express": "~4.17.1",
"express-session": "~1.17.1",
"morgan": "~1.10.0",
"passport": "^0.4.1",
"passport-custom": "^1.1.1",
"passport-http": "^0.3.0",
"passport-local": "^1.0.0",
"read-config-ng": "^3.0.2",
"serve-favicon": "^2.5.0",
"socket.io": "^4.1.1",

View file

@ -1,71 +1,105 @@
/* jshint esversion: 6, asi: true, node: true */
/* eslint no-unused-expressions: ["error", { "allowShortCircuit": true, "allowTernary": true }],
no-console: ["error", { allow: ["warn", "error"] }] */
// app.js
// eslint-disable-next-line import/order
const config = require('./config');
const path = require('path');
const debug = require('debug')('webssh2');
require('colors');
// allow for color property extensions in log messages
const nodeRoot = path.dirname(require.main.filename);
const publicPath = path.join(nodeRoot, 'client', 'public');
const express = require('express');
const logger = require('morgan');
const passport = require('passport');
const { BasicStrategy } = require('passport-http');
const CustomStrategy = require('passport-custom').Strategy;
const LocalStrategy = require('passport-local').Strategy;
const app = express();
const server = require('http').Server(app);
const validator = require('validator');
const favicon = require('serve-favicon');
const io = require('socket.io')(server, {
serveClient: false,
path: '/ssh/socket.io',
origins: config.http.origins,
});
const session = require('express-session')({
secret: config.session.secret,
name: config.session.name,
resave: true,
saveUninitialized: false,
unset: 'destroy',
});
const session = require('express-session')(config.session);
const { setupSession } = require('./setupSession');
const appSocket = require('./socket');
const expressOptions = require('./expressOptions');
const myutil = require('./util');
const safeShutdown = require('./safeShutdown');
myutil.setDefaultCredentials(
config.user.name,
config.user.password,
config.user.privatekey,
config.user.overridebasic
// Static credentials strategy
// when config.user.overridebasic is true, those credentials
// are used instead of HTTP basic auth.
passport.use(
'overridebasic',
new CustomStrategy((req, done) => {
if (config.user.overridebasic) {
const user = {
username: config.user.name,
password: config.user.password,
privatekey: config.user.privatekey,
};
debug(
`overridebasic username: ${user.username.yellow.bold.underline} and password ${
user.password ? 'exists'.yellow.bold.underline : 'is blank'.underline.red.bold
}`
);
return done(null, user);
}
return done(null, false);
})
);
// safe shutdown
let shutdownMode = false;
let shutdownInterval = 0;
let connectionCount = 0;
// eslint-disable-next-line consistent-return
function safeShutdownGuard(req, res, next) {
if (shutdownMode) {
res.status(503).end('Service unavailable: Server shutting down');
} else {
return next();
}
}
// clean stop
function stopApp(reason) {
shutdownMode = false;
// eslint-disable-next-line no-console
if (reason) console.log(`Stopping: ${reason}`);
if (shutdownInterval) clearInterval(shutdownInterval);
io.close();
server.close();
}
// Basic auth strategy
passport.use(
new BasicStrategy((username, password, done) => {
const user = {
username,
password,
};
debug(
`HTTP Basic username: ${user.username.yellow.bold.underline} and password ${
user.password ? 'exists'.yellow.bold.underline : 'is blank'.underline.red.bold
}`
);
return done(null, user);
})
);
// Local auth strategy
// for taking credentials from GET/POST
passport.use(
new LocalStrategy((username, password, done) => {
const user = {
username,
password,
};
debug(
`HTTP GET/POST username: ${user.username.yellow.bold.underline} and password ${
user.password ? 'exists'.yellow.bold.underline : 'is blank'.underline.red.bold
}`
);
return done(null, user);
})
);
passport.serializeUser((user, done) => {
done(null, user);
});
passport.deserializeUser((user, done) => {
done(null, user);
});
module.exports = { server, config };
// express
app.use(safeShutdownGuard);
app.use(safeShutdown.safeShutdownGuard);
app.use(session);
app.use(myutil.basicAuth);
app.use(passport.initialize());
app.use(passport.session());
if (config.accesslog) app.use(logger('common'));
app.disable('x-powered-by');
@ -75,8 +109,12 @@ app.use('/ssh', express.static(publicPath, expressOptions));
// favicon from root if being pre-fetched by browser to prevent a 404
app.use(favicon(path.join(publicPath, 'favicon.ico')));
// this is currently broken due to the way passport works with Basic Auth...
// maybe this should never have worked in the first place
app.get('/ssh/reauth', (req, res) => {
const r = req.headers.referer || '/';
req.logout();
req.session.destroy();
res
.status(401)
.send(
@ -84,71 +122,25 @@ app.get('/ssh/reauth', (req, res) => {
);
});
// eslint-disable-next-line complexity
app.get('/ssh/host/:host?', (req, res) => {
res.sendFile(path.join(path.join(publicPath, 'client.htm')));
// capture, assign, and validate variables
req.session.ssh = {
host:
config.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),
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,
};
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);
});
// This route allows for collection of credentials from POST/GET
app.get(
'/ssh/login/host/:host?',
passport.authenticate(['overridebasic', 'local'], { session: true }),
(req, res) => {
setupSession(req, config);
res.sendFile(path.join(path.join(publicPath, 'client.htm')));
}
);
// This route allows for collection of credentials from HTTP Basic
app.get(
'/ssh/host/:host?',
passport.authenticate(['overridebasic', 'basic'], { session: true }),
(req, res) => {
setupSession(req, config);
res.sendFile(path.join(path.join(publicPath, 'client.htm')));
}
);
// express error handling
app.use((req, res) => {
@ -166,41 +158,17 @@ io.on('connection', appSocket);
// socket.io
// expose express session with socket.request.session
io.use((socket, next) => {
socket.request.res ? session(socket.request, socket.request.res, next) : next(next); // eslint disable-line
socket.request.res ? session(socket.request, socket.request.res, next) : next(next);
});
io.on('connection', (socket) => {
connectionCount += 1;
socket.on('disconnect', () => {
connectionCount -= 1;
if (connectionCount <= 0 && shutdownMode) {
stopApp('All clients disconnected');
if (io.of('/').sockets.size <= 1 && safeShutdown.shutdownMode) {
safeShutdown.stopApp(io, server, 'All clients disconnected');
}
});
});
// trap SIGTERM and SIGINT (CTRL-C) and handle shutdown gracefully
const signals = ['SIGTERM', 'SIGINT'];
signals.forEach((signal) =>
process.on(signal, () => {
if (shutdownMode) stopApp('Safe shutdown aborted, force quitting');
else if (connectionCount > 0) {
let remainingSeconds = config.safeShutdownDuration;
shutdownMode = true;
const message =
connectionCount === 1 ? ' client is still connected' : ' clients are still connected';
console.error(connectionCount + message);
console.error(`Starting a ${remainingSeconds} seconds countdown`);
console.error('Press Ctrl+C again to force quit');
shutdownInterval = setInterval(() => {
remainingSeconds -= 1;
if (remainingSeconds <= 0) {
stopApp('Countdown is over');
} else {
io.sockets.emit('shutdownCountdownUpdate', remainingSeconds);
}
}, 1000);
} else stopApp();
})
);
signals.forEach((signal) => process.on(signal, () => safeShutdown.doShutdown(io, server, config)));

View file

@ -1,5 +1,5 @@
/* eslint no-unused-expressions: ["error", { "allowShortCircuit": true, "allowTernary": true }],
no-console: ["error", { allow: ["warn", "error"] }] */
// config.js
const fs = require('fs');
const path = require('path');
@ -24,6 +24,8 @@ let config = {
ssh: {
host: null,
port: 22,
localAddress: null,
localPort: null,
term: 'xterm-color',
readyTimeout: 20000,
keepaliveInterval: 120000,
@ -43,6 +45,9 @@ let config = {
session: {
name: 'WebSSH2',
secret: 'mysecret',
resave: false,
saveUninitialized: false,
unset: 'destroy',
},
options: {
challengeButton: true,
@ -78,6 +83,14 @@ let config = {
safeShutdownDuration: 300,
};
// We want our environment varaibles to take priority if they're set
function setOverrides() {
config.listen.ip = process.env.LISTEN_IP || config.listen.ip;
config.listen.port = process.env.PORT || config.listen.port;
config.session.name = process.env.SESSION_NAME || config.session.name;
config.session.secret = process.env.SESSION_SECRET || config.session.secret;
}
// test if config.json exists, if not provide error message but try to run anyway
try {
if (fs.existsSync(configPath)) {
@ -85,11 +98,14 @@ try {
console.info(`WebSSH2 service reading config from: ${configPath}`);
// eslint-disable-next-line global-require
config = require('read-config-ng')(configPath);
// setup overrides for environment variables
setOverrides();
} else {
console.error(
`\n\nERROR: Missing config.json for WebSSH2. Current config: ${JSON.stringify(config)}`
);
console.error('\n See config.json.sample for details\n\n');
setOverrides();
}
} catch (err) {
console.error(

View file

@ -1,3 +1,4 @@
// expressOptions.js
module.exports = {
dotfiles: 'ignore',
etag: false,

View file

@ -0,0 +1,47 @@
// safeShutdown.js
// safe shutdown
let shutdownMode = false;
let shutdownInterval = 0;
// eslint-disable-next-line consistent-return
exports.safeShutdownGuard = (req, res, next) => {
if (shutdownMode) {
res.status(503).end('Service unavailable: Server shutting down');
} else {
return next();
}
};
// clean stop
const stopApp = (io, server, reason) => {
shutdownMode = false;
// eslint-disable-next-line no-console
if (reason) console.log(`Stopping: ${reason}`);
if (shutdownInterval) clearInterval(shutdownInterval);
return process.exit(0);
};
exports.doShutdown = (io, server, config) => {
if (shutdownMode) stopApp(io, server, 'Safe shutdown aborted, force quitting');
else if (io.of('/').sockets.size > 0) {
let remainingSeconds = config.safeShutdownDuration;
shutdownMode = true;
const message =
io.of('/').sockets.size === 1 ? ' client is still connected' : ' clients are still connected';
console.error(io.of('/').sockets.size + message);
console.error(`Starting a ${remainingSeconds} seconds countdown`);
console.error('Press Ctrl+C again to force quit');
shutdownInterval = setInterval(() => {
remainingSeconds -= 1;
if (remainingSeconds <= 0) {
console.error('shutdown remaining seconds 0');
stopApp('Countdown is over');
} else {
io.sockets.emit('shutdownCountdownUpdate', remainingSeconds);
}
}, 1000);
} else stopApp(io, server);
};
exports.stopApp = stopApp;
exports.shutdownMode = shutdownMode;

View file

@ -0,0 +1,71 @@
// setupSession.js
const validator = require('validator');
const myutil = require('./util');
// private
// capture, assign, and validate variables for later use
exports.setupSession = function setupSession(req, config) {
req.session.username = req.user.username;
req.session.userpassword = req.user.password;
req.session.ssh = {
host:
config.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),
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,
};
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);
};

View file

@ -1,7 +1,3 @@
/* eslint-disable complexity */
/* eslint no-unused-expressions: ["error", { "allowShortCircuit": true, "allowTernary": true }],
no-console: ["error", { allow: ["warn", "error"] }] */
/* jshint esversion: 6, asi: true, node: true */
// socket.js
// private

View file

@ -1,53 +1,6 @@
/* jshint esversion: 6, asi: true, node: true */
// util.js
// private
require('colors'); // allow for color property extensions in log messages
const debug = require('debug')('WebSSH2');
const Auth = require('basic-auth');
const defaultCredentials = { username: null, password: null, privatekey: null };
exports.setDefaultCredentials = function setDefaultCredentials(
username,
password,
privatekey,
overridebasic
) {
defaultCredentials.username = username;
defaultCredentials.password = password;
defaultCredentials.privatekey = privatekey;
defaultCredentials.overridebasic = overridebasic;
};
exports.basicAuth = function basicAuth(req, res, next) {
const myAuth = Auth(req);
// If Authorize: Basic header exists and the password isn't blank
// AND config.user.overridebasic is false, extract basic credentials
// from client
if (myAuth && myAuth.pass !== '' && !defaultCredentials.overridebasic) {
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
}`
);
} else {
req.session.username = defaultCredentials.username;
req.session.userpassword = defaultCredentials.password;
req.session.privatekey = defaultCredentials.privatekey;
}
if (!req.session.userpassword && !req.session.privatekey) {
res.statusCode = 401;
debug('basicAuth credential request (401)');
res.setHeader('WWW-Authenticate', 'Basic realm="WebSSH"');
res.end('Username and password required for web SSH service.');
return;
}
next();
};
// takes a string, makes it boolean (true if the string is true, false otherwise)
exports.parseBool = function parseBool(str) {
return str.toLowerCase() === 'true';