0.4.0 Release (#246)

* feat: upgrade to socket.io 4.1.1 #242

* chore: lint ./app/client/src/js/index.js #242

* chore: eslint disable global Blob warning #242

* chore: lint ./app/index.js #242

* chore: lint ./app/server/app.js #242

* chore: setup eslint and airbnb rules disable standard #242

* Delete package-lock-old.json

* chore: lint ./app/index.js #242

* feat: implement alpine docker image from #213

* chore: lint ./app/server/app.js still TODO for stop function #242

* chore: lint ./app/server/util.js #242

* chore: lint ./app/server/app.js reorg socket and safe shutdown

* chore: grammar / spelling

* chore: fix some misplaced next returns in some Express routes #242

* chore: lint ./app/server/socket.js #242

* chore: bump version in ./app/package.json #242

* docs: update docs for 0.4.0 #242

* chore: update package-lock.json

* chore: install Prettier code linter #242

* chore: linting for Prettier #242

* chore: lint ./app/client/src/js/index.js #242

* chore: client linting #242

* Update package-lock.json

* chore: repackage wbssh2 bundle for testing #242

* chore: convert ./app/client/src/js/index.js to typescript #242

* chore: remove html rendering from node

* Update tsconfig.json

* Update tsconfig.json

* Delete index.js

* Update ChangeLog.md

* chore: config for development container #242

* Update BUILDING.md

* feat: pull in #234 staged for 0.4.0 #242

* docs: update changelog

* update package.json

* chore: split config from app/server/app.js #242

* chore: version bump

* chore: consistency

* feat: overridebasic fixes #243 included for #242

* chore: remove serverlog code

* docs: update changelog
This commit is contained in:
Bill Church 2021-05-19 10:22:29 -04:00 committed by GitHub
parent 379335b9e7
commit 6bc9ffe2ed
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
31 changed files with 1898 additions and 15248 deletions

16
.devcontainer/Dockerfile Normal file
View file

@ -0,0 +1,16 @@
# See here for image contents: https://github.com/microsoft/vscode-dev-containers/tree/v0.177.0/containers/typescript-node/.devcontainer/base.Dockerfile
# [Choice] Node.js version: 16, 14, 12
ARG VARIANT="16-buster"
FROM mcr.microsoft.com/vscode/devcontainers/typescript-node:0-${VARIANT}
# [Optional] Uncomment this section to install additional OS packages.
# RUN apt-get update && export DEBIAN_FRONTEND=noninteractive \
# && apt-get -y install --no-install-recommends <your-package-list-here>
# [Optional] Uncomment if you want to install an additional version of node using nvm
# ARG EXTRA_NODE_VERSION=10
# RUN su node -c "source /usr/local/share/nvm/nvm.sh && nvm install ${EXTRA_NODE_VERSION}"
# [Optional] Uncomment if you want to install more global node packages
# RUN su node -c "npm install -g <your-package-list -here>"

View file

@ -0,0 +1,31 @@
// For format details, see https://aka.ms/devcontainer.json. For config options, see the README at:
// https://github.com/microsoft/vscode-dev-containers/tree/v0.177.0/containers/typescript-node
{
"name": "Node.js & TypeScript",
"build": {
"dockerfile": "Dockerfile",
// Update 'VARIANT' to pick a Node version: 12, 14, 16
"args": {
"VARIANT": "14"
}
},
// Set *default* container specific settings.json values on container create.
"settings": {
"terminal.integrated.shell.linux": "/bin/bash"
},
// Add the IDs of extensions you want installed when the container is created.
"extensions": [
"dbaeumer.vscode-eslint"
],
// Use 'forwardPorts' to make a list of ports inside the container available locally.
// "forwardPorts": [],
// Use 'postCreateCommand' to run commands after the container is created.
// "postCreateCommand": "yarn install",
// Comment out connect as root instead. More info: https://aka.ms/vscode-remote/containers/non-root.
"remoteUser": "node"
}

View file

@ -1 +0,0 @@
**/*{.,-}min.js

View file

@ -1,277 +0,0 @@
---
parserOptions:
sourceType: module
ecmaFeatures:
jsx: true
env:
amd: true
browser: true
es6: true
jquery: true
node: true
# http://eslint.org/docs/rules/
rules:
# Possible Errors
no-await-in-loop: off
no-cond-assign: error
no-console: off
no-constant-condition: error
no-control-regex: error
no-debugger: error
no-dupe-args: error
no-dupe-keys: error
no-duplicate-case: error
no-empty-character-class: error
no-empty: error
no-ex-assign: error
no-extra-boolean-cast: error
no-extra-parens: off
no-extra-semi: error
no-func-assign: error
no-inner-declarations:
- error
- functions
no-invalid-regexp: error
no-irregular-whitespace: error
no-negated-in-lhs: error
no-obj-calls: error
no-prototype-builtins: off
no-regex-spaces: error
no-sparse-arrays: error
no-template-curly-in-string: off
no-unexpected-multiline: error
no-unreachable: error
no-unsafe-finally: off
no-unsafe-negation: off
use-isnan: error
valid-jsdoc: off
valid-typeof: error
# Best Practices
accessor-pairs: error
array-callback-return: off
block-scoped-var: off
class-methods-use-this: off
complexity:
- error
- 6
consistent-return: off
curly: off
default-case: off
dot-location: off
dot-notation: off
eqeqeq: error
guard-for-in: error
no-alert: error
no-caller: error
no-case-declarations: error
no-div-regex: error
no-else-return: off
no-empty-function: off
no-empty-pattern: error
no-eq-null: error
no-eval: error
no-extend-native: error
no-extra-bind: error
no-extra-label: off
no-fallthrough: error
no-floating-decimal: off
no-global-assign: off
no-implicit-coercion: off
no-implied-eval: error
no-invalid-this: off
no-iterator: error
no-labels:
- error
- allowLoop: true
allowSwitch: true
no-lone-blocks: error
no-loop-func: error
no-magic-number: off
no-multi-spaces: off
no-multi-str: off
no-native-reassign: error
no-new-func: error
no-new-wrappers: error
no-new: error
no-octal-escape: error
no-octal: error
no-param-reassign: off
no-proto: error
no-redeclare: error
no-restricted-properties: off
no-return-assign: error
no-return-await: off
no-script-url: error
no-self-assign: off
no-self-compare: error
no-sequences: off
no-throw-literal: off
no-unmodified-loop-condition: off
no-unused-expressions: error
no-unused-labels: off
no-useless-call: error
no-useless-concat: error
no-useless-escape: off
no-useless-return: off
no-void: error
no-warning-comments: off
no-with: error
prefer-promise-reject-errors: off
radix: error
require-await: off
vars-on-top: off
wrap-iife: error
yoda: off
# Strict
strict: off
# Variables
init-declarations: off
no-catch-shadow: error
no-delete-var: error
no-label-var: error
no-restricted-globals: off
no-shadow-restricted-names: error
no-shadow: off
no-undef-init: error
no-undef: off
no-undefined: off
no-unused-vars: off
no-use-before-define: off
# Node.js and CommonJS
callback-return: error
global-require: error
handle-callback-err: error
no-mixed-requires: off
no-new-require: off
no-path-concat: error
no-process-env: off
no-process-exit: error
no-restricted-modules: off
no-sync: off
# Stylistic Issues
array-bracket-spacing: off
block-spacing: off
brace-style: off
camelcase: off
capitalized-comments: off
comma-dangle:
- error
- never
comma-spacing: off
comma-style: off
computed-property-spacing: off
consistent-this: off
eol-last: off
func-call-spacing: off
func-name-matching: off
func-names: off
func-style: off
id-length: off
id-match: off
indent: off
jsx-quotes: off
key-spacing: off
keyword-spacing: off
line-comment-position: off
linebreak-style: off
lines-around-comment: off
lines-around-directive: off
max-depth: off
max-len: off
max-nested-callbacks: off
max-params: off
max-statements-per-line: off
max-statements:
- error
- 30
multiline-ternary: off
new-cap: off
new-parens: off
newline-after-var: off
newline-before-return: off
newline-per-chained-call: off
no-array-constructor: off
no-bitwise: off
no-continue: off
no-inline-comments: off
no-lonely-if: off
no-mixed-operators: off
no-mixed-spaces-and-tabs: off
no-multi-assign: off
no-multiple-empty-lines: off
no-negated-condition: off
no-nested-ternary: off
no-new-object: off
no-plusplus: off
no-restricted-syntax: off
no-spaced-func: off
no-tabs: off
no-ternary: off
no-trailing-spaces: off
no-underscore-dangle: off
no-unneeded-ternary: off
object-curly-newline: off
object-curly-spacing: off
object-property-newline: off
one-var-declaration-per-line: off
one-var: off
operator-assignment: off
operator-linebreak: off
padded-blocks: off
quote-props: off
quotes: off
require-jsdoc: off
semi-spacing: off
semi: off
sort-keys: off
sort-vars: off
space-before-blocks: off
space-before-function-paren: off
space-in-parens: off
space-infix-ops: off
space-unary-ops: off
spaced-comment: off
template-tag-spacing: off
unicode-bom: off
wrap-regex: off
# ECMAScript 6
arrow-body-style: off
arrow-parens: off
arrow-spacing: off
constructor-super: off
generator-star-spacing: off
no-class-assign: off
no-confusing-arrow: off
no-const-assign: off
no-dupe-class-members: off
no-duplicate-imports: off
no-new-symbol: off
no-restricted-imports: off
no-this-before-super: off
no-useless-computed-key: off
no-useless-constructor: off
no-useless-rename: off
no-var: off
object-shorthand: off
prefer-arrow-callback: off
prefer-const: off
prefer-destructuring: off
prefer-numeric-literals: off
prefer-rest-params: off
prefer-reflect: off
prefer-spread: off
prefer-template: off
require-yield: off
rest-spread-spacing: off
sort-imports: off
symbol-description: off
template-curly-spacing: off
yield-star-spacing: off

View file

@ -3,7 +3,7 @@ on:
release: release:
types: [published] types: [published]
schedule: schedule:
- cron: '0 6 * * 1' - cron: '0 1 * * 1'
jobs: jobs:
build: build:
@ -19,5 +19,5 @@ jobs:
imageName: ${{ secrets.DOCKER_HUB_USER }}/webssh2 imageName: ${{ secrets.DOCKER_HUB_USER }}/webssh2
platform: linux/amd64,linux/arm64,linux/ppc64le,linux/s390x,linux/arm/v7 platform: linux/amd64,linux/arm64,linux/ppc64le,linux/s390x,linux/arm/v7
publish: true publish: true
dockerHubUser: ${{ secrets.DOCKER_HUB_USER }} dockerUser: ${{ secrets.DOCKER_HUB_USER }}
dockerHubPassword: ${{ secrets.DOCKER_HUB_PASSWORD }} dockerPassword: ${{ secrets.DOCKER_HUB_PASSWORD }}

View file

@ -1,6 +1,6 @@
# Buliding # Buliding
To rebuild the client files, you need at least Node v10. To rebuild the client files, you need at least Node v14.
The source of the client files are located in `./app/client/source` The source of the client files are located in `./app/client/source`

View file

@ -1,4 +1,24 @@
# Change Log # Change Log
## 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.
- Dropping support for node versions under 14
### Changes
- Removed HTML menu code from ./app/server/socket.js, the menu is now fully laid out in the ./app/client/src/index.html and the option elements are hidden by default. Not sure why it wasn't done this way from the start, but there it is.
- Updated socket.io to v4.1.1
- Client javascript `./app/client/src/js/index.ts` is now built on TypeScript (`npm run build` will generate javascript for client and place into `app/client/public/webssh2.bundle.js` as before)
- Build environment changes
- removed unused xterm-addon-search, xterm-addon-weblinks, standard, postcss-discard-comments
- added prettier 2.3.0, typescript modules, socket.io-client 4.1.1, airbnb linting tools
### Added
- Lookup ip address for hostname in URL, fixes #199 thanks to @zwiy
- Ability to override `Authorization: Basic` header and replace with credentials specified in `config.json` fixes #243. New config.json option `user.overridebasic`
### CONTRIBUTING
In this release, we're trying our best to conform to the [Airbnb Javascript Style Guide](https://airbnb.io/projects/javascript/). I'm hoping this will make contributions easier and keep the code readable. I love shortcuts more than anyone but I've found when making changes to code I've not looked at in a while, it can take me a few momements to deconstruct what was being done due to readbility issues. While I don't agree with every decision in the style guide (semi-colons, yuk), it is a good base to keep the code consistent.
If you've not used it before, I recommend installing the [vscode extensions](https://blog.echobind.com/integrating-prettier-eslint-airbnb-style-guide-in-vscode-47f07b5d7d6a) for that and [Prettier](https://prettier.io/) and getting familiar. The autocorrections are great (especially if you hate dealing with semi-colons...)
As of 0.4.0-testing-0, the client code is written in [TypeScript](https://www.typescriptlang.org/docs/handbook/typescript-in-5-minutes.html). It's not that much different from JavaScript, and the introduction strong typing will ultimately help to produce better code. Eventually we want to move the whole project to TypeScript but that make take a bit more time. Take a moment to look at ./app/client/src/js/index.ts to see what TypeScript looks like.
## 0.3.1 [20210513] ## 0.3.1 [20210513]
### BREAKING ### BREAKING
- Ability to configure CORS settings for socket.io see [#240](../../issues/240) for more information on how this may break existing deployments. Default settings in example `config.json` are currently permissive `http.origins: ["*:*"]` please note that if a `config.json` is not present, the default is `http.origins: ["localhost:2222"] - Ability to configure CORS settings for socket.io see [#240](../../issues/240) for more information on how this may break existing deployments. Default settings in example `config.json` are currently permissive `http.origins: ["*:*"]` please note that if a `config.json` is not present, the default is `http.origins: ["localhost:2222"]

View file

@ -1,7 +1,7 @@
FROM node:8.6 FROM node:14.17-alpine
WORKDIR /usr/src WORKDIR /usr/src
COPY app/ /usr/src/ COPY app/ /usr/src/
RUN npm install --production RUN npm install --production
EXPOSE 2222 EXPOSE 2222/tcp
ENTRYPOINT [ "/usr/local/bin/node", "index.js" ] ENTRYPOINT [ "/usr/local/bin/node", "index.js" ]

View file

@ -109,6 +109,8 @@ docker run --name webssh2 -d -p 2222:2222 -v `pwd`/app/config.json:/usr/src/conf
* **user.password** - _string_ - Specify password 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.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.port** - _integer_ - Specify SSH port to connect to, defaults to `22`
@ -239,12 +241,22 @@ docker run --name webssh2 -d -p 2222:2222 -v `pwd`/app/config.json:/usr/src/conf
* **safeShutdownDuration** - _integer_ - maximum delay, in seconds, given to users before the server stops when doing a safe shutdown * **safeShutdownDuration** - _integer_ - maximum delay, in seconds, given to users before the server stops when doing a safe shutdown
# Experimental client-side logging # 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. 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.
# Example: # Example:
http://localhost:2222/ssh/host/192.168.1.1?port=2244&header=My%20Header&headerBackground=red http://localhost:2222/ssh/host/192.168.1.1?port=2244&header=My%20Header&headerBackground=red
# CONTRIBUTING
As of 0.4.0, we're trying our best to conform to the [Airbnb Javascript Style Guide](https://airbnb.io/projects/javascript/). I'm hoping this will make contributions easier and keep the code readable. I love shortcuts more than anyone but I've found when making changes to code I've not looked at in a while, it can take me a few momements to deconstruct what was being done due to readbility issues. While I don't agree with every decision in the style guide (semi-colons, yuk), it is a good base to keep the code consistent.
If you've not used it before, I recommend installing the [vscode extensions](https://blog.echobind.com/integrating-prettier-eslint-airbnb-style-guide-in-vscode-47f07b5d7d6a) for that and [Prettier](https://prettier.io/) and getting familiar. The autocorrections are great (especially if you hate dealing with semi-colons...)
All contributions are welcome, all may not make it into a release... To increase the chances of your contribution making it into a release, try your best to conform to the style guides and targets of the project.
# Tips # Tips
* If you want to add custom JavaScript to the browser client you can either modify `./src/client.html` and add a **<script>** element, modify `./src/index.js` directly, or check out `webpack.*.js` and add your custom javascript file to a task there (best option). * You can enable extended debug messages in the browser Java console using:
* `localStorage.debug = '*';` - Debug Everything (a lot of messages)
* `localStorage.debug = 'WebSSH2';` - Debug potentially interesting WebSSH2 related messages (replaying credentials, resizing data, other control messages)
* If you want to add custom JavaScript to the browser client you can either modify `./src/client.html` and add a **\<script\>** element, modify `./src/index.js` directly, or check out `webpack.*.js` and add your custom javascript file to a task there (best option).

24
app/.eslintrc.json Normal file
View file

@ -0,0 +1,24 @@
{
"ignorePatterns": ["**/*{.,-}min.js"],
"env": {
"browser": true,
"es2021": true,
"node": true
},
"extends": [
"airbnb-base",
"prettier"
],
"parser": "@typescript-eslint/parser",
"parserOptions": {
"ecmaVersion": 12,
"sourceType": "module"
},
"plugins": [
"@typescript-eslint",
"prettier"
],
"rules": {
"prettier/prettier": ["error"]
}
}

4
app/.prettierrc Normal file
View file

@ -0,0 +1,4 @@
{
"printWidth": 100,
"singleQuote": true
}

View file

@ -15,7 +15,12 @@
<div id="bottomdiv"> <div id="bottomdiv">
<div class="dropup" id="menu"> <div class="dropup" id="menu">
<i class="fas fa-bars fa-fw"></i> Menu <i class="fas fa-bars fa-fw"></i> Menu
<div id="dropupContent" class="dropup-content"></div> <div id="dropupContent" class="dropup-content">
<a id="logBtn"><i class="fas fa-clipboard fa-fw"></i> Start Log</a>
<a id="downloadLogBtn"><i class="fas fa-download fa-fw"></i> Download Log</a>
<a id="reauthBtn" style="display: none;"><i class="fas fa-key fa-fw"></i> Switch User</a>
<a id="credentialsBtn" style="display: none;"><i class="fas fa-key fa-fw"></i> Credentials</a>
</div>
</div> </div>
<div id="footer"></div> <div id="footer"></div>
<div id="status"></div> <div id="status"></div>

File diff suppressed because one or more lines are too long

View file

@ -2,11 +2,3 @@
* Font Awesome Free 5.15.3 by @fontawesome - https://fontawesome.com * Font Awesome Free 5.15.3 by @fontawesome - https://fontawesome.com
* License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) * License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License)
*/ */
/*!
* Socket.IO v2.2.0
* (c) 2014-2018 Guillermo Rauch
* Released under the MIT License.
*/
/*! https://mths.be/utf8js v2.1.2 by @mathias */

View file

@ -15,7 +15,12 @@
<div id="bottomdiv"> <div id="bottomdiv">
<div class="dropup" id="menu"> <div class="dropup" id="menu">
<i class="fas fa-bars fa-fw"></i> Menu <i class="fas fa-bars fa-fw"></i> Menu
<div id="dropupContent" class="dropup-content"></div> <div id="dropupContent" class="dropup-content">
<a id="logBtn"><i class="fas fa-clipboard fa-fw"></i> Start Log</a>
<a id="downloadLogBtn"><i class="fas fa-download fa-fw"></i> Download Log</a>
<a id="reauthBtn" style="display: none;"><i class="fas fa-key fa-fw"></i> Switch User</a>
<a id="credentialsBtn" style="display: none;"><i class="fas fa-key fa-fw"></i> Credentials</a>
</div>
</div> </div>
<div id="footer"></div> <div id="footer"></div>
<div id="status"></div> <div id="status"></div>

View file

@ -1,252 +0,0 @@
'use strict'
import * as io from 'socket.io-client/dist/socket.io.slim'
import { Terminal } from 'xterm'
import { FitAddon } from 'xterm-addon-fit'
/* import * as fit from 'xterm/dist/addons/fit/fit'
*/
import { library, dom } from '@fortawesome/fontawesome-svg-core'
import { faBars, faClipboard, faDownload, faKey, faCog } from '@fortawesome/free-solid-svg-icons'
library.add(faBars, faClipboard, faDownload, faKey, faCog)
dom.watch()
require('xterm/css/xterm.css')
require('../css/style.css')
/* global Blob, logBtn, credentialsBtn, reauthBtn, downloadLogBtn */
var sessionLogEnable = false
var loggedData = false
var allowreplay = false
var allowreauth = false
var sessionLog, sessionFooter, logDate, currentDate, myFile, errorExists
var socket, termid // eslint-disable-line
const term = new Terminal()
// DOM properties
var status = document.getElementById('status')
var header = document.getElementById('header')
var dropupContent = document.getElementById('dropupContent')
var footer = document.getElementById('footer')
var countdown = document.getElementById('countdown')
var fitAddon = new FitAddon()
var terminalContainer = document.getElementById('terminal-container')
term.loadAddon(fitAddon)
term.open(terminalContainer)
term.focus()
fitAddon.fit()
window.addEventListener('resize', resizeScreen, false)
function resizeScreen () {
fitAddon.fit()
socket.emit('resize', { cols: term.cols, rows: term.rows })
}
socket = io.connect({
path: '/ssh/socket.io'
})
term.onData(function (data) {
socket.emit('data', data)
})
socket.on('data', function (data) {
term.write(data)
if (sessionLogEnable) {
sessionLog = sessionLog + data
}
})
socket.on('connect', function () {
socket.emit('geometry', term.cols, term.rows)
})
socket.on('setTerminalOpts', function (data) {
term.setOption('cursorBlink', data.cursorBlink)
term.setOption('scrollback', data.scrollback)
term.setOption('tabStopWidth', data.tabStopWidth)
term.setOption('bellStyle', data.bellStyle)
})
socket.on('title', function (data) {
document.title = data
})
socket.on('menu', function (data) {
drawMenu(data)
})
socket.on('status', function (data) {
status.innerHTML = data
})
socket.on('ssherror', function (data) {
status.innerHTML = data
status.style.backgroundColor = 'red'
errorExists = true
})
socket.on('headerBackground', function (data) {
header.style.backgroundColor = data
})
socket.on('header', function (data) {
if (data) {
header.innerHTML = data
header.style.display = 'block'
// header is 19px and footer is 19px, recaculate new terminal-container and resize
terminalContainer.style.height = 'calc(100% - 38px)'
resizeScreen()
}
})
socket.on('footer', function (data) {
sessionFooter = data
footer.innerHTML = data
})
socket.on('statusBackground', function (data) {
status.style.backgroundColor = data
})
socket.on('allowreplay', function (data) {
if (data === true) {
console.log('allowreplay: ' + data)
allowreplay = true
drawMenu(dropupContent.innerHTML + '<a id="credentialsBtn"><i class="fas fa-key fa-fw"></i> Credentials</a>')
} else {
allowreplay = false
console.log('allowreplay: ' + data)
}
})
socket.on('allowreauth', function (data) {
if (data === true) {
console.log('allowreauth: ' + data)
allowreauth = true
drawMenu(dropupContent.innerHTML + '<a id="reauthBtn"><i class="fas fa-key fa-fw"></i> Switch User</a>')
} else {
allowreauth = false
console.log('allowreauth: ' + data)
}
})
socket.on('disconnect', function (err) {
if (!errorExists) {
status.style.backgroundColor = 'red'
status.innerHTML =
'WEBSOCKET SERVER DISCONNECTED: ' + err
}
socket.io.reconnection(false)
countdown.classList.remove('active')
})
socket.on('error', function (err) {
if (!errorExists) {
status.style.backgroundColor = 'red'
status.innerHTML = 'ERROR: ' + err
}
})
socket.on('reauth', function () {
(allowreauth) && reauthSession()
})
// safe shutdown
var hasCountdownStarted = false
socket.on('shutdownCountdownUpdate', function (remainingSeconds) {
if (!hasCountdownStarted) {
countdown.classList.add('active')
hasCountdownStarted = true
}
countdown.innerText = 'Shutting down in ' + remainingSeconds + 's'
})
term.onTitleChange(function (title) {
document.title = title
})
// draw/re-draw menu and reattach listeners
// when dom is changed, listeners are abandonded
function drawMenu (data) {
dropupContent.innerHTML = data
logBtn.addEventListener('click', toggleLog)
allowreauth && reauthBtn.addEventListener('click', reauthSession)
allowreplay && credentialsBtn.addEventListener('click', replayCredentials)
loggedData && downloadLogBtn.addEventListener('click', downloadLog)
}
// reauthenticate
function reauthSession () { // eslint-disable-line
console.log('re-authenticating')
window.location.href = '/ssh/reauth'
return false
}
// replay password to server, requires
function replayCredentials () { // eslint-disable-line
socket.emit('control', 'replayCredentials')
console.log('replaying credentials')
term.focus()
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
loggedData = true
logBtn.innerHTML = '<i class="fas fa-clipboard fa-fw"></i> Start Log'
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
term.focus()
return false
} else {
sessionLogEnable = true
loggedData = true
logBtn.innerHTML = '<i class="fas fa-cog fa-spin fa-fw"></i> Stop Log'
downloadLogBtn.style.color = '#000'
downloadLogBtn.addEventListener('click', downloadLog)
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
term.focus()
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
if (loggedData === true) {
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, '')], { // eslint-disable-line no-control-regex
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)
}
}
term.focus()
}

282
app/client/src/js/index.ts Normal file
View file

@ -0,0 +1,282 @@
/* eslint-disable import/no-extraneous-dependencies */
import { io } from 'socket.io-client';
import { Terminal } from 'xterm';
import { FitAddon } from 'xterm-addon-fit';
import { library, dom } from '@fortawesome/fontawesome-svg-core';
import { faBars, faClipboard, faDownload, faKey, faCog } from '@fortawesome/free-solid-svg-icons';
library.add(faBars, faClipboard, faDownload, faKey, faCog);
dom.watch();
const debug = require('debug')('WebSSH2');
require('xterm/css/xterm.css');
require('../css/style.css');
/* global Blob, logBtn, credentialsBtn, reauthBtn, downloadLogBtn */ // eslint-disable-line
let sessionLogEnable = false;
let loggedData = false;
let allowreplay = false;
let allowreauth = false;
let sessionLog: string;
let sessionFooter: any;
let logDate: {
getFullYear: () => any;
getMonth: () => number;
getDate: () => any;
getHours: () => any;
getMinutes: () => any;
getSeconds: () => any;
};
let currentDate: Date;
let myFile: string;
let errorExists: boolean;
const term = new Terminal();
// DOM properties
const logBtn = document.getElementById('logBtn');
const credentialsBtn = document.getElementById('credentialsBtn');
const reauthBtn = document.getElementById('reauthBtn');
const downloadLogBtn = document.getElementById('downloadLogBtn');
const status = document.getElementById('status');
const header = document.getElementById('header');
const footer = document.getElementById('footer');
const countdown = document.getElementById('countdown');
const fitAddon = new FitAddon();
const terminalContainer = document.getElementById('terminal-container');
term.loadAddon(fitAddon);
term.open(terminalContainer);
term.focus();
fitAddon.fit();
const socket = io({
path: '/ssh/socket.io',
});
// reauthenticate
function reauthSession () { // eslint-disable-line
debug('re-authenticating');
window.location.href = '/ssh/reauth';
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
if (loggedData === true) {
myFile = `WebSSH2-${logDate.getFullYear()}${
logDate.getMonth() + 1
}${logDate.getDate()}_${logDate.getHours()}${logDate.getMinutes()}${logDate.getSeconds()}.log`;
// regex should eliminate escape sequences from being logged.
const blob = new Blob(
[
sessionLog.replace(
// eslint-disable-next-line no-control-regex
/[\u001b\u009b][[\]()#;?]*(?:[0-9]{1,4}(?:;[0-9]{0,4})*)?[0-9A-ORZcf-nqry=><;]/g,
''
),
],
{
// eslint-disable-line no-control-regex
type: 'text/plain',
}
);
if (window.navigator.msSaveOrOpenBlob) {
window.navigator.msSaveBlob(blob, myFile);
} else {
const elem = window.document.createElement('a');
elem.href = window.URL.createObjectURL(blob);
elem.download = myFile;
document.body.appendChild(elem);
elem.click();
document.body.removeChild(elem);
}
}
term.focus();
}
// 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;
loggedData = true;
logBtn.innerHTML = '<i class="fas fa-clipboard fa-fw"></i> Start Log';
// 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;
term.focus();
return false;
}
sessionLogEnable = true;
loggedData = true;
logBtn.innerHTML = '<i class="fas fa-cog fa-spin fa-fw"></i> Stop Log';
downloadLogBtn.style.color = '#000';
downloadLogBtn.addEventListener('click', downloadLog);
// 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;
term.focus();
return false;
}
// replay password to server, requires
function replayCredentials () { // eslint-disable-line
socket.emit('control', 'replayCredentials');
// console.log('replaying credentials');
term.focus();
return false;
}
// draw/re-draw menu and reattach listeners
// when dom is changed, listeners are abandonded
function drawMenu() {
logBtn.addEventListener('click', toggleLog);
if (allowreauth) {
reauthBtn.addEventListener('click', reauthSession);
reauthBtn.style.display = 'block';
}
if (allowreplay) {
credentialsBtn.addEventListener('click', replayCredentials);
credentialsBtn.style.display = 'block';
}
if (loggedData) {
downloadLogBtn.addEventListener('click', downloadLog);
downloadLogBtn.style.display = 'block';
}
}
function resizeScreen() {
fitAddon.fit();
socket.emit('resize', { cols: term.cols, rows: term.rows });
}
window.addEventListener('resize', resizeScreen, false);
term.onData((data) => {
socket.emit('data', data);
});
socket.on('data', (data: string | Uint8Array) => {
term.write(data);
if (sessionLogEnable) {
sessionLog += data;
}
});
socket.on('connect', () => {
socket.emit('geometry', term.cols, term.rows);
});
socket.on(
'setTerminalOpts',
(data: { cursorBlink: any; scrollback: any; tabStopWidth: any; bellStyle: any }) => {
term.setOption('cursorBlink', data.cursorBlink);
term.setOption('scrollback', data.scrollback);
term.setOption('tabStopWidth', data.tabStopWidth);
term.setOption('bellStyle', data.bellStyle);
}
);
socket.on('title', (data: string) => {
document.title = data;
});
socket.on('menu', () => {
drawMenu();
});
socket.on('status', (data: string) => {
status.innerHTML = data;
});
socket.on('ssherror', (data: string) => {
status.innerHTML = data;
status.style.backgroundColor = 'red';
errorExists = true;
});
socket.on('headerBackground', (data: string) => {
header.style.backgroundColor = data;
});
socket.on('header', (data: string) => {
if (data) {
header.innerHTML = data;
header.style.display = 'block';
// header is 19px and footer is 19px, recaculate new terminal-container and resize
terminalContainer.style.height = 'calc(100% - 38px)';
resizeScreen();
}
});
socket.on('footer', (data: string) => {
sessionFooter = data;
footer.innerHTML = data;
});
socket.on('statusBackground', (data: string) => {
status.style.backgroundColor = data;
});
socket.on('allowreplay', (data: boolean) => {
if (data === true) {
debug(`allowreplay: ${data}`);
allowreplay = true;
drawMenu();
} else {
allowreplay = false;
debug(`allowreplay: ${data}`);
}
});
socket.on('allowreauth', (data: boolean) => {
if (data === true) {
debug(`allowreauth: ${data}`);
allowreauth = true;
drawMenu();
} else {
allowreauth = false;
debug(`allowreauth: ${data}`);
}
});
socket.on('disconnect', (err: any) => {
if (!errorExists) {
status.style.backgroundColor = 'red';
status.innerHTML = `WEBSOCKET SERVER DISCONNECTED: ${err}`;
}
socket.io.reconnection(false);
countdown.classList.remove('active');
});
socket.on('error', (err: any) => {
if (!errorExists) {
status.style.backgroundColor = 'red';
status.innerHTML = `ERROR: ${err}`;
}
});
socket.on('reauth', () => {
if (allowreauth) {
reauthSession();
}
});
// safe shutdown
let hasCountdownStarted = false;
socket.on('shutdownCountdownUpdate', (remainingSeconds: any) => {
if (!hasCountdownStarted) {
countdown.classList.add('active');
hasCountdownStarted = true;
}
countdown.innerText = `Shutting down in ${remainingSeconds}s`;
});
term.onTitleChange((title) => {
document.title = title;
});

9
app/client/tsconfig.json Normal file
View file

@ -0,0 +1,9 @@
{
"compilerOptions": {
"outDir": "./built",
"allowJs": true,
"target": "es6",
"moduleResolution": "node"
},
"include": ["./src/**/*"],
}

View file

@ -10,6 +10,7 @@
"name": null, "name": null,
"password": null, "password": null,
"privatekey": null "privatekey": null
"overridebasic": false
}, },
"ssh": { "ssh": {
"host": null, "host": null,

View file

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

File diff suppressed because it is too large Load diff

5471
app/package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -1,6 +1,6 @@
{ {
"name": "webssh2", "name": "webssh2",
"version": "0.3.1", "version": "0.4.0-testing-2",
"ignore": [ "ignore": [
".gitignore" ".gitignore"
], ],
@ -41,7 +41,7 @@
"morgan": "~1.10.0", "morgan": "~1.10.0",
"read-config-ng": "^3.0.2", "read-config-ng": "^3.0.2",
"serve-favicon": "^2.5.0", "serve-favicon": "^2.5.0",
"socket.io": "^2.2.0", "socket.io": "^4.1.1",
"ssh2": "~0.8.9", "ssh2": "~0.8.9",
"validator": "^13.6.0" "validator": "^13.6.0"
}, },
@ -52,44 +52,36 @@
"analyze": "webpack --json --config scripts/webpack.prod.js | webpack-bundle-size-analyzer", "analyze": "webpack --json --config scripts/webpack.prod.js | webpack-bundle-size-analyzer",
"test": "snyk test", "test": "snyk test",
"watch": "nodemon index.js", "watch": "nodemon index.js",
"standard": "standard --verbose --fix | snazzy",
"cleanmac": "find . -name '.DS_Store' -type f -delete", "cleanmac": "find . -name '.DS_Store' -type f -delete",
"release": "standard-version" "release": "standard-version"
}, },
"standard": {
"ignore": [
"client/public/webssh2.bundle.js",
"bigip/*",
"screenshots/*",
"bin/*",
"build/*",
"workspace/*"
]
},
"devDependencies": { "devDependencies": {
"@fortawesome/fontawesome-svg-core": "^1.2.35", "@fortawesome/fontawesome-svg-core": "^1.2.35",
"@fortawesome/free-solid-svg-icons": "^5.15.3", "@fortawesome/free-solid-svg-icons": "^5.15.3",
"@typescript-eslint/eslint-plugin": "^4.23.0",
"@typescript-eslint/parser": "^4.23.0",
"clean-webpack-plugin": "^3.0.0", "clean-webpack-plugin": "^3.0.0",
"copy-webpack-plugin": "^8.1.1", "copy-webpack-plugin": "^8.1.1",
"cross-env": "^7.0.3",
"css-loader": "^5.2.4", "css-loader": "^5.2.4",
"file-loader": "^6.2.0", "eslint": "^7.26.0",
"eslint-config-airbnb-base": "^14.2.1",
"eslint-config-prettier": "^8.3.0",
"eslint-plugin-import": "^2.22.1",
"eslint-plugin-prettier": "^3.4.0",
"mini-css-extract-plugin": "^1.6.0", "mini-css-extract-plugin": "^1.6.0",
"nodaemon": "0.0.5", "nodaemon": "0.0.5",
"postcss-discard-comments": "^5.0.0", "prettier": "^2.3.0",
"snazzy": "^9.0.0", "snazzy": "^9.0.0",
"standard": "^16.0.3", "socket.io-client": "^4.1.1",
"source-map-loader": "^2.0.1",
"standard-version": "^9.3.0", "standard-version": "^9.3.0",
"style-loader": "^2.0.0", "terser-webpack-plugin": "^5.1.2",
"terser-webpack-plugin": "^5.1.1", "ts-loader": "^9.1.2",
"url-loader": "^4.1.1", "typescript": "^4.2.4",
"webpack": "^5.37.0", "webpack": "^5.37.0",
"webpack-cli": "^4.7.0", "webpack-cli": "^4.7.0",
"webpack-merge": "^5.7.3", "webpack-merge": "^5.7.3",
"webpack-stream": "^6.1.2",
"xterm": "^4.12.0", "xterm": "^4.12.0",
"xterm-addon-fit": "^0.5.0", "xterm-addon-fit": "^0.5.0"
"xterm-addon-search": "^0.8.0",
"xterm-addon-web-links": "^0.4.0"
} }
} }

View file

@ -1,32 +1,45 @@
const path = require('path') /* eslint-disable import/no-extraneous-dependencies */
const { CleanWebpackPlugin } = require('clean-webpack-plugin') const path = require('path');
const CopyWebpackPlugin = require('copy-webpack-plugin') const { CleanWebpackPlugin } = require('clean-webpack-plugin');
const CopyWebpackPlugin = require('copy-webpack-plugin');
const MiniCssExtractPlugin = require('mini-css-extract-plugin'); const MiniCssExtractPlugin = require('mini-css-extract-plugin');
module.exports = { module.exports = {
context: path.resolve('__dirname', '../'), context: path.resolve('__dirname', '../'),
resolve: {
// Add '.ts' and '.tsx' as resolvable extensions.
extensions: ['', '.webpack.js', '.web.js', '.ts', '.tsx', '.js'],
},
entry: { entry: {
webssh2: './client/src/js/index.js' webssh2: './client/src/js/index.ts',
}, },
plugins: [ plugins: [
new CleanWebpackPlugin(), new CleanWebpackPlugin(),
new CopyWebpackPlugin({ new CopyWebpackPlugin({
patterns: [ patterns: ['./client/src/client.htm', './client/src/favicon.ico'],
'./client/src/client.htm',
'./client/src/favicon.ico'
]
}), }),
new MiniCssExtractPlugin() new MiniCssExtractPlugin(),
], ],
output: { output: {
filename: '[name].bundle.js', filename: '[name].bundle.js',
path: path.resolve(__dirname, '../client/public') path: path.resolve(__dirname, '../client/public'),
}, },
module: { module: {
rules: [ rules: [
{ {
test: /\.css$/, test: /\.css$/,
use: [MiniCssExtractPlugin.loader, 'css-loader'] use: [MiniCssExtractPlugin.loader, 'css-loader'],
} },
] // All files with a '.ts' or '.tsx' extension will be handled by 'ts-loader'.
} {
} test: /\.tsx?$/,
loader: 'ts-loader',
},
// All output '.js' files will have any sourcemaps re-processed by 'source-map-loader'.
{
test: /\.js$/,
loader: 'source-map-loader',
},
],
},
};

View file

@ -1,9 +1,10 @@
const merge = require('webpack-merge') /* eslint-disable import/no-extraneous-dependencies */
const common = require('./webpack.common.js') const merge = require('webpack-merge');
const common = require('./webpack.common.js');
module.exports = merge(common, { module.exports = merge(common, {
devtool: 'inline-source-map', devtool: 'inline-source-map',
devServer: { devServer: {
contentBase: '../client/public' contentBase: '../client/public',
} },
}) });

View file

@ -1,16 +1,19 @@
const TerserPlugin = require('terser-webpack-plugin') /* eslint-disable import/no-extraneous-dependencies */
const { merge } = require('webpack-merge') const TerserPlugin = require('terser-webpack-plugin');
const common = require('./webpack.common.js') const { merge } = require('webpack-merge');
const common = require('./webpack.common.js');
module.exports = merge(common, { module.exports = merge(common, {
mode: 'production', mode: 'production',
optimization: { optimization: {
minimize: true, minimize: true,
minimizer: [new TerserPlugin({ minimizer: [
new TerserPlugin({
terserOptions: { terserOptions: {
ie8: false, ie8: false,
safari10: false safari10: false,
} },
})] }),
} ],
}) },
});

View file

@ -1,259 +1,206 @@
'use strict'
/* jshint esversion: 6, asi: true, node: true */ /* jshint esversion: 6, asi: true, node: true */
/* eslint no-unused-expressions: ["error", { "allowShortCircuit": true, "allowTernary": true }],
no-console: ["error", { allow: ["warn", "error"] }] */
// app.js // app.js
var path = require('path') // eslint-disable-next-line import/order
var fs = require('fs') const config = require('./config');
var nodeRoot = path.dirname(require.main.filename) const path = require('path');
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 const nodeRoot = path.dirname(require.main.filename);
let config = { const publicPath = path.join(nodeRoot, 'client', 'public');
listen: { const express = require('express');
ip: '0.0.0.0', const logger = require('morgan');
port: 2222
},
http: {
origins: ['localhost: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,
safeShutdownDuration: 300
}
// test if config.json exists, if not provide error message but try to run const app = express();
// anyway const server = require('http').Server(app);
try { const validator = require('validator');
if (fs.existsSync(configPath)) { const favicon = require('serve-favicon');
console.log('ephemeral_auth service reading config from: ' + configPath) const io = require('socket.io')(server, {
config = require('read-config-ng')(configPath) serveClient: false,
} else { path: '/ssh/socket.io',
console.error('\n\nERROR: Missing config.json for webssh. Current config: ' + JSON.stringify(config)) origins: config.http.origins,
console.error('\n See config.json.sample for details\n\n') });
} const session = require('express-session')({
} 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)
}
var session = require('express-session')({
secret: config.session.secret, secret: config.session.secret,
name: config.session.name, name: config.session.name,
resave: true, resave: true,
saveUninitialized: false, saveUninitialized: false,
unset: 'destroy' unset: 'destroy',
}) });
var app = express() const appSocket = require('./socket');
var server = require('http').Server(app) const expressOptions = require('./expressOptions');
var myutil = require('./util') const 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, path: '/ssh/socket.io', origins: config.http.origins })
var socket = require('./socket')
var expressOptions = require('./expressOptions')
var favicon = require('serve-favicon')
myutil.setDefaultCredentials(
config.user.name,
config.user.password,
config.user.privatekey,
config.user.overridebasic
);
// 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();
}
module.exports = { server, config };
// express // express
app.use(safeShutdownGuard) app.use(safeShutdownGuard);
app.use(session) app.use(session);
app.use(myutil.basicAuth) app.use(myutil.basicAuth);
if (config.accesslog) app.use(logger('common')) if (config.accesslog) app.use(logger('common'));
app.disable('x-powered-by') app.disable('x-powered-by');
// static files // static files
app.use('/ssh', express.static(publicPath, expressOptions)) app.use('/ssh', express.static(publicPath, expressOptions));
// favicon from root if being pre-fetched by browser to prevent a 404 // favicon from root if being pre-fetched by browser to prevent a 404
app.use(favicon(path.join(publicPath,'favicon.ico'))) app.use(favicon(path.join(publicPath, 'favicon.ico')));
app.get('/ssh/reauth', function (req, res, next) { app.get('/ssh/reauth', (req, res) => {
var r = req.headers.referer || '/' const r = req.headers.referer || '/';
res.status(401).send('<!DOCTYPE html><html><head><meta http-equiv="refresh" content="0; url=' + r + '"></head><body bgcolor="#000"></body></html>') res
}) .status(401)
.send(
`<!DOCTYPE html><html><head><meta http-equiv="refresh" content="0; url=${r}"></head><body bgcolor="#000"></body></html>`
);
});
// eslint-disable-next-line complexity // eslint-disable-next-line complexity
app.get('/ssh/host/:host?', function (req, res, next) { app.get('/ssh/host/:host?', (req, res) => {
res.sendFile(path.join(path.join(publicPath, 'client.htm'))) res.sendFile(path.join(path.join(publicPath, 'client.htm')));
// capture, assign, and validated variables // capture, assign, and validate variables
req.session.ssh = { req.session.ssh = {
host: config.ssh.host || (validator.isIP(req.params.host + '') && req.params.host) || host:
config.ssh.host ||
(validator.isIP(`${req.params.host}`) && req.params.host) ||
(validator.isFQDN(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) && (/^(([a-z]|[A-Z]|[0-9]|[!^(){}\-_~])+)?\w$/.test(req.params.host) && req.params.host),
req.params.host), port:
port: (validator.isInt(req.query.port + '', { min: 1, max: 65535 }) && (validator.isInt(`${req.query.port}`, { min: 1, max: 65535 }) && req.query.port) ||
req.query.port) || config.ssh.port, config.ssh.port,
localAddress: config.ssh.localAddress, localAddress: config.ssh.localAddress,
localPort: config.ssh.localPort, localPort: config.ssh.localPort,
header: { header: {
name: req.query.header || config.header.text, name: req.query.header || config.header.text,
background: req.query.headerBackground || config.header.background background: req.query.headerBackground || config.header.background,
}, },
algorithms: config.algorithms, algorithms: config.algorithms,
keepaliveInterval: config.ssh.keepaliveInterval, keepaliveInterval: config.ssh.keepaliveInterval,
keepaliveCountMax: config.ssh.keepaliveCountMax, keepaliveCountMax: config.ssh.keepaliveCountMax,
allowedSubnets: config.ssh.allowedSubnets, allowedSubnets: config.ssh.allowedSubnets,
term: (/^(([a-z]|[A-Z]|[0-9]|[!^(){}\-_~])+)?\w$/.test(req.query.sshterm) && term:
req.query.sshterm) || config.ssh.term, (/^(([a-z]|[A-Z]|[0-9]|[!^(){}\-_~])+)?\w$/.test(req.query.sshterm) && req.query.sshterm) ||
config.ssh.term,
terminal: { terminal: {
cursorBlink: (validator.isBoolean(req.query.cursorBlink + '') ? myutil.parseBool(req.query.cursorBlink) : config.terminal.cursorBlink), cursorBlink: validator.isBoolean(`${req.query.cursorBlink}`)
scrollback: (validator.isInt(req.query.scrollback + '', { min: 1, max: 200000 }) && req.query.scrollback) ? req.query.scrollback : config.terminal.scrollback, ? myutil.parseBool(req.query.cursorBlink)
tabStopWidth: (validator.isInt(req.query.tabStopWidth + '', { min: 1, max: 100 }) && req.query.tabStopWidth) ? req.query.tabStopWidth : config.terminal.tabStopWidth, : config.terminal.cursorBlink,
bellStyle: ((req.query.bellStyle) && (['sound', 'none'].indexOf(req.query.bellStyle) > -1)) ? req.query.bellStyle : config.terminal.bellStyle 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), allowreplay:
config.options.challengeButton ||
(validator.isBoolean(`${req.headers.allowreplay}`)
? myutil.parseBool(req.headers.allowreplay)
: false),
allowreauth: config.options.allowreauth || false, allowreauth: config.options.allowreauth || false,
mrhsession: ((validator.isAlphanumeric(req.headers.mrhsession + '') && req.headers.mrhsession) ? req.headers.mrhsession : 'none'), mrhsession:
validator.isAlphanumeric(`${req.headers.mrhsession}`) && req.headers.mrhsession
? req.headers.mrhsession
: 'none',
serverlog: { serverlog: {
client: config.serverlog.client || false, client: config.serverlog.client || false,
server: config.serverlog.server || false server: config.serverlog.server || false,
}, },
readyTimeout: (validator.isInt(req.query.readyTimeout + '', { min: 1, max: 300000 }) && readyTimeout:
req.query.readyTimeout) || config.ssh.readyTimeout (validator.isInt(`${req.query.readyTimeout}`, { min: 1, max: 300000 }) &&
} req.query.readyTimeout) ||
if (req.session.ssh.header.name) validator.escape(req.session.ssh.header.name) config.ssh.readyTimeout,
if (req.session.ssh.header.background) validator.escape(req.session.ssh.header.background) };
}) 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 // express error handling
app.use(function (req, res, next) { app.use((req, res) => {
res.status(404).send("Sorry can't find that!") res.status(404).send("Sorry, can't find that!");
}) });
app.use(function (err, req, res, next) { app.use((err, req, res) => {
console.error(err.stack) console.error(err.stack);
res.status(500).send('Something broke!') res.status(500).send('Something broke!');
}) });
// bring up socket
io.on('connection', appSocket);
// socket.io // socket.io
// expose express session with socket.request.session // expose express session with socket.request.session
io.use(function (socket, next) { io.use((socket, next) => {
(socket.request.res) ? session(socket.request, socket.request.res, next) socket.request.res ? session(socket.request, socket.request.res, next) : next(next); // eslint disable-line
: next(next) });
})
// bring up socket io.on('connection', (socket) => {
io.on('connection', socket) connectionCount += 1;
// safe shutdown socket.on('disconnect', () => {
var shutdownMode = false connectionCount -= 1;
var shutdownInterval = 0 if (connectionCount <= 0 && shutdownMode) {
var connectionCount = 0 stopApp('All clients disconnected');
function safeShutdownGuard (req, res, next) {
if (shutdownMode) res.status(503).end('Service unavailable: Server shutting down')
else return next()
} }
});
});
io.on('connection', function (socket) { const signals = ['SIGTERM', 'SIGINT'];
connectionCount++ signals.forEach((signal) =>
process.on(signal, () => {
socket.on('disconnect', function () { if (shutdownMode) stopApp('Safe shutdown aborted, force quitting');
if ((--connectionCount <= 0) && shutdownMode) {
stop('All clients disconnected')
}
})
})
const signals = ['SIGTERM', 'SIGINT']
signals.forEach(signal => process.on(signal, function () {
if (shutdownMode) stop('Safe shutdown aborted, force quitting')
else if (connectionCount > 0) { else if (connectionCount > 0) {
var remainingSeconds = config.safeShutdownDuration let remainingSeconds = config.safeShutdownDuration;
shutdownMode = true 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');
var message = (connectionCount === 1) ? ' client is still connected' shutdownInterval = setInterval(() => {
: ' clients are still connected' remainingSeconds -= 1;
console.error(connectionCount + message) if (remainingSeconds <= 0) {
console.error('Starting a ' + remainingSeconds + ' seconds countdown') stopApp('Countdown is over');
console.error('Press Ctrl+C again to force quit')
shutdownInterval = setInterval(function () {
if ((remainingSeconds--) <= 0) {
stop('Countdown is over')
} else { } else {
io.sockets.emit('shutdownCountdownUpdate', remainingSeconds) io.sockets.emit('shutdownCountdownUpdate', remainingSeconds);
} }
}, 1000) }, 1000);
} else stop() } else stopApp();
})) })
);
// clean stop
function stop (reason) {
shutdownMode = false
if (reason) console.log('Stopping: ' + reason)
if (shutdownInterval) clearInterval(shutdownInterval)
io.close()
server.close()
}
module.exports = { server: server, config: config }

102
app/server/config.js Normal file
View file

@ -0,0 +1,102 @@
/* eslint no-unused-expressions: ["error", { "allowShortCircuit": true, "allowTernary": true }],
no-console: ["error", { allow: ["warn", "error"] }] */
const fs = require('fs');
const path = require('path');
const nodeRoot = path.dirname(require.main.filename);
const configPath = path.join(nodeRoot, 'config.json');
// sane defaults if config.json or parts are missing
let config = {
listen: {
ip: '0.0.0.0',
port: 2222,
},
http: {
origins: ['localhost:2222'],
},
user: {
name: null,
password: null,
privatekey: null,
overridebasic: false,
},
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,
safeShutdownDuration: 300,
};
// test if config.json exists, if not provide error message but try to run anyway
try {
if (fs.existsSync(configPath)) {
// eslint-disable-next-line no-console
console.info(`WebSSH2 service reading config from: ${configPath}`);
// eslint-disable-next-line global-require
config = require('read-config-ng')(configPath);
} 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');
}
} catch (err) {
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');
console.error(`ERROR:\n\n ${err}`);
}
module.exports = config;

View file

@ -5,7 +5,7 @@ module.exports = {
index: false, index: false,
maxAge: '1s', maxAge: '1s',
redirect: false, redirect: false,
setHeaders: function (res, path, stat) { setHeaders(res) {
res.set('x-timestamp', Date.now()) res.set('x-timestamp', Date.now());
} },
} };

View file

@ -1,136 +1,224 @@
/* eslint-disable complexity */ /* eslint-disable complexity */
'use strict' /* eslint no-unused-expressions: ["error", { "allowShortCircuit": true, "allowTernary": true }],
no-console: ["error", { allow: ["warn", "error"] }] */
/* jshint esversion: 6, asi: true, node: true */ /* jshint esversion: 6, asi: true, node: true */
// socket.js // socket.js
// private // private
var debug = require('debug') const debug = require('debug');
var debugWebSSH2 = require('debug')('WebSSH2') const debugWebSSH2 = require('debug')('WebSSH2');
var SSH = require('ssh2').Client const SSH = require('ssh2').Client;
var CIDRMatcher = require('cidr-matcher') const CIDRMatcher = require('cidr-matcher');
const validator = require('validator');
const dnsPromises = require('dns').promises;
// var fs = require('fs') // var fs = require('fs')
// var hostkeys = JSON.parse(fs.readFileSync('./hostkeyhashes.json', 'utf8')) // var hostkeys = JSON.parse(fs.readFileSync('./hostkeyhashes.json', 'utf8'))
var termCols, termRows let termCols;
var menuData = '<a id="logBtn"><i class="fas fa-clipboard fa-fw"></i> Start Log</a>' + let termRows;
'<a id="downloadLogBtn"><i class="fas fa-download fa-fw"></i> Download Log</a>'
// public // public
module.exports = function socket (socket) { module.exports = function appSocket(socket) {
async function setupConnection() {
// if websocket connection arrives without an express session, kill it // if websocket connection arrives without an express session, kill it
if (!socket.request.session) { if (!socket.request.session) {
socket.emit('401 UNAUTHORIZED') socket.emit('401 UNAUTHORIZED');
debugWebSSH2('SOCKET: No Express Session / REJECTED') debugWebSSH2('SOCKET: No Express Session / REJECTED');
socket.disconnect(true) socket.disconnect(true);
return return;
} }
/**
* Error handling for various events. Outputs error to client, logs to
* server, destroys session and disconnects socket.
* @param {string} myFunc Function calling this function
* @param {object} err error object or error message
*/
// eslint-disable-next-line complexity
function SSHerror(myFunc, err) {
let theError;
if (socket.request.session) {
// we just want the first error of the session to pass to the client
const firstError = socket.request.session.error || (err ? err.message : undefined);
theError = firstError ? `: ${firstError}` : '';
// log unsuccessful login attempt
if (err && err.level === 'client-authentication') {
console.error(
`WebSSH2 ${'error: Authentication failure'.red.bold} user=${
socket.request.session.username.yellow.bold.underline
} from=${socket.handshake.address.yellow.bold.underline}`
);
socket.emit('allowreauth', socket.request.session.ssh.allowreauth);
socket.emit('reauth');
} else {
// eslint-disable-next-line no-console
console.log(
`WebSSH2 Logout: user=${socket.request.session.username} from=${socket.handshake.address} host=${socket.request.session.ssh.host} port=${socket.request.session.ssh.port} sessionID=${socket.request.sessionID}/${socket.id} allowreplay=${socket.request.session.ssh.allowreplay} term=${socket.request.session.ssh.term}`
);
if (err) {
theError = err ? `: ${err.message}` : '';
console.error(`WebSSH2 error${theError}`);
}
}
socket.emit('ssherror', `SSH ${myFunc}${theError}`);
socket.request.session.destroy();
socket.disconnect(true);
} else {
theError = err ? `: ${err.message}` : '';
socket.disconnect(true);
}
debugWebSSH2(`SSHerror ${myFunc}${theError}`);
}
// If configured, check that requsted host is in a permitted subnet // 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)) { if (
var matcher = new CIDRMatcher(socket.request.session.ssh.allowedSubnets) (((socket.request.session || {}).ssh || {}).allowedSubnets || {}).length &&
if (!matcher.contains(socket.request.session.ssh.host)) { socket.request.session.ssh.allowedSubnets.length > 0
console.log('WebSSH2 ' + 'error: Requested host outside configured subnets / REJECTED'.red.bold + ) {
' user=' + socket.request.session.username.yellow.bold.underline + let ipaddress = socket.request.session.ssh.host;
' from=' + socket.handshake.address.yellow.bold.underline) if (!validator.isIP(`${ipaddress}`)) {
socket.emit('ssherror', '401 UNAUTHORIZED') try {
socket.disconnect(true) const result = await dnsPromises.lookup(socket.request.session.ssh.host);
return ipaddress = result.address;
} catch (err) {
console.error(
`WebSSH2 ${`error: ${err.code} ${err.hostname}`.red.bold} user=${
socket.request.session.username.yellow.bold.underline
} from=${socket.handshake.address.yellow.bold.underline}`
);
socket.emit('ssherror', '404 HOST IP NOT FOUND');
socket.disconnect(true);
return;
} }
} }
var conn = new SSH() const matcher = new CIDRMatcher(socket.request.session.ssh.allowedSubnets);
socket.on('geometry', function socketOnGeometry (cols, rows) { if (!matcher.contains(ipaddress)) {
termCols = cols console.error(
termRows = rows `WebSSH2 ${
}) `error: Requested host ${ipaddress} outside configured subnets / REJECTED`.red.bold
conn.on('banner', function connOnBanner (data) { } user=${socket.request.session.username.yellow.bold.underline} from=${
socket.handshake.address.yellow.bold.underline
}`
);
socket.emit('ssherror', '401 UNAUTHORIZED');
socket.disconnect(true);
return;
}
}
const conn = new SSH();
socket.on('geometry', (cols, rows) => {
termCols = cols;
termRows = rows;
});
conn.on('banner', (data) => {
// need to convert to cr/lf for proper formatting // need to convert to cr/lf for proper formatting
data = data.replace(/\r?\n/g, '\r\n') socket.emit('data', data.replace(/\r?\n/g, '\r\n').toString('utf-8'));
socket.emit('data', data.toString('utf-8')) });
})
conn.on('ready', function connOnReady () { conn.on('ready', () => {
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 + ' mrhsession=' + socket.request.session.ssh.mrhsession + ' allowreplay=' + socket.request.session.ssh.allowreplay + ' term=' + socket.request.session.ssh.term) debugWebSSH2(
socket.emit('menu', menuData) `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} mrhsession=${socket.request.session.ssh.mrhsession} allowreplay=${socket.request.session.ssh.allowreplay} term=${socket.request.session.ssh.term}`
socket.emit('allowreauth', socket.request.session.ssh.allowreauth) );
socket.emit('setTerminalOpts', socket.request.session.ssh.terminal) socket.emit('menu');
socket.emit('title', 'ssh://' + socket.request.session.ssh.host) socket.emit('allowreauth', socket.request.session.ssh.allowreauth);
if (socket.request.session.ssh.header.background) socket.emit('headerBackground', socket.request.session.ssh.header.background) socket.emit('setTerminalOpts', socket.request.session.ssh.terminal);
if (socket.request.session.ssh.header.name) socket.emit('header', socket.request.session.ssh.header.name) socket.emit('title', `ssh://${socket.request.session.ssh.host}`);
socket.emit('footer', 'ssh://' + socket.request.session.username + '@' + socket.request.session.ssh.host + ':' + socket.request.session.ssh.port) if (socket.request.session.ssh.header.background)
socket.emit('status', 'SSH CONNECTION ESTABLISHED') socket.emit('headerBackground', socket.request.session.ssh.header.background);
socket.emit('statusBackground', 'green') if (socket.request.session.ssh.header.name)
socket.emit('allowreplay', socket.request.session.ssh.allowreplay) socket.emit('header', socket.request.session.ssh.header.name);
conn.shell({ 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, term: socket.request.session.ssh.term,
cols: termCols, cols: termCols,
rows: termRows rows: termRows,
}, function connShell (err, stream) { },
(err, stream) => {
if (err) { if (err) {
SSHerror('EXEC ERROR' + err) SSHerror(`EXEC ERROR${err}`);
conn.end() conn.end();
return return;
} }
// poc to log commands from client socket.on('data', (data) => {
if (socket.request.session.ssh.serverlog.client) var dataBuffer stream.write(data);
socket.on('data', function socketOnData (data) { });
stream.write(data) socket.on('control', (controlData) => {
// poc to log commands from client
if (socket.request.session.ssh.serverlog.client) {
if (data === '\r') {
console.log('serverlog.client: ' + socket.request.session.id + '/' + socket.id + ' host: ' + socket.request.session.ssh.host + ' command: ' + dataBuffer)
dataBuffer = undefined
} else {
dataBuffer = (dataBuffer) ? dataBuffer + data : data
}
}
})
socket.on('control', function socketOnControl (controlData) {
switch (controlData) { switch (controlData) {
case 'replayCredentials': case 'replayCredentials':
if (socket.request.session.ssh.allowreplay) { if (socket.request.session.ssh.allowreplay) {
stream.write(socket.request.session.userpassword + '\n') stream.write(`${socket.request.session.userpassword}\n`);
} }
/* falls through */ /* falls through */
default: default:
console.log('controlData: ' + controlData) debugWebSSH2(`controlData: ${controlData}`);
} }
}) });
socket.on('resize', function socketOnResize (data) { socket.on('resize', (data) => {
stream.setWindow(data.rows, data.cols) stream.setWindow(data.rows, data.cols);
}) });
socket.on('disconnecting', function socketOnDisconnecting (reason) { debugWebSSH2('SOCKET DISCONNECTING: ' + reason) }) socket.on('disconnecting', (reason) => {
socket.on('disconnect', function socketOnDisconnect (reason) { debugWebSSH2(`SOCKET DISCONNECTING: ${reason}`);
debugWebSSH2('SOCKET DISCONNECT: ' + reason) });
err = { message: reason } socket.on('disconnect', (reason) => {
SSHerror('CLIENT SOCKET DISCONNECT', err) debugWebSSH2(`SOCKET DISCONNECT: ${reason}`);
conn.end() const errMsg = { message: reason };
SSHerror('CLIENT SOCKET DISCONNECT', errMsg);
conn.end();
// socket.request.session.destroy() // socket.request.session.destroy()
}) });
socket.on('error', function socketOnError (err) { socket.on('error', (errMsg) => {
SSHerror('SOCKET ERROR', err) SSHerror('SOCKET ERROR', errMsg);
conn.end() conn.end();
}) });
stream.on('data', function streamOnData (data) { socket.emit('data', data.toString('utf-8')) }) stream.on('data', (data) => {
stream.on('close', function streamOnClose (code, signal) { socket.emit('data', data.toString('utf-8'));
err = { message: ((code || signal) ? (((code) ? 'CODE: ' + code : '') + ((code && signal) ? ' ' : '') + ((signal) ? 'SIGNAL: ' + signal : '')) : undefined) } });
SSHerror('STREAM CLOSE', err) stream.on('close', (code, signal) => {
conn.end() const errMsg = {
}) message:
stream.stderr.on('data', function streamStderrOnData (data) { code || signal
console.log('STDERR: ' + data) ? (code ? `CODE: ${code}` : '') +
}) (code && signal ? ' ' : '') +
}) (signal ? `SIGNAL: ${signal}` : '')
}) : undefined,
};
SSHerror('STREAM CLOSE', errMsg);
conn.end();
});
stream.stderr.on('data', (data) => {
console.error(`STDERR: ${data}`);
});
}
);
});
conn.on('end', function connOnEnd (err) { SSHerror('CONN END BY HOST', err) }) conn.on('end', (err) => {
conn.on('close', function connOnClose (err) { SSHerror('CONN CLOSE', err) }) SSHerror('CONN END BY HOST', err);
conn.on('error', function connOnError (err) { SSHerror('CONN ERROR', err) }) });
conn.on('keyboard-interactive', function connOnKeyboardInteractive (name, instructions, instructionsLang, prompts, finish) { conn.on('close', (err) => {
debugWebSSH2('conn.on(\'keyboard-interactive\')') SSHerror('CONN CLOSE', err);
finish([socket.request.session.userpassword]) });
}) conn.on('error', (err) => {
if (socket.request.session.username && (socket.request.session.userpassword || socket.request.session.privatekey) && socket.request.session.ssh) { SSHerror('CONN ERROR', err);
});
conn.on('keyboard-interactive', (name, instructions, instructionsLang, prompts, finish) => {
debugWebSSH2("conn.on('keyboard-interactive')");
finish([socket.request.session.userpassword]);
});
if (
socket.request.session.username &&
(socket.request.session.userpassword || socket.request.session.privatekey) &&
socket.request.session.ssh
) {
// console.log('hostkeys: ' + hostkeys[0].[0]) // console.log('hostkeys: ' + hostkeys[0].[0])
conn.connect({ conn.connect({
host: socket.request.session.ssh.host, host: socket.request.session.ssh.host,
@ -145,49 +233,18 @@ module.exports = function socket (socket) {
readyTimeout: socket.request.session.ssh.readyTimeout, readyTimeout: socket.request.session.ssh.readyTimeout,
keepaliveInterval: socket.request.session.ssh.keepaliveInterval, keepaliveInterval: socket.request.session.ssh.keepaliveInterval,
keepaliveCountMax: socket.request.session.ssh.keepaliveCountMax, keepaliveCountMax: socket.request.session.ssh.keepaliveCountMax,
debug: debug('ssh2') debug: debug('ssh2'),
}) });
} else { } 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)) debugWebSSH2(
socket.emit('ssherror', 'WEBSOCKET ERROR - Refresh the browser and try again') `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.request.session.destroy() socket.handshake
socket.disconnect(true) )}`
} );
socket.emit('ssherror', 'WEBSOCKET ERROR - Refresh the browser and try again');
/** socket.request.session.destroy();
* Error handling for various events. Outputs error to client, logs to socket.disconnect(true);
* server, destroys session and disconnects socket.
* @param {string} myFunc Function calling this function
* @param {object} err error object or error message
*/
// eslint-disable-next-line complexity
function SSHerror (myFunc, err) {
var theError
if (socket.request.session) {
// we just want the first error of the session to pass to the client
socket.request.session.error = (socket.request.session.error) || ((err) ? err.message : undefined)
theError = (socket.request.session.error) ? ': ' + socket.request.session.error : ''
// log unsuccessful login attempt
if (err && (err.level === 'client-authentication')) {
console.log('WebSSH2 ' + 'error: Authentication failure'.red.bold +
' user=' + socket.request.session.username.yellow.bold.underline +
' from=' + socket.handshake.address.yellow.bold.underline)
socket.emit('allowreauth', socket.request.session.ssh.allowreauth)
socket.emit('reauth')
} else {
console.log('WebSSH2 Logout: user=' + socket.request.session.username + ' from=' + socket.handshake.address + ' host=' + socket.request.session.ssh.host + ' port=' + socket.request.session.ssh.port + ' sessionID=' + socket.request.sessionID + '/' + socket.id + ' allowreplay=' + socket.request.session.ssh.allowreplay + ' term=' + socket.request.session.ssh.term)
if (err) {
theError = (err) ? ': ' + err.message : ''
console.log('WebSSH2 error' + theError)
}
}
socket.emit('ssherror', 'SSH ' + myFunc + theError)
socket.request.session.destroy()
socket.disconnect(true)
} else {
theError = (err) ? ': ' + err.message : ''
socket.disconnect(true)
}
debugWebSSH2('SSHerror ' + myFunc + theError)
} }
} }
setupConnection();
};

View file

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