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:
parent
379335b9e7
commit
6bc9ffe2ed
31 changed files with 1898 additions and 15248 deletions
16
.devcontainer/Dockerfile
Normal file
16
.devcontainer/Dockerfile
Normal 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>"
|
31
.devcontainer/devcontainer.json
Normal file
31
.devcontainer/devcontainer.json
Normal 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"
|
||||||
|
}
|
|
@ -1 +0,0 @@
|
||||||
**/*{.,-}min.js
|
|
277
.eslintrc.yml
277
.eslintrc.yml
|
@ -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
|
|
6
.github/workflows/build_publish.yml
vendored
6
.github/workflows/build_publish.yml
vendored
|
@ -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 }}
|
|
@ -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`
|
||||||
|
|
||||||
|
|
20
ChangeLog.md
20
ChangeLog.md
|
@ -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"]
|
||||||
|
|
|
@ -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" ]
|
||||||
|
|
16
README.md
16
README.md
|
@ -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
24
app/.eslintrc.json
Normal 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
4
app/.prettierrc
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
{
|
||||||
|
"printWidth": 100,
|
||||||
|
"singleQuote": true
|
||||||
|
}
|
|
@ -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
|
@ -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 */
|
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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
282
app/client/src/js/index.ts
Normal 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
9
app/client/tsconfig.json
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"outDir": "./built",
|
||||||
|
"allowJs": true,
|
||||||
|
"target": "es6",
|
||||||
|
"moduleResolution": "node"
|
||||||
|
},
|
||||||
|
"include": ["./src/**/*"],
|
||||||
|
}
|
|
@ -10,6 +10,7 @@
|
||||||
"name": null,
|
"name": null,
|
||||||
"password": null,
|
"password": null,
|
||||||
"privatekey": null
|
"privatekey": null
|
||||||
|
"overridebasic": false
|
||||||
},
|
},
|
||||||
"ssh": {
|
"ssh": {
|
||||||
"host": null,
|
"host": null,
|
||||||
|
|
28
app/index.js
28
app/index.js
|
@ -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
5471
app/package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
@ -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"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
|
@ -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',
|
||||||
}
|
},
|
||||||
})
|
});
|
||||||
|
|
|
@ -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,
|
||||||
}
|
},
|
||||||
})]
|
}),
|
||||||
}
|
],
|
||||||
})
|
},
|
||||||
|
});
|
||||||
|
|
|
@ -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
102
app/server/config.js
Normal 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;
|
|
@ -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());
|
||||||
}
|
},
|
||||||
}
|
};
|
||||||
|
|
|
@ -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();
|
||||||
|
};
|
||||||
|
|
|
@ -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';
|
||||||
}
|
};
|
||||||
|
|
Loading…
Reference in a new issue