Compare commits

...

129 commits

Author SHA1 Message Date
Bill Church
510b91c85a
feat: update node packages for node v18+ 2024-10-14 20:27:26 +00:00
Bill Church
6ff58c55f5
chore: update debug messages 2024-10-14 19:33:50 +00:00
Bill Church
8d515f6a77
chore: update debug for watch 2024-10-14 19:33:29 +00:00
Bill Church
f336cc0df6
chore: update dev docs 2024-08-23 11:28:19 +00:00
Bill Church
f8be9e1704
chore: images 2024-08-23 01:37:32 +00:00
Bill Church
27d9bfb97e
fix: pass full ssh error to browser 2024-08-23 01:19:19 +00:00
Bill Church
3133ad8e97
chore: update readme 2024-08-23 01:19:00 +00:00
Bill Church
f24693b672
chore: update test server 2024-08-23 00:55:28 +00:00
Bill Church
34afe70dd5
chore(release): 0.2.20 2024-08-22 18:13:37 +00:00
Bill Church
0d1aaef4fb
chore: dev env 2024-08-22 18:13:24 +00:00
Bill Church
63a7ab8061
chore(release): 0.2.19 2024-08-22 18:06:55 +00:00
Bill Church
2f5a2275d6
chore(release): 0.2.18 2024-08-22 18:06:36 +00:00
Bill Church
fdb846bd7d
chore(release): 0.2.18 2024-08-22 17:54:05 +00:00
Bill Church
0f3c7ab230
feat: ssh keyboard-interactive authentication support 2024-08-22 17:27:48 +00:00
Bill Church
1c4bfc2680
chore: code linting 2024-08-22 16:07:47 +00:00
Bill Church
d96b299e7d
chore: update maskSensitiveData function for new module 2024-08-22 16:07:33 +00:00
Bill Church
12e5431a34
feat: express.js session secret configurable in docker with WEBSSH_SESSION_SECRET env variable 2024-08-22 16:06:49 +00:00
Bill Church
06063a8009
chore: update jsmasker@1.2.0 2024-08-22 14:57:47 +00:00
Bill Church
60eae6a165
chore: changelog 2024-08-22 02:34:32 +00:00
Bill Church
66e0eeccde
chore(release): 0.2.17 2024-08-22 02:33:53 +00:00
Bill Church
ca0c33004f
chore: dev env 2024-08-22 02:33:42 +00:00
Bill Church
83fb54d98f
chore: dev env 2024-08-22 02:32:49 +00:00
Bill Church
c19dbf019d
chore: update package.json 2024-08-22 02:11:24 +00:00
Bill Church
87d5f5ee3f
chore: socket.js test 2024-08-22 02:08:26 +00:00
Bill Church
eb551d1c4a
chore: create additional tests 2024-08-22 01:34:18 +00:00
Bill Church
1a5ebc649d
chore: create initial tests 2024-08-22 01:11:45 +00:00
Bill Church
0899cb0efa
chore: template keyboard-interactive 2024-08-21 20:51:33 +00:00
Bill Church
2bd225e12e
chore: refactoring 2024-08-21 20:00:34 +00:00
Bill Church
01ddac958e
chore: minor refactoring 2024-08-21 19:44:47 +00:00
Bill Church
6f694a96a7
chore: linting 2024-08-21 19:31:56 +00:00
Bill Church
2c3a89b5dc
chore: refactor jsmasker calls
chore: implement constants
2024-08-21 19:16:40 +00:00
Bill Church
47910dd066
chore: update README.md 2024-08-21 19:07:44 +00:00
Bill Church
17bc82db85
feat: get HTTP session secret from WEBSSH_SESSION_SECRET env if available. 2024-08-21 17:44:15 +00:00
Bill Church
cd56b3c51f
chore: implement constants 2024-08-21 17:43:24 +00:00
Bill Church
4196ef3554
chore: implement constants 2024-08-21 17:42:45 +00:00
Bill Church
5a65b6e91d
chore: implement constants 2024-08-21 17:42:22 +00:00
Bill Church
13fd49e008
chore: update dev env 2024-08-21 15:10:58 +00:00
Bill Church
66ce643dd9
chore: refactor debugging, logging and error handling. 2024-08-21 15:08:31 +00:00
Bill Church
e30c0c1c9b
chore: remove unused eslint config 2024-08-21 14:21:48 +00:00
Bill Church
76b2f787fd
chore: refactor socket.js 2024-08-21 12:56:13 +00:00
Bill Church
edceab346d
chore: update docs 2024-08-21 12:25:57 +00:00
Bill Church
d387cb796d
chore: linting 2024-08-21 12:25:32 +00:00
Bill Church
da21c89f20
chore: linting 2024-08-21 11:04:28 +00:00
Bill Church
dbcfc30cd0
chore: update dev 2024-08-21 11:01:54 +00:00
Bill Church
e06fabc2a7
feat: update webssh2_client@0.2.23 2024-08-20 19:56:21 +00:00
Bill Church
46338e4f2c
chore: introduce jsmasker to mask sensitive debug logging data. 2024-08-20 19:42:06 +00:00
Bill Church
01e312d701
chore: establish mascot 2024-08-20 19:41:14 +00:00
Bill Church
a7384a8126
chore: update dockerfile 2024-08-19 21:26:39 +00:00
Bill Church
b469cb63c7
chore: Update Dockerfile to use Debian bookworm-slim base image and install Node.js using nvm 2024-08-19 19:57:11 +00:00
Bill Church
5cf06dd46e
chore: update Dockerfile 2024-08-19 19:42:10 +00:00
Bill Church
e3ec6f08eb
chore: update docs 2024-08-19 19:10:08 +00:00
Bill Church
3e45c98c62
feat: Switch User or reauth feature for Basic Auth sessions
feat: update webssh2 to 0.2.22
2024-08-19 19:00:45 +00:00
Bill Church
a530f59704
feat: Switch User or reauth feature for Basic Auth sessions 2024-08-19 18:51:07 +00:00
Bill Church
96ee27fe2b
CHORE: update README.md 2024-08-19 12:38:14 +00:00
Bill Church
324e209df5
chore: update README.md
chore: update DEPRECATED.md
2024-08-19 12:36:57 +00:00
Bill Church
e1fc4fee1b
docs: README.md 2024-08-19 08:29:04 -04:00
Bill Church
cfe9b5cb3d
docs: update README.md
docs: add DEPRECATED.md
2024-08-19 12:26:48 +00:00
Bill Church
60d2528437
refactor: remove unused terminal configuration options
refactor: remove unused serverlog configuration options
refactor: remove unused accesslog configuration options
refactor: remove unused verify configuration options
2024-08-19 11:48:52 +00:00
Bill Church
252e4f16d6
chore: dev env cleanup package.json 2024-08-19 11:38:07 +00:00
Bill Church
c9591d637d
fix: enable autoConnect only on /ssh/host/ 2024-08-19 11:36:34 +00:00
Bill Church
9cfccb109a
feat: update webssh2_client 0.2.21 2024-08-19 11:01:03 +00:00
Bill Church
9b94627cbd
feat: update webssh2_client 0.2.20 2024-08-18 13:03:19 +00:00
Bill Church
7017aef56b
docs: event flow diagram 2024-08-18 13:03:08 +00:00
Bill Church
589f12b72c
chore: formatting 2024-08-18 13:02:57 +00:00
Bill Church
b5ee677ac6
chore: refactor split ssh and socket to individual modules 2024-08-18 13:02:39 +00:00
Bill Church
ad627e6596
chore: Update config.js to read config file synchronously and merge configuration 2024-08-17 16:19:19 +00:00
Bill Church
b4cbfb4b46
feat: validate handleResize
feat: validate handleControl
2024-08-17 14:25:44 +00:00
Bill Church
aab1a35bc9
feat: validate handleTerminal
feat: validate handleAuthenticate
2024-08-17 14:21:31 +00:00
Bill Church
28f329e315
feat: validateSshTerm checks if term is undefined or null before validation 2024-08-17 14:20:01 +00:00
Bill Church
82c0da0ff7
chore: refactor validateSshTerm into utils.js 2024-08-17 13:10:23 +00:00
Bill Church
72d747763c
feat: routes.js validate input from url parameters 2024-08-17 12:57:22 +00:00
Bill Church
5c8c1a2333
chore: dev env add validator package 2024-08-17 10:48:53 +00:00
Bill Church
303f53dc43
fix: honor ssh.term settings as default when url param sshTerm is undefined 2024-08-17 10:41:56 +00:00
Bill Church
40f6bf232b
chore: build env 2024-08-17 10:40:48 +00:00
Bill Church
418af1bcfa
feat: update webssh2_client to 0.2.19 2024-08-16 23:03:26 +00:00
Bill Church
e2ea06866e
feat: add allowReconnect, allowReauth, and autoLog features, normalize debug logs 2024-08-16 22:58:53 +00:00
Bill Church
e3f97ad6b6
chore: dev env 2024-08-16 22:47:58 +00:00
Bill Church
ea017016b7
fix: sanitize object no longer mutates original object 2024-08-16 22:47:42 +00:00
Bill Church
f14f0bccf5
chore: dev env 2024-08-16 19:22:41 +00:00
Bill Church
8671180f18
chore: major refactoring 2024-08-16 19:22:17 +00:00
Bill Church
fc102380cb
chore: update webssh_client 0.2.18 2024-08-15 18:40:23 +00:00
Bill Church
b9ca79e7cf
fix: correct handling of sshTerm query parameters 2024-08-14 01:05:51 +00:00
Bill Church
266f9876d3
chore: light refactoring 2024-08-13 23:30:44 +00:00
Bill Church
cc8b014af8
chore: update dev env 2024-08-13 23:30:06 +00:00
Bill Church
a0affca261
feat: HTTP Basic Authentication and auto-connection with /ssh/host/<hostIP> 2024-08-13 17:31:27 +00:00
Bill Church
aec8be86b4
chore: utils function sanitizeObject Recursively sanitizes an object by replacing the value of any password property with asterisks (*) matching the length of the original password. 2024-08-13 17:30:32 +00:00
Bill Church
1fc35f74da
fix: handle http basic auth in /ssh/host/ route 2024-08-13 14:30:32 +00:00
Bill Church
aa633aef0b
chore: formatting 2024-08-13 12:38:00 +00:00
Bill Church
40715023b2
chore: dev env 2024-08-13 12:25:53 +00:00
Bill Church
d7b73f95d2
chore: dev env 2024-08-13 12:25:47 +00:00
Bill Church
c573b9b989
chore: cleanup 2024-08-13 12:25:35 +00:00
Bill Church
a7bc5e2da1
chore: dev env 2024-08-13 10:43:26 +00:00
Bill Church
66c75643d7
chore: update debug logging 2024-08-13 10:43:13 +00:00
Bill Church
617ce151c0
chore: refactor logging to debug 2024-08-13 10:42:34 +00:00
Bill Church
650f4eb8f0
fix: vareiable scoping for conn and stream would prevent multiple user sessions 2024-08-09 02:21:44 +00:00
Bill Church
20c3915832
chore: Update webssh2_server dependency to version 0.2.16 2024-07-19 23:19:28 +00:00
Bill Church
bb0b6cca58
chore: Update webssh2_client dependency to version 0.2.16 2024-07-19 23:16:31 +00:00
Bill Church
f9e6fd8351
chore: version bump 2024-07-19 20:03:48 +00:00
Bill Church
39dfdcb5ae
chore: update changelog 2024-07-19 20:03:40 +00:00
Bill Church
8686215ad1
docs: 📝 add conventional commits extension 2024-07-19 13:19:54 +00:00
Bill Church
a1f4c7b985
chore: Update webssh2_client dependency to use the 'main' branch 2024-07-19 10:47:13 +00:00
Bill Church
beee8e63e8
chore: Update tools.sh script to create allowed_signers file 2024-07-19 10:46:47 +00:00
Bill Church
afe462b180
feat: Add session-based authentication for SSH connections using HTTP Basic auth and express.js 2024-07-18 17:13:23 +00:00
Bill Church
fe7248e056
feat: Update connectionHandler.js and routes.js to propmpt for basic credentials when accessing /ssh/host/<address> and pre-populate credentials and host info AND auto-connect to server. 2024-07-18 15:59:08 +00:00
Bill Church
e39fb885fd
feat: Inject SSH host and port into webssh2 configuration 2024-07-18 15:35:41 +00:00
Bill Church
8fcf4b7b75
fix: Serve the static files from the webssh2_client module with a custom prefix '/ssh/assets' instead of just '/ssh'. 2024-07-18 15:05:04 +00:00
Bill Church
2d19f49091
feat: Add SSH routes and connection handler 2024-07-18 14:59:03 +00:00
Bill Church
7d80e10604
chore: Add server comments to code files 2024-07-18 14:58:35 +00:00
Bill Church
32af90bc3f
chore: Update npm dependencies and refactor server startup code, add express.js, host webssh2 client through webssh2-client module. 2024-07-18 13:45:37 +00:00
Bill Church
27e79df8b1
refactor: Update socket.js to use consistent naming for allowReplay and allowReauth options 2024-07-18 13:44:57 +00:00
Bill Church
c887a64f83
refactor: Update allowReauth option naming consistency 2024-07-17 12:37:56 +00:00
Bill Church
4360f546ee
refactor: Remove console.log statement in config.js 2024-07-16 17:39:36 +00:00
Bill Church
d9931334de
refactor: Improve socket authentication error handling and message 2024-07-16 17:39:30 +00:00
Bill Church
1ecf19c5df
chore: Add ajv dependency and refactor server startup code 2024-07-16 14:21:20 +00:00
Bill Church
50f1769fd5
chore: minor refactoring 2024-07-11 21:14:35 +00:00
Bill Church
ff978703da
chore: removed unused config options 2024-07-11 21:09:20 +00:00
Bill Church
bf50fca786
chore: client / server bifurcation mostly complete 2024-07-11 21:07:29 +00:00
Bill Church
b6e5089ee6
chore: stage one complete 2024-07-11 20:45:04 +00:00
Bill Church
25f52b3f1e
chore: update docs 2024-07-11 11:24:59 +00:00
Bill Church
3ecda672ba
chore: initial bifurcation of client and server code 2024-07-11 11:23:23 +00:00
Bill Church
ea12cc8b7e
fix: version comment in client.html 2024-07-10 12:03:10 -04:00
Bill Church
cfa097bd0e
chore: swap read-config git to read-config-ng 2024-07-10 11:00:53 -04:00
Bill Church
170047517b
chore: build 0.2.12 web assets 2024-07-10 13:50:36 +00:00
Bill Church
cd64cc0637
chore: update webpack 2024-07-10 13:50:18 +00:00
Bill Church
533f719cca
chore: dev build testing 2024-07-10 13:09:24 +00:00
Bill Church
b8782c565a
chore: release 0.2.12 2024-07-10 12:18:21 +00:00
Bill Church
0dda8d56d9
build: release 0.2.12 2024-07-10 12:13:53 +00:00
Bill Church
24c94aed98
chore: update dev environment 2024-07-10 12:12:33 +00:00
Bill Church
7223f2cd8f chore: big-ip specific release v0.2.11 2021-05-12 13:59:43 -04:00
97 changed files with 13690 additions and 2283 deletions

View file

@ -1,21 +0,0 @@
{
"critics": {
"lint": {
"engine": "standard"
},
"wc": {
"limit": 5000
}
},
"dependencies": {
"mute": [
"read-config",
"socket.io",
"standard",
"bithound"
]
},
"ignore": [
"public/webssh2.bundle.js",
]
}

View file

@ -1,36 +0,0 @@
---
engines:
csslint:
enabled: true
exclude_paths:
- "client/public/*"
duplication:
exclude_paths:
- "client/public/*"
- "workspace/*"
enabled: true
config:
languages:
- ruby
- javascript
- python
- php
eslint:
enabled: true
fixme:
enabled: true
ratings:
paths:
- "**.css"
- "**.inc"
- "**.js"
- "**.jsx"
- "**.module"
- "**.php"
- "**.py"
- "**.rb"
exclude_paths:
- "node_modules/"
- "client/public/*"
- "workspace/*"

View file

@ -1,2 +0,0 @@
--exclude-exts=.min.css
--ignore=adjoining-classes,box-model,ids,order-alphabetical,unqualified-attributes

View file

@ -0,0 +1,39 @@
{
"name": "Debian Bullseye Node.js DevContainer",
"image": "mcr.microsoft.com/vscode/devcontainers/base:bullseye",
"features": {
"ghcr.io/devcontainers/features/node:1": {
"version": "6.9.1"
},
"ghcr.io/devcontainers/features/docker-outside-of-docker:1": {}
},
// mount the ssh public identity file for the this project
// I limit to just what I need and not the whole ~/.ssh folder
"mounts": [
"source=${localEnv:HOME}${localEnv:USERPROFILE}/.ssh/personal_id_rsa.pub,target=/home/vscode/.hostssh/id_rsa.pub,readonly,type=bind,consistency=cached"
],
"customizations": {
"vscode": {
"extensions": [
"bierner.markdown-mermaid",
"dbaeumer.vscode-eslint",
"esbenp.prettier-vscode",
"GitHub.copilot-chat",
"GitHub.copilot",
"mohsen1.prettify-json",
"mechatroner.rainbow-csv",
"ms-vscode-remote.remote-containers",
"oderwat.indent-rainbow",
"rvest.vs-code-prettier-eslint",
"stylelint.vscode-stylelint",
"vivaxy.vscode-conventional-commits"
]
},
"settings": {
"terminal.integrated.defaultProfile.linux": "bash"
}
},
"postCreateCommand": "/bin/bash ./.devcontainer/scripts/tools.sh >> ~/post-create-tools.log",
"remoteUser": "vscode"
}

View file

@ -0,0 +1,43 @@
#!/bin/bash
mkdir -p ~/.ssh && \
touch ~/.ssh/known_hosts && \
sudo tee ~/.ssh/config > /dev/null << EOF
Host github.com
HostName github.com
PreferredAuthentications publickey
IdentityFile ~/.hostssh/id_rsa.pub
EOF
sudo chown -R vscode:vscode ~/.ssh && \
sudo chmod 600 ~/.ssh/config && \
sudo chmod 600 ~/.ssh/known_hosts
git config --global --add safe.directory ${PWD}
# Get the signing key from git config
signing_key=$(git config --get user.signingkey)
if [ -z "$signing_key" ]; then
echo "No signing key found in git config."
exit 1
fi
# Get the user email from git config
user_email=$(git config --get user.email)
if [ -z "$user_email" ]; then
echo "No user email found in git config."
exit 1
fi
# Create the ~/.ssh directory if it doesn't exist
mkdir -p ~/.ssh
# Write the signing key and email to the allowed_signers file
echo "$user_email $signing_key" > ~/.ssh/allowed_signers
# Set the correct permissions for the allowed_signers file
chmod 644 ~/.ssh/allowed_signers
echo "allowed_signers file created successfully."
npm install

28
.eslintrc.yaml Normal file
View file

@ -0,0 +1,28 @@
extends:
- airbnb-base
- prettier
- plugin:node/recommended
- plugin:jest/recommended
plugins:
- prettier
- jest
env:
jest/globals: true
rules:
prettier/prettier: error
no-unused-vars: warn
no-console: off
func-names: off
no-process-exit: off
object-shorthand: off
class-methods-use-this: off
semi: [2, never]
overrides:
- files:
- "**/*.test.js"
env:
jest: true

View file

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

View file

@ -0,0 +1,69 @@
---
name: 'Build Docker On Tag'
on:
push:
branches:
- bigip-server
tags:
- 'v[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}'
workflow_dispatch: # Allows manual triggering from the GitHub UI
jobs:
docker:
runs-on: ubuntu-latest
steps:
- name: 'Checkout'
uses: actions/checkout@v3
- name: Prepare
id: prep
run: |
DOCKER_IMAGE=${{ secrets.DOCKER_USERNAME }}/${GITHUB_REPOSITORY#*/}
# If this is a git tag, use the tag name as a docker tag
if [[ $GITHUB_REF == refs/tags/* ]]; then
VERSION=${GITHUB_REF#refs/tags/v}
TAGS="${DOCKER_IMAGE}:${VERSION}"
fi
# If this is a git branch, use the branch name as a docker tag
if [[ $GITHUB_REF == refs/heads/* ]]; then
VERSION=${GITHUB_REF#refs/heads/}
TAGS="${DOCKER_IMAGE}:${VERSION}"
fi
# If the VERSION looks like a version number, also tag as 'latest'
if [[ $VERSION =~ ^[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}$ ]]; then
TAGS="$TAGS,${DOCKER_IMAGE}:latest"
fi
# Set output parameters
echo ::set-output name=tags::${TAGS}
echo ::set-output name=docker_image::${DOCKER_IMAGE}
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
with:
platforms: all
- name: Set up Docker Buildx
id: buildx
uses: docker/setup-buildx-action@v3
- name: Login to DockerHub
if: github.event_name != 'pull_request'
uses: docker/login-action@v2
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
- name: Build
uses: docker/build-push-action@v4
with:
builder: ${{ steps.buildx.outputs.name }}
context: .
file: ./Dockerfile
platforms: linux/amd64,linux/arm64,linux/ppc64le
push: true
tags: ${{ steps.prep.outputs.tags }}

1
.gitignore vendored
View file

@ -49,3 +49,4 @@ jspm_packages
.DS_Store
Build/Release
app/todo.md

1
.nvmrc Normal file
View file

@ -0,0 +1 @@
v20.18.0

5
.prettierrc Normal file
View file

@ -0,0 +1,5 @@
{
"semi": false,
"singleQuote": false,
"trailingComma": "none"
}

4
.snyk
View file

@ -1,4 +0,0 @@
# Snyk (https://snyk.io) policy file, patches or ignores known vulnerabilities.
version: v1.13.1
ignore: {}
patch: {}

1
.tool-versions Normal file
View file

@ -0,0 +1 @@
nodejs 6.9.1

View file

@ -1,4 +0,0 @@
language: node_js
node_js:
- 6
- 10

39
BUILD.md Normal file
View file

@ -0,0 +1,39 @@
## Issues with building
Working with node 6.9.1 has uncovered some challenges from time to time. Some of these are related to node 6.9.1 and others may not be, however these are some of the issues and fixes I've come across:
### npm ERR! Error: EPERM: operation not permitted, utime
This error with `npm` is related to be related to file permission issues, specifically the `EPERM` (Error: Operation not permitted) error. This is typically caused by the `npm` process trying to modify or access files in a way that the user running the process doesn't have permission to do.
Here are some steps you can try to resolve this issue:
1. **Ensure Correct File Permissions**:
- Check the ownership and permissions of the directory `/workspaces/webssh2/node_modules/.staging/esquery-7b94f06a/dist` and its parent directories.
- You can try running:
```bash
sudo chown -R $(whoami) /workspaces/webssh2/node_modules
```
- Alternatively, ensure that the `vscode` user has the necessary permissions to access and modify these files.
2. **Run npm install as the correct user**:
- Make sure you're not accidentally running `npm` as a root or another user within the Docker container. If you're running it as the `vscode` user, ensure that the `vscode` user has the appropriate permissions in the `/workspaces/webssh2` directory.
3. **Clear npm cache**:
- Sometimes this error can be caused by a corrupt npm cache. You can try clearing the cache:
```bash
npm cache clean --force
```
- After clearing the cache, try running `npm install` again.
4. **Check for Staging Issues**:
- The error is occurring in a `.staging` directory, which is used by `npm` during the installation process. If the `.staging` directory is corrupted or incomplete, it can cause issues. You can remove the `node_modules` directory and try reinstalling:
```bash
rm -rf node_modules
npm install
```
5. **Try Running as Root (Not Recommended for Production)**:
- As a last resort, you can try running `npm install` as root within the container. However, this is not recommended due to potential security risks:
```bash
sudo npm install
```

View file

@ -1 +0,0 @@
e2e70f7d2949b6c8fe0299f888a3725763a62c01a1faea1fb729babc2ed51c92 Build/Release/BIG-IP-ILX-WebSSH2-0.2.8.tgz

View file

@ -1,46 +0,0 @@
# Contributor Covenant Code of Conduct
## Our Pledge
In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation.
## Our Standards
Examples of behavior that contributes to creating a positive environment include:
* Using welcoming and inclusive language
* Being respectful of differing viewpoints and experiences
* Gracefully accepting constructive criticism
* Focusing on what is best for the community
* Showing empathy towards other community members
Examples of unacceptable behavior by participants include:
* The use of sexualized language or imagery and unwelcome sexual attention or advances
* Trolling, insulting/derogatory comments, and personal or political attacks
* Public or private harassment
* Publishing others' private information, such as a physical or electronic address, without explicit permission
* Other conduct which could reasonably be considered inappropriate in a professional setting
## Our Responsibilities
Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior.
Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful.
## Scope
This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers.
## Enforcement
Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at wmchurch@gmail.com. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately.
Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership.
## Attribution
This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version]
[homepage]: http://contributor-covenant.org
[version]: http://contributor-covenant.org/version/1/4/

View file

@ -1,94 +0,0 @@
Guidelines for contributing code:
Make sure code passes linting from (StandardJS)[https://standardjs.com/]
# Contributing
When contributing to this repository, please first discuss the change you wish to make via issue,
email, or any other method with the owners of this repository before making a change.
Please note we have a code of conduct, please follow it in all your interactions with the project.
## Pull Request Process
1. Ensure any install or build dependencies are removed before the end of the layer when doing a
build.
2. Make sure code passes linting from (StandardJS)[https://standardjs.com/]
3. Explain what you're trying to accomplish in your commits
4. Update changelog and Readme if needed.
## Code of Conduct
### Our Pledge
In the interest of fostering an open and welcoming environment, we as
contributors and maintainers pledge to making participation in our project and
our community a harassment-free experience for everyone, regardless of age, body
size, disability, ethnicity, gender identity and expression, level of experience,
nationality, personal appearance, race, religion, or sexual identity and
orientation.
### Our Standards
Examples of behavior that contributes to creating a positive environment
include:
* Using welcoming and inclusive language
* Being respectful of differing viewpoints and experiences
* Gracefully accepting constructive criticism
* Focusing on what is best for the community
* Showing empathy towards other community members
Examples of unacceptable behavior by participants include:
* The use of sexualized language or imagery and unwelcome sexual attention or
advances
* Trolling, insulting/derogatory comments, and personal or political attacks
* Public or private harassment
* Publishing others' private information, such as a physical or electronic
address, without explicit permission
* Other conduct which could reasonably be considered inappropriate in a
professional setting
### Our Responsibilities
Project maintainers are responsible for clarifying the standards of acceptable
behavior and are expected to take appropriate and fair corrective action in
response to any instances of unacceptable behavior.
Project maintainers have the right and responsibility to remove, edit, or
reject comments, commits, code, wiki edits, issues, and other contributions
that are not aligned to this Code of Conduct, or to ban temporarily or
permanently any contributor for other behaviors that they deem inappropriate,
threatening, offensive, or harmful.
### Scope
This Code of Conduct applies both within project spaces and in public spaces
when an individual is representing the project or its community. Examples of
representing a project or community include using an official project e-mail
address, posting via an official social media account, or acting as an appointed
representative at an online or offline event. Representation of a project may be
further defined and clarified by project maintainers.
### Enforcement
Instances of abusive, harassing, or otherwise unacceptable behavior may be
reported by contacting the project team at [INSERT EMAIL ADDRESS]. All
complaints will be reviewed and investigated and will result in a response that
is deemed necessary and appropriate to the circumstances. The project team is
obligated to maintain confidentiality with regard to the reporter of an incident.
Further details of specific enforcement policies may be posted separately.
Project maintainers who do not follow or enforce the Code of Conduct in good
faith may face temporary or permanent repercussions as determined by other
members of the project's leadership.
### Attribution
This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
available at [http://contributor-covenant.org/version/1/4][version]
[homepage]: http://contributor-covenant.org
[version]: http://contributor-covenant.org/version/1/4/

401
ChangeLog-Old.md Normal file
View file

@ -0,0 +1,401 @@
<a name="0.2.17"></a>
## 0.2.17 (2024-08-22)
### Bug Fixes
* correct handling of sshTerm query parameters ([b9ca79e](https://github.com/billchurch/WebSSH2/commit/b9ca79e))
* enable `autoConnect` only on `/ssh/host/` ([c9591d6](https://github.com/billchurch/WebSSH2/commit/c9591d6))
* handle http basic auth in `/ssh/host/` route ([1fc35f7](https://github.com/billchurch/WebSSH2/commit/1fc35f7))
* honor `ssh.term` settings as default when url param `sshTerm` is undefined ([303f53d](https://github.com/billchurch/WebSSH2/commit/303f53d))
* sanitize object no longer mutates original object ([ea01701](https://github.com/billchurch/WebSSH2/commit/ea01701))
* Serve the static files from the webssh2_client module with a custom prefix '/ssh/assets' instead of just '/ssh'. ([8fcf4b7](https://github.com/billchurch/WebSSH2/commit/8fcf4b7))
* vareiable scoping for conn and stream would prevent multiple user sessions ([650f4eb](https://github.com/billchurch/WebSSH2/commit/650f4eb))
* version comment in client.html ([ea12cc8](https://github.com/billchurch/WebSSH2/commit/ea12cc8))
### Features
* `Switch User` or `reauth` feature for Basic Auth sessions ([3e45c98](https://github.com/billchurch/WebSSH2/commit/3e45c98))
* `Switch User` or `reauth` feature for Basic Auth sessions ([a530f59](https://github.com/billchurch/WebSSH2/commit/a530f59))
* add allowReconnect, allowReauth, and autoLog features, normalize debug logs ([e2ea068](https://github.com/billchurch/WebSSH2/commit/e2ea068))
* Add session-based authentication for SSH connections using HTTP Basic auth and express.js ([afe462b](https://github.com/billchurch/WebSSH2/commit/afe462b))
* Add SSH routes and connection handler ([2d19f49](https://github.com/billchurch/WebSSH2/commit/2d19f49))
* get HTTP session secret from `WEBSSH_SESSION_SECRET` env if available. ([17bc82d](https://github.com/billchurch/WebSSH2/commit/17bc82d))
* HTTP Basic Authentication and auto-connection with /ssh/host/<hostIP> ([a0affca](https://github.com/billchurch/WebSSH2/commit/a0affca))
* Inject SSH host and port into webssh2 configuration ([e39fb88](https://github.com/billchurch/WebSSH2/commit/e39fb88))
* routes.js validate input from url parameters ([72d7477](https://github.com/billchurch/WebSSH2/commit/72d7477))
* Update connectionHandler.js and routes.js to propmpt for basic credentials when accessing `/ssh/host/<address>` and pre-populate credentials and host info AND auto-connect to server. ([fe7248e](https://github.com/billchurch/WebSSH2/commit/fe7248e))
* update webssh2_client 0.2.20 ([9b94627](https://github.com/billchurch/WebSSH2/commit/9b94627))
* update webssh2_client 0.2.21 ([9cfccb1](https://github.com/billchurch/WebSSH2/commit/9cfccb1))
* update webssh2_client to 0.2.19 ([418af1b](https://github.com/billchurch/WebSSH2/commit/418af1b))
* update webssh2_client@0.2.23 ([e06fabc](https://github.com/billchurch/WebSSH2/commit/e06fabc))
* validate handleResize ([b4cbfb4](https://github.com/billchurch/WebSSH2/commit/b4cbfb4))
* validate handleTerminal ([aab1a35](https://github.com/billchurch/WebSSH2/commit/aab1a35))
* validateSshTerm checks if term is undefined or null before validation ([28f329e](https://github.com/billchurch/WebSSH2/commit/28f329e))
<a name="0.2.12"></a>
## 0.2.12 (2024-07-10)
BIG-IP Specific version
### Changes
- `[ctrl]+[shift]+[6]` or `[ctrl]+[^]` now sends `RS` or `0x1E`
## [0.2.11] 2
<a name="0.2.11"></a>
## 0.2.11 (2021-05-12)
BIG-IP Specific version
### BREAKING
- Not compatible with versions of ephemeral_auth before 0.4.8 due to child resources moving under /ssh
### Changes
- in `config.json.sample` - `allowreauth` set to `false` by default
- in `config.json.sample` - potential future proofing for CORS support `http.origins`
- `ssh` module updated to 0.8.9
- Move all child resources to start from under /ssh
- /socket.io -> /ssh/socket.io
- /webssh2.css -> /ssh/webssh2.css
- /webssh2.bundle.js -> /ssh/webssh2.bundle.js
- /reauth -> /ssh/reauth
- perhaps more
<a name="0.2.9"></a>
## 0.2.9 (2019-06-13)
### Changes
- Missing require('fs') in `server/app.js` See issue [#135](../../issues/135)
- Patched read-config to mitigate vulnerability in js-yaml
- issue not exploitable on webssh2 implementation
- patched anyway
- sending my patch upstream to read-config, webssh2 package.json points to patched version in my repository https://github.com/billchurch/nodejs-read-config
- See https://github.com/nodeca/js-yaml/issues/475 for more detail
<a name="0.2.8"></a>
## 0.2.8 (2019-05-26)
### Changes
- Fixes issue if no password is entered, browser must be closed and restart to attempt to re-auth. See issue [#118](../../issues/118). Thanks @smilesm2 for the idea.
- fixes broken `npm run (build|builddev)`
- update font-awesome fonts to 5.6.3
- update webpack and dependancies
- update xterm to 3.8.0
### Fixes
- ILX
<a name="0.2.7"></a>
## 0.2.7 (2018-11-11)
### Changes
- `config.reauth` was not respected if initial auth presented was incorrect, regardless of `reauth` setting in `config.json` reauth would always be attempted. fixes [#117](../../issues/117)
- **BREAKING** moved app files to /app, this may be a breaking change
- Updated dockerfile for new app path
- Updated app dependancies
- xterm v3.8.0
- https://github.com/xtermjs/xterm.js/releases/tag/3.8.0
- basic-auth v2.0.1
- https://github.com/jshttp/basic-auth/releases/tag/v2.0.1
- express v4.16.4
- https://github.com/expressjs/express/releases/tag/4.16.4
- validator v10.9.0
- https://github.com/chriso/validator.js/releases/tag/10.9.0
- Updated dev dependancies
- snazzy v8.0.0
- standard v12.0.1
- uglifyjs-webpack-plugin v2.0.1
- ajv v6.5.5
- copy-webpack-plugin v4.6.0
- css-loader v1.0.1
- nodemon v1.18.6
- postcss-discard-comments v4.0.1
- snyk v1.108.2
- url-loader v1.1.2
- webpack v4.25.1
- webpack-cli v3.1.2
<a name="0.2.6"></a>
## 0.2.6 (2018-11-07)
### Changes
- Reauth didn't work if intial auth presented was incorrect, (see issue #112) fixed thanks @vvalchev
- Update node version supported to >=6 (PR #115) thanks @perlun
- Update packages
- developer dependencies
<a name="0.2.5"></a>
## 0.2.5 (2018-09-11)
### Added
- Reauth function thanks to @vbeskrovny and @vvalchev (9bbc116)
- Controlled by `config.json` option `options.allowreauth` true presents reauth dialog and false hides dialog
### Changed
- `options.challengeButton` enabled
- previously this configuration option did nothing, this now enables the Credentials button site-wide regardless of the `allowreplay` header value
- Updated debug module to v4
<a name="0.2.4"></a>
## 0.2.4 (2018-07-18)
### Added
- Browser title window now changes with xterm escape sequences (see http://tldp.org/HOWTO/Xterm-Title-3.html)
- Added bellStyle options
- `GET var`: **bellStyle** - _string_ - Style of terminal bell: ("sound"|"none"). **Default:** "sound". **Enforced Values:** "sound", "none"
- `config.json`: **terminal.bellStyle** - _string_ - Style of terminal bell: (sound|none). **Default:** "sound".
- `workspace` folder on GITHUB for BIG-IP specific fixes/changes
### Changed
- Updated xterm.js to 3.1.0
- https://github.com/xtermjs/xterm.js/releases/tag/3.1.0
- Default listen IP in `config.json` changed back to 127.0.0.1
### Bug Fixes
- ESC]0; is now removed from log files when using the browser-side logging feature
* **package:** update ssh2 to version 0.6.1 ([bf15b3e](https://github.com/billchurch/WebSSH2/commit/bf15b3e)), closes [#55](https://github.com/billchurch/WebSSH2/issues/55)
* **package:** update validator to version 10.1.0 ([1a15fa5](https://github.com/billchurch/WebSSH2/commit/1a15fa5)), closes [#62](https://github.com/billchurch/WebSSH2/issues/62)
<a name="0.2.0"></a>
# 0.2.0 (2018-02-10)
Mostly client (browser) related changes in this release
### Added
- Menu system
- Fontawesome icons
- Resizing browser window sends resize events to terminal container as well as SSH session (pty)
- New terminal options (config.json as well as GET vars)
- terminal.cursorBlink - boolean - Cursor blinks (true), does not (false) Default: true.
- terminal.scrollback - integer - Lines in the scrollback buffer. Default: 10000.
- terminal.tabStopWidth - integer - Tab stops at n characters Default: 8.
- New serverside (nodejs) terminal configuration options (cursorBlink, scrollback, tabStopWidth)
- Logging of MRH session (unassigned if not present)
- Express compression feature
### Changed
- Updated xterm.js to 3.0.2
- See https://github.com/xtermjs/xterm.js/releases/tag/3.0.2
- See https://github.com/xtermjs/xterm.js/releases/tag/3.0.1
- See https://github.com/xtermjs/xterm.js/releases/tag/3.0.0
- Moved javascript events out of html into javascript
- Changed asset packaging from grunt to Webpack to be inline with xterm.js direction
- Moved logging and credentials buttons to menu system
- Removed non-minified options (if you need to disable minification, modify webpack scripts and 'npm run build')
### Fixed
- Resolved loss of terminal foucs when interacting with option buttons (Logging, etc...)
<a name="0.1.4"></a>
## 0.1.4 (2018-01-30)
### Changed
- Moved socket and util out of folders into .js in root.
- added keepaliveInterval and keepaliveCountMax config options
### Bug Fixes
* package.json to reduce vulnerabilities ([196d769](https://github.com/billchurch/WebSSH2/commit/196d769))
<a name="0.1.3"></a>
## 0.1.3 (2017-09-28)
### Changed
- Upgrade to debug@3.1 to eliminate ReDoS in %o formatter
- Upgrade Express to 4.15.5 for ReDOS
- Upgrade basic-auth to v2.0
<a name="0.1.2"></a>
## 0.1.2 (2017-08-21)
### Added
- ssh.readyTimeout option in config.json (time in ms, default 20000, 20sec)
### Changed
- Updated xterm.js to 2.9.2 from 2.6.0
- See https://github.com/sourcelair/xterm.js/releases/tag/2.9.2
- See https://github.com/sourcelair/xterm.js/releases/tag/2.9.1
- See https://github.com/sourcelair/xterm.js/releases/tag/2.9.0
- See https://github.com/sourcelair/xterm.js/releases/tag/2.8.1
- See https://github.com/sourcelair/xterm.js/releases/tag/2.8.0
- See https://github.com/sourcelair/xterm.js/releases/tag/2.7.0
- Updated ssh2 to 0.5.5 to keep current, no fixes impacting WebSSH2
- ssh-streams to 0.1.19 from 0.1.16
- Updated validator.js to 8.0.0, no fixes impacting WebSSH2
- https://github.com/chriso/validator.js/releases/tag/8.0.0
- Updated Express to 4.15.4, no fixes impacting WebSSH2
- https://github.com/expressjs/express/releases/tag/4.15.4
- Updated Express-session to 1.15.5, no fixes impacting WebSSH2
- https://github.com/expressjs/session/releases/tag/v1.15.5
- Updated Debug to 3.0.0, no fixes impacting WebSSH2
- https://github.com/visionmedia/debug/releases/tag/3.0.0
- Running in strict mode ('use strict';)
### Bug Fixes
* package.json to reduce vulnerabilities ([e65a964](https://github.com/billchurch/WebSSH2/commit/e65a964))
<a name="0.1.1"></a>
## 0.1.1 (2017-06-03)
- `serverlog.client` and `serverlog.server` options added to `config.json` to enable logging of client commands to server log (only client portion implemented at this time)
- morgan express middleware for logging
### Changed
- Updated socket.io to 1.7.4
- continued refactoring, breaking up `index.js`
- revised error handling methods
- revised session termination methods
### Fixed
### Removed
- color console decorations from `util/index.js`
- SanatizeHeaders function from `util/index.js`
<a name="0.1.0"></a>
# 0.1.0 (2017-05-27)
### Added
- This ChangeLog.md file
- Support for UTF-8 characters (thanks @bara666)
- Snyk, Bithound, Travis CI
- Cross platform improvements (path mappings)
- Session fixup between Express and Socket.io
- Session secret settings in `config.json`
- env variable `DEBUG=ssh2` will put the `ssh2` module into debug mode
- env variable `DEBUG=WebSSH2` will output additional debug messages for functions
and events in the application (not including the ssh2 module debug)
- using Grunt to pull js and css source files from other modules `npm run build` to rebuild these if changed or updated.
- `useminified` option in `config.json` to enable using minified client side javascript (true) defaults to false (non-minified)
- sshterm= query option to specify TERM environment variable for host, valid strings are alpha-numeric with a hypen (validated). Otherwise the default ssh.term variable from `config.json` will be used.
- validation for host (v4,v6,fqdn,hostname), port (integer 2-65535), and header (sanitized) from URL input
### Changed
- error handling in public/client.js
- moved socket.io operations to their own file /socket/index.js, more changes like this to come (./socket/index.js)
- all session based variables are now under the req.session.ssh property or socket.request.ssh (./index.js)
- moved SSH algorithms to `config.json` and defined as a session variable (..session.ssh.algorithms)
-- prep for future feature to define algorithms in header or some other method to enable separate ciphers per host
- minified and combined all js files to a single js in `./public/webssh2.min.js` also included a sourcemap `./public/webssh2.min.js` which maps to `./public/webssh2.js` for easier troubleshooting.
- combined all css files to a single css in `./public/webssh2.css`
- minified all css files to a single css in `./public/webssh2.min.css`
- copied all unmodified source css and js to /public/src/css and /public/src/js respectively (for troubleshooting/etc)
- sourcemaps of all minified code (in /public/src and /public/src/js)
- renamed `client.htm` to `client-full.htm`
- created `client-min.htm` to serve minified javascript
- if header.text is null in `config.json` and header is not defined as a get parameter the Header will not be displayed. Both of these must be null / undefined and not specified as get parameters.
### Fixed
- Multiple errors may overwrite status bar which would cause confusion as to what originally caused the error. Example, ssh server disconnects which prompts a cascade of events (conn.on('end'), socket.on('disconnect'), conn.on('close')) and the original reason (conn.on('end')) would be lost and the user would erroneously receive a WEBSOCKET error as the last event to fire would be the websocket connection closing from the app.
- ensure ssh session is closed when a browser disconnects from the websocket
- if headerBackground is changed, status background is changed to the same color (typo, fixed)
### Removed
- Express Static References directly to module source directories due to concatenating and minifying js/css
<a name="0.0.5"></a>
## 0.0.5 (2017-03-23)
### Added
- Added experimental support for logging (see Readme)
### Fixed
- Terminal geometry now properly fills the browser screen and communicates this to the ssh session. Tested with IE 11 and recent versions of Chrome/Safari/Firefox.
<a name="0.0.4"></a>
## 0.0.4 (2017-03-23)
### Added
- Set default terminal to xterm-color
- Mouse event support
- New config option, config.ssh.term to set terminal
### Changed
- Update to Xterm.js 2.4.0
- Minor code formatting cleanup
<a name="0.0.3"></a>
## 0.0.3 (2017-02-16)
### Changed
- Update xterm to latest (2.3.0)
### Fixed
- Fixed misspelled config.ssh.port property
<a name="0.0.2"></a>
## 0.0.2 (2017-02-01)
### Changed
- Moving terminal emulation to xterm.js
- updating module version dependencies
### Fixed
- Fixed issue with banners not being displayed properly from UNIX hosts when only lf is used
<a name="0.0.1"></a>
## 0.0.1 (2016-07-28)
### Added
- Initial proof of concept and release. For historical purposes only.

View file

@ -1,6 +1,221 @@
# Change Log
## [0.2.9] 2019-06-13
All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.
<a name="0.2.20"></a>
## [0.2.20](https://github.com/billchurch/WebSSH2/compare/v0.2.19...v0.2.20) (2024-08-22)
<a name="0.2.19"></a>
## [0.2.19](https://github.com/billchurch/WebSSH2/compare/v0.2.18...v0.2.19) (2024-08-22)
<a name="0.2.18"></a>
## [0.2.18](https://github.com/billchurch/WebSSH2/compare/v0.2.17...v0.2.18) (2024-08-22)
### Features
* express.js session secret configurable in docker with WEBSSH_SESSION_SECRET env variable ([12e5431](https://github.com/billchurch/WebSSH2/commit/12e5431))
* ssh keyboard-interactive authentication support ([0f3c7ab](https://github.com/billchurch/WebSSH2/commit/0f3c7ab))
<a name="0.2.17"></a>
## 0.2.17 (2024-08-22)
### Bug Fixes
* correct handling of sshTerm query parameters ([b9ca79e](https://github.com/billchurch/WebSSH2/commit/b9ca79e))
* enable `autoConnect` only on `/ssh/host/` ([c9591d6](https://github.com/billchurch/WebSSH2/commit/c9591d6))
* handle http basic auth in `/ssh/host/` route ([1fc35f7](https://github.com/billchurch/WebSSH2/commit/1fc35f7))
* honor `ssh.term` settings as default when url param `sshTerm` is undefined ([303f53d](https://github.com/billchurch/WebSSH2/commit/303f53d))
* sanitize object no longer mutates original object ([ea01701](https://github.com/billchurch/WebSSH2/commit/ea01701))
* Serve the static files from the webssh2_client module with a custom prefix '/ssh/assets' instead of just '/ssh'. ([8fcf4b7](https://github.com/billchurch/WebSSH2/commit/8fcf4b7))
* vareiable scoping for conn and stream would prevent multiple user sessions ([650f4eb](https://github.com/billchurch/WebSSH2/commit/650f4eb))
* version comment in client.html ([ea12cc8](https://github.com/billchurch/WebSSH2/commit/ea12cc8))
### Features
* `Switch User` or `reauth` feature for Basic Auth sessions ([3e45c98](https://github.com/billchurch/WebSSH2/commit/3e45c98))
* `Switch User` or `reauth` feature for Basic Auth sessions ([a530f59](https://github.com/billchurch/WebSSH2/commit/a530f59))
* add allowReconnect, allowReauth, and autoLog features, normalize debug logs ([e2ea068](https://github.com/billchurch/WebSSH2/commit/e2ea068))
* Add session-based authentication for SSH connections using HTTP Basic auth and express.js ([afe462b](https://github.com/billchurch/WebSSH2/commit/afe462b))
* Add SSH routes and connection handler ([2d19f49](https://github.com/billchurch/WebSSH2/commit/2d19f49))
* get HTTP session secret from `WEBSSH_SESSION_SECRET` env if available. ([17bc82d](https://github.com/billchurch/WebSSH2/commit/17bc82d))
* HTTP Basic Authentication and auto-connection with /ssh/host/<hostIP> ([a0affca](https://github.com/billchurch/WebSSH2/commit/a0affca))
* Inject SSH host and port into webssh2 configuration ([e39fb88](https://github.com/billchurch/WebSSH2/commit/e39fb88))
* routes.js validate input from url parameters ([72d7477](https://github.com/billchurch/WebSSH2/commit/72d7477))
* Update connectionHandler.js and routes.js to propmpt for basic credentials when accessing `/ssh/host/<address>` and pre-populate credentials and host info AND auto-connect to server. ([fe7248e](https://github.com/billchurch/WebSSH2/commit/fe7248e))
* update webssh2_client 0.2.20 ([9b94627](https://github.com/billchurch/WebSSH2/commit/9b94627))
* update webssh2_client 0.2.21 ([9cfccb1](https://github.com/billchurch/WebSSH2/commit/9cfccb1))
* update webssh2_client to 0.2.19 ([418af1b](https://github.com/billchurch/WebSSH2/commit/418af1b))
* update webssh2_client@0.2.23 ([e06fabc](https://github.com/billchurch/WebSSH2/commit/e06fabc))
* validate handleResize ([b4cbfb4](https://github.com/billchurch/WebSSH2/commit/b4cbfb4))
* validate handleTerminal ([aab1a35](https://github.com/billchurch/WebSSH2/commit/aab1a35))
* validateSshTerm checks if term is undefined or null before validation ([28f329e](https://github.com/billchurch/WebSSH2/commit/28f329e))
<a name="0.2.12"></a>
## 0.2.12 (2024-07-10)
<a name="0.2.11"></a>
## 0.2.11 (2021-05-12)
<a name="0.2.9"></a>
## 0.2.9 (2019-06-13)
<a name="0.2.8"></a>
## 0.2.8 (2019-05-26)
<a name="0.2.7"></a>
## 0.2.7 (2018-11-11)
<a name="0.2.6"></a>
## 0.2.6 (2018-11-07)
<a name="0.2.5"></a>
## 0.2.5 (2018-09-11)
<a name="0.2.4"></a>
## 0.2.4 (2018-07-18)
### Bug Fixes
* **package:** update ssh2 to version 0.6.1 ([bf15b3e](https://github.com/billchurch/WebSSH2/commit/bf15b3e)), closes [#55](https://github.com/billchurch/WebSSH2/issues/55)
* **package:** update validator to version 10.1.0 ([1a15fa5](https://github.com/billchurch/WebSSH2/commit/1a15fa5)), closes [#62](https://github.com/billchurch/WebSSH2/issues/62)
<a name="0.2.0"></a>
# 0.2.0 (2018-02-10)
<a name="0.1.4"></a>
## 0.1.4 (2018-01-30)
### Bug Fixes
* package.json to reduce vulnerabilities ([196d769](https://github.com/billchurch/WebSSH2/commit/196d769))
<a name="0.1.3"></a>
## 0.1.3 (2017-09-28)
<a name="0.1.2"></a>
## 0.1.2 (2017-08-21)
### Bug Fixes
* package.json to reduce vulnerabilities ([e65a964](https://github.com/billchurch/WebSSH2/commit/e65a964))
<a name="0.1.1"></a>
## 0.1.1 (2017-06-03)
<a name="0.1.0"></a>
# 0.1.0 (2017-05-27)
<a name="0.0.5"></a>
## 0.0.5 (2017-03-23)
<a name="0.0.4"></a>
## 0.0.4 (2017-03-23)
<a name="0.0.3"></a>
## 0.0.3 (2017-02-16)
<a name="0.0.2"></a>
## 0.0.2 (2017-02-01)
<a name="0.0.1"></a>
## 0.0.1 (2016-07-28)
# Change Log
## [0.2.13] 2024-07-11
BIG-IP Specific version
### Fixes
- fixed missing reference to `read-config-ng` switchover which could prevent `config.json` from being read
## [0.2.12] 2024-07-10
BIG-IP Specific version
### Changes
- `[ctrl]+[shift]+[6]` or `[ctrl]+[^]` now sends `RS` or `0x1E`
## [0.2.11] 2020-05-12
BIG-IP Specific version
### BREAKING
- Not compatible with versions of ephemeral_auth before 0.4.8 due to child resources moving under /ssh
### Changes
- in `config.json.sample` - `allowreauth` set to `false` by default
- in `config.json.sample` - potential future proofing for CORS support `http.origins`
- `ssh` module updated to 0.8.9
- Move all child resources to start from under /ssh
- /socket.io -> /ssh/socket.io
- /webssh2.css -> /ssh/webssh2.css
- /webssh2.bundle.js -> /ssh/webssh2.bundle.js
- /reauth -> /ssh/reauth
- perhaps more
## [0.2.10] not actually released
## [0.2.9] 2019-06-13
### Changes
- Missing require('fs') in `server/app.js` See issue [#135](../../issues/135)
- Patched read-config to mitigate vulnerability in js-yaml
- issue not exploitable on webssh2 implementation
@ -9,7 +224,9 @@
- See https://github.com/nodeca/js-yaml/issues/475 for more detail
## [0.2.8] 2019-05-25
### Changes
- Fixes issue if no password is entered, browser must be closed and restart to attempt to re-auth. See issue [#118](../../issues/118). Thanks @smilesm2 for the idea.
- fixes broken `npm run (build|builddev)`
- update font-awesome fonts to 5.6.3
@ -17,10 +234,13 @@
- update xterm to 3.8.0
### Fixes
- ILX workspace may not always import properly due to symbolic links (specifically ./node_modules/.bin). This is removed from the ILX package
## [0.2.7] 2018-11-11
### Changes
- `config.reauth` was not respected if initial auth presented was incorrect, regardless of `reauth` setting in `config.json` reauth would always be attempted. fixes [#117](../../issues/117)
- **BREAKING** moved app files to /app, this may be a breaking change
- Updated dockerfile for new app path
@ -48,45 +268,59 @@
- webpack-cli v3.1.2
## [0.2.6] 2018-11-09
### Changes
- Reauth didn't work if intial auth presented was incorrect, (see issue #112) fixed thanks @vvalchev
- Update node version supported to >=6 (PR #115) thanks @perlun
- Update packages
- developer dependencies
## [0.2.5] 2018-09-11
### Added
- Reauth function thanks to @vbeskrovny and @vvalchev (9bbc116)
- Controlled by `config.json` option `options.allowreauth` true presents reauth dialog and false hides dialog
### Changed
- `options.challengeButton` enabled
- previously this configuration option did nothing, this now enables the Credentials button site-wide regardless of the `allowreplay` header value
- Updated debug module to v4
## [0.2.4] 2018-07-18
### Added
- Browser title window now changes with xterm escape sequences (see http://tldp.org/HOWTO/Xterm-Title-3.html)
- Added bellStyle options
- `GET var`: **bellStyle** - _string_ - Style of terminal bell: ("sound"|"none"). **Default:** "sound". **Enforced Values:** "sound", "none"
- `config.json`: **terminal.bellStyle** - _string_ - Style of terminal bell: (sound|none). **Default:** "sound".
- `workspace` folder on GITHUB for BIG-IP specific fixes/changes
### Changed
- Updated xterm.js to 3.1.0
- https://github.com/xtermjs/xterm.js/releases/tag/3.1.0
- Default listen IP in `config.json` changed back to 127.0.0.1
### Fixed
- ESC]0; is now removed from log files when using the browser-side logging feature
## [0.2.3] unreleased
### Fixed
- ESC]0; is now removed from log files when using the browser-side logging feature
## [0.2.0] 2018-02-10
Mostly client (browser) related changes in this release
### Added
- Menu system
- Fontawesome icons
- Resizing browser window sends resize events to terminal container as well as SSH session (pty)
@ -99,6 +333,7 @@ Mostly client (browser) related changes in this release
- Express compression feature
### Changed
- Updated xterm.js to 3.0.2
- See https://github.com/xtermjs/xterm.js/releases/tag/3.0.2
- See https://github.com/xtermjs/xterm.js/releases/tag/3.0.1
@ -109,23 +344,34 @@ Mostly client (browser) related changes in this release
- Removed non-minified options (if you need to disable minification, modify webpack scripts and 'npm run build')
### Fixed
- Resolved loss of terminal foucs when interacting with option buttons (Logging, etc...)
# Change Log
## [0.1.4] 2018-01-30
### Changed
- Moved socket and util out of folders into .js in root.
- added keepaliveInterval and keepaliveCountMax config options
## [0.1.3] 2017-09-28
### Changed
- Upgrade to debug@3.1 to eliminate ReDoS in %o formatter
- Upgrade Express to 4.15.5 for ReDOS
- Upgrade basic-auth to v2.0
## [0.1.2] 2017-07-31
### Added
- ssh.readyTimeout option in config.json (time in ms, default 20000, 20sec)
### Changed
- Updated xterm.js to 2.9.2 from 2.6.0
- See https://github.com/sourcelair/xterm.js/releases/tag/2.9.2
- See https://github.com/sourcelair/xterm.js/releases/tag/2.9.1
@ -145,23 +391,31 @@ Mostly client (browser) related changes in this release
- https://github.com/visionmedia/debug/releases/tag/3.0.0
- Running in strict mode ('use strict';)
## [0.1.1] 2017-06-03
### Added
- `serverlog.client` and `serverlog.server` options added to `config.json` to enable logging of client commands to server log (only client portion implemented at this time)
- morgan express middleware for logging
### Changed
- Updated socket.io to 1.7.4
- continued refactoring, breaking up `index.js`
- revised error handling methods
- revised session termination methods
### Fixed
### Removed
- color console decorations from `util/index.js`
- SanatizeHeaders function from `util/index.js`
## [0.1.0] 2017-05-27
### Added
- This ChangeLog.md file
- Support for UTF-8 characters (thanks @bara666)
- Snyk, Bithound, Travis CI
@ -177,6 +431,7 @@ and events in the application (not including the ssh2 module debug)
- validation for host (v4,v6,fqdn,hostname), port (integer 2-65535), and header (sanitized) from URL input
### Changed
- error handling in public/client.js
- moved socket.io operations to their own file /socket/index.js, more changes like this to come (./socket/index.js)
- all session based variables are now under the req.session.ssh property or socket.request.ssh (./index.js)
@ -192,44 +447,61 @@ and events in the application (not including the ssh2 module debug)
- if header.text is null in `config.json` and header is not defined as a get parameter the Header will not be displayed. Both of these must be null / undefined and not specified as get parameters.
### Fixed
- Multiple errors may overwrite status bar which would cause confusion as to what originally caused the error. Example, ssh server disconnects which prompts a cascade of events (conn.on('end'), socket.on('disconnect'), conn.on('close')) and the original reason (conn.on('end')) would be lost and the user would erroneously receive a WEBSOCKET error as the last event to fire would be the websocket connection closing from the app.
- ensure ssh session is closed when a browser disconnects from the websocket
- if headerBackground is changed, status background is changed to the same color (typo, fixed)
### Removed
- Express Static References directly to module source directories due to concatenating and minifying js/css
## [0.0.5] - 2017-03-23
### Added
- Added experimental support for logging (see Readme)
### Fixed
- Terminal geometry now properly fills the browser screen and communicates this to the ssh session. Tested with IE 11 and recent versions of Chrome/Safari/Firefox.
## [0.0.4] - 2017-03-23
### Added
- Set default terminal to xterm-color
- Mouse event support
- New config option, config.ssh.term to set terminal
### Changed
- Update to Xterm.js 2.4.0
- Minor code formatting cleanup
## [0.0.3] - 2017-02-16
### Changed
- Update xterm to latest (2.3.0)
### Fixed
- Fixed misspelled config.ssh.port property
## [0.0.2] - 2017-02-01
### Changed
- Moving terminal emulation to xterm.js
- updating module version dependencies
### Fixed
- Fixed issue with banners not being displayed properly from UNIX hosts when only lf is used
## [0.0.1] - 2016-06-28
### Added
- Initial proof of concept and release. For historical purposes only.

74
DEPRECATED.md Normal file
View file

@ -0,0 +1,74 @@
# Deprecated Features in WebSSH2
This document outlines features, configuration options, and parameters that have been deprecated in the this version of WebSSH2. Please review this information to ensure your setup remains compatible and to make necessary adjustments.
## Removed `config.json` Options
The following configuration options have been **removed** from `config.json`:
### Terminal Configuration
The following options have been replaced with client-side terminal configuration handling in the browser:
- `terminal.cursorBlink` (boolean): Whether the cursor blinks.
- `terminal.scrollback` (integer): Scrollback limit.
- `terminal.tabStopWidth` (integer): Tab stop width.
- `terminal.bellStyle` (string): Bell style.
### Logging Configuration
- `serverlog.client` (boolean): Enabled or disabled client logging.
- `serverlog.server` (boolean): Enabled or disabled server logging.
- `accesslog` (boolean): Controlled whether access logging was enabled.
### Other
- `verify` (boolean): This option was never implemented and has been removed.
## Removed GET Parameters
The following GET parameters have been **removed** from the application:
### Terminal Configuration Parameters
These have been replaced with client-side terminal configuration handling in the browser:
- `readyTimeout=`
- `cursorBlink=`
- `scrollback=`
- `tabStopWidth=`
- `bellStyle=`
### Header Parameters
- `allowReplay=` (boolean): Controlled the use of the password replay feature. This is now exclusively controlled from server-side `config.json`.
- `mrhsession=` (string): Used to pass an APM session for event correlation. This unused option has been removed.
## Required Actions
1. **Review and Update Configuration Files:**
- Remove references to deprecated options in your `config.json` file.
- If you relied on any of the removed terminal configuration options, implement client-side configurations instead.
2. **Update Integrations:**
- If your integrations or workflows use any of the removed GET parameters, update them to remove these references.
3. **Logging and Verification Adjustments:**
- If you relied on `serverlog`, `accesslog`, or `verify` options, you may need to implement custom solutions for logging and verification.
4. **Client-Side Terminal Configuration:**
- Implement client-side terminal configurations to replace the removed server-side options.
5. **Review Header Configurations:**
- Update any configurations or integrations that relied on `allowReplay` or `mrhsession` GET parameters.
6. **Test Your Setup:**
- After making these changes, thoroughly test your WebSSH2 setup to ensure everything works as expected with the new configuration.
## Additional Notes
- The removal of these options is part of our effort to simplify the codebase and improve performance.
- If you encounter any issues after updating, please refer to the latest documentation or open an issue on our GitHub repository.
- For the most up-to-date information on configuration options, always refer to the current README.md and configuration files in the repository.
I appreciate your understanding and cooperation as we continue to improve WebSSH2. If you have any questions or need assistance with these changes, please don't hesitate to reach out to the project maintainers.

View file

@ -1,7 +1,51 @@
FROM node:8.6
# Use debian:bookworm-slim runtime as a parent image
FROM debian:bookworm-slim
WORKDIR /usr/src
COPY app/ /usr/src/
RUN npm install --production
RUN rm /bin/sh && ln -s /bin/bash /bin/sh
RUN apt-get update \
&& apt-get install -y curl \
&& apt-get -y autoclean
# nvm environment variables
ENV NVM_DIR /usr/local/nvm
ENV NODE_VERSION 6.9.1
RUN mkdir -p $NVM_DIR
# install nvm
# https://github.com/creationix/nvm#install-script
RUN curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.0/install.sh | bash
ENV NODE_PATH $NVM_DIR/v$NODE_VERSION/lib/node_modules
ENV PATH $NVM_DIR/versions/node/v$NODE_VERSION/bin:$PATH
RUN echo "source $NVM_DIR/nvm.sh && \
nvm install $NODE_VERSION && \
nvm alias default $NODE_VERSION && \
nvm use default" | bash
# Set the working directory in the container
WORKDIR /usr/src/app
# Copy package.json and package-lock.json (if available)
COPY package*.json index.js ./
# Install production dependencies
# RUN npm install --production
RUN npm i --audit=false --bin-links=false --fund=false --production
COPY app/ ./app/
COPY config.json.sample config.json
# Set environment variables
ENV PORT=2222
ENV DEBUG=
ENV WEBSSH_SESSION_SECRET=
# Make port 2222 available to the world outside this container
EXPOSE 2222
CMD npm run start
# Run the app when the container launches
CMD ["npm", "start"]

33
EventFlow.md Normal file
View file

@ -0,0 +1,33 @@
# Typical Event Flow
```mermaid
sequenceDiagram
participant Client
participant Socket.IO
participant WebSSH2 Server
participant SSH Server
Client->>Socket.IO: Connect
Socket.IO->>WebSSH2 Server: io.on connection
Note over WebSSH2 Server: Socket Established
WebSSH2 Server->>Client: Emit request_auth
Client->>WebSSH2 Server: Send authentication data
Note over WebSSH2 Server: handleAuthenticate
Note over WebSSH2 Server: initializeConnection
WebSSH2 Server->>SSH Server: Connect (ssh.connect)
SSH Server-->>WebSSH2 Server: Connection ready
Note over WebSSH2 Server: conn.on ready
WebSSH2 Server->>Client: Emit authentication success
WebSSH2 Server->>Client: Emit permissions
WebSSH2 Server->>Client: Update footer element
Client->>WebSSH2 Server: Send terminal data
Note over WebSSH2 Server: handleTerminal
Note over WebSSH2 Server: Set term, rows, cols
Note over WebSSH2 Server: Ready for SSH communication
Note over WebSSH2 Server: createShell
WebSSH2 Server->>SSH Server: open shell
SSH Server-->>WebSSH2 Server: stream.on('data')
WebSSH2 Server-->>Client: socket.emit('data')
Client-->>WebSSH2 Server: socket.on('data')
WebSSH2 Server-->>SSH Server: stream.write('data')
```

39
FLOWS.md Normal file
View file

@ -0,0 +1,39 @@
# Application Flow
```mermaid
sequenceDiagram
participant Client
participant Express
participant SocketIO
participant SSHConnection
participant SSHServer
Client->>Express: HTTP Request
Express->>Client: Send client files
Client->>SocketIO: Establish Socket.IO connection
alt HTTP Basic Auth used
SocketIO->>SSHConnection: Jump to "Connect with credentials"
else No pre-existing credentials
SocketIO->>Client: Emit "authentication" (request_auth)
Client->>SocketIO: Emit "authenticate" (with credentials)
end
SocketIO->>SSHConnection: Connect with credentials
SSHConnection->>SSHServer: Establish SSH connection
alt Keyboard Interactive Auth
SSHServer->>SSHConnection: Request additional auth
SSHConnection->>SocketIO: Emit "authentication" (keyboard-interactive)
SocketIO->>Client: Forward auth request
Client->>SocketIO: Send auth response
SocketIO->>SSHConnection: Forward auth response
SSHConnection->>SSHServer: Complete authentication
end
SSHServer->>SSHConnection: Connection established
SSHConnection->>SocketIO: Connection successful
SocketIO->>Client: Emit "authentication" (success)
Client->>SocketIO: Emit "terminal" (with specs)
SocketIO->>SSHConnection: Create shell with specs
SSHConnection->>SSHServer: Create shell session
SSHConnection->>SocketIO: Shell created
SocketIO->>Client: Ready for input/output
Note over Client,SSHServer: Bidirectional data flow established
```

View file

@ -1,15 +0,0 @@
Depending on the type of issue, please include the follwing information:
- Node and NPM Version
- `node -v`
- `npm -v`
- Server OS Version / Distribution / Processor Architecture
- `uname -a`
- `cat /etc/os-release`
- WebSSH2 release version
- `grep version app/package.json`
- OS and Version of SSH server connecting to
- `uname -a`
- `sshd -v`
- Browser Version and OS
- Information from brwoser's About... or a screenshot of the about screen.
- Any log or messages from the WebSSH2 output

15
Jenkinsfile vendored
View file

@ -1,15 +0,0 @@
pipeline {
agent {
docker {
image 'node:6-alpine'
args '-p 3000:3000'
}
}
stages {
stage('Build') {
steps {
sh 'npm install'
}
}
}
}

341
README.md
View file

@ -1,228 +1,205 @@
# WebSSH2
[![GitHub version](https://badge.fury.io/gh/billchurch%2Fwebssh2.svg)](https://badge.fury.io/gh/billchurch%2Fwebssh2)
# WebSSH2 - Web SSH Client
[![Buy Me A Coffee](https://www.buymeacoffee.com/assets/img/custom_images/orange_img.png)](https://www.buymeacoffee.com/billchurch)
![Orthrus Mascot](images/orthrus2.png)
Web SSH Client using ssh2, socket.io, xterm.js, and express
WebSSH2 is an HTML5 web-based terminal emulator and SSH client. It uses SSH2 as a client on a host to proxy a Websocket / Socket.io connection to an SSH2 server.
A bare bones example of an HTML5 web-based terminal emulator and SSH client. We use SSH2 as a client on a host to proxy a Websocket / Socket.io connection to a SSH2 server.
![WebSSH2 demo](https://user-images.githubusercontent.com/1668075/182425293-acc8741e-cc92-4105-afdc-9538e1685d4b.gif)
<img width="600" height="340" alt="WebSSH2 v0.2.0 demo" src="https://github.com/billchurch/WebSSH2/raw/master/screenshots/demo-800.gif">
![WebSSH2 Screenshot](images/Screenshot.png)
# Requirements
Node v6.x or above. If using <v6.x you should be able to run by replacing the "read-config" package to @1 like this (after a clone):
## Table of Contents
`npm install --save read-config@1
`
- [Requirements](#requirements)
- [Installation](#installation)
- [Docker Setup](#docker-setup)
- [Usage](#usage)
- [Configuration](#configuration)
- [Features](#features)
- [Routes](#routes)
- [Deprecation Notice](#deprecation-notice)
- [Tips](#tips)
- [Support](#support)
Just keep in mind that there is no intention to ensure compatability with Node < v6.x
## Requirements
# Instructions
To install:
- Node.js 6.9.1
1. Clone to a location somewhere and then `cd app` and `npm install --production`. If you want to develop and rebuild javascript and other files utilize `npm install` instead.
## Installation
2. If desired, edit config.json to change the listener to your liking. There are also some default options which may be definied for a few of the variables.
1. Clone the repository:
```
git clone https://github.com/billchurch/webssh2.git
cd webssh2
```
3. Run `npm start`
2. Install dependencies:
```
npm install --production
```
For development purposes, use `npm install` instead.
4. Fire up a browser, navigate to IP/port of your choice and specify a host (https isn't used here because it's assumed it will be off-loaded to
some sort of proxy):
3. Configure the application by editing `config.json` if needed.
4. Start the server:
```
npm start
```
## Docker Setup
1. Build and run the Docker container (with debug messages):
```bash
docker build -t webssh2 .
docker run --name webssh2 --rm -it -p 2222:2222 -e "DEBUG=webssh*,-webssh2:ssh2" webssh2
```
## Usage
Access the web client by navigating to:
```
http://localhost:2222/ssh
```
You'll be prompted for host details and SSH credentials.
Alternatively you may use the `/ssh/host/<host>` route:
```
http://localhost:2222/ssh/host/127.0.0.1
```
You will be prompted for credentials to use on the SSH server via HTTP Basic authentcaiton. This is to permit usage with some SSO systems that can replay credentials over HTTP basic.
You'll be prompted for SSH credentials via HTTP Basic Authentication.
P
## Configuration
# Docker Instructions
### GET Parameters
Modify config.json
- `port=` - _integer_ - SSH server port (default: `22`)
- `header=` - _string_ - Optional header text
- `headerBackground=` - _string_ - Optional background color (default: `"green"`)
- `sshterm=` - _string_ - Terminal type for pty (default: xterm-color)
### Config File Options
Edit `config.json` to customize the following options:
- `listen.ip` - _string_ - IP address to listen on (default: `"127.0.0.1"`)
- `listen.port` - _integer_ - Port to listen on (default: `2222`)
- `http.origins` - _array_ - CORS origins for socket.io (default: `["*:*"]`)
- `user.name` - _string_ - Default SSH username (default: `null`)
- `user.password` - _string_ - Default SSH password (default: `null`)
- `ssh.host` - _string_ - Default SSH host (default: `null`)
- `ssh.port` - _integer_ - Default SSH port (default: `22`)
- `ssh.term` - _string_ - Terminal emulation (default: `"xterm-color"`)
- `ssh.readyTimeout` - _integer_ - SSH handshake timeout in ms (default: `20000`)
- `ssh.keepaliveInterval` - _integer_ - SSH keepalive interval in ms (default: `120000`)
- `ssh.keepaliveCountMax` - _integer_ - Max SSH keepalive packets (default: `10`)
- `header.text` - _string_ - Header text (default: `null`)
- `header.background` - _string_ - Header background color (default: `"green"`)
- `session.name` - _string_ - Session cookie name (default: `"webssh2.sid"`)
- `session.secret` - _string_ - Session secret key (default: `crypto.randomBytes(32).toString("hex")`)
- `options.challengeButton` - _boolean_ - Enable challenge button (default: `true`)
- `options.autoLog` - _boolean_ - Enable auto-logging (default: `false`)
- `options.allowReauth` - _boolean_ - Allow reauthentication (default: `true`)
- `options.allowReconnect` - _boolean_ - Allow reconnection (default: `true`)
- `options.allowReplay` - _boolean_ - Allow credential replay (default: `true`)
For detailed SSH algorithm configurations, refer to the full config file.
## Features
### Keyboard Interactive Authentication
Keyboard Interactive authentication provides a flexible way to handle various authentication scenarios, including multi-factor authentication.
#### How it works
1. When the SSH server requests Keyboard Interactive authentication, WebSSH2 can handle it in two ways:
a) Automatically (default behavior)
b) By prompting the user through the web interface
2. In automatic mode:
- If all prompts contain the word "password" (case-insensitive), WebSSH2 will automatically respond using the password provided during the initial connection attempt.
- If any prompt doesn't contain "password", all prompts will be forwarded to the web client for user input.
3. When prompts are sent to the web client:
- A dialog box appears in the user's browser, displaying all prompts from the SSH server.
- The user can input responses for each prompt.
- Responses are sent back to the SSH server to complete the authentication process.
#### Configuration Options
You can customize the Keyboard Interactive authentication behavior using the following option in your `config.json`:
```json
{
"listen": {
"ip": "0.0.0.0",
"port": 2222
"ssh": {
"alwaysSendKeyboardInteractivePrompts": false
}
}
```
Build and run
- `alwaysSendKeyboardInteractivePrompts` (boolean, default: false):
- When set to `true`, all Keyboard Interactive prompts will always be sent to the web client, regardless of their content.
- When set to `false` (default), WebSSH2 will attempt to automatically handle password prompts and only send non-password prompts to the web client.
```bash
docker build -t webssh2 .
docker run --name webssh2 -d -p 2222:2222 webssh2
```
#### Use Cases
# Options
1. **Simple Password Authentication**:
With default settings, if the SSH server uses Keyboard Interactive for password authentication, WebSSH2 will automatically handle it without additional user interaction.
## GET request vars
2. **Multi-Factor Authentication**:
For SSH servers requiring additional factors (e.g., OTP), WebSSH2 will present prompts to the user through the web interface.
* **port=** - _integer_ - port of SSH server (defaults to 22)
3. **Always Prompt User**:
By setting `alwaysSendKeyboardInteractivePrompts` to `true`, you can ensure that users always see and respond to all authentication prompts, which can be useful for security-sensitive environments or for debugging purposes.
* **header=** - _string_ - optional header to display on page
#### Security Considerations
* **headerBackground=** - _string_ - optional background color of header to display on page
- The automatic password handling feature is designed for convenience but may not be suitable for high-security environments. Consider setting `alwaysSendKeyboardInteractivePrompts` to `true` if you want users to explicitly enter their credentials for each session.
- Ensure that your WebSSH2 installation uses HTTPS to protect the communication between the web browser and the WebSSH2 server.
* **readyTimeout=** - _integer_ - How long (in milliseconds) to wait for the SSH handshake to complete. **Default:** 20000. **Enforced Values:** Min: 1, Max: 300000
For more information on SSH keyboard-interactive authentication, refer to [RFC 4256](https://tools.ietf.org/html/rfc4256).
* **cursorBlink** - _boolean_ - Cursor blinks (true), does not (false) **Default:** true.
## Routes
* **scrollback** - _integer_ - Lines in the scrollback buffer. **Default:** 10000. **Enforced Values:** Min: 1, Max: 200000
WebSSH2 provides two main routes:
* **tabStopWidth** - _integer_ - Tab stops at _n_ characters **Default:** 8. **Enforced Values:** Min: 1, Max: 100
### 1. `/ssh`
* **bellStyle** - _string_ - Style of terminal bell: ("sound"|"none"). **Default:** "sound". **Enforced Values:** "sound", "none"
- **URL:** `http(s)://your-webssh2-server/ssh`
- **Features:**
- Interactive login form
## Headers
<img width="341" alt="image" src="https://github.com/user-attachments/assets/829d1776-3bc5-4315-b0c6-9e96a648ce06">
- Terminal configuration options
* **allowreplay** - _boolean_ - Allow use of password replay feature, example `allowreplay: true`
<img width="341" alt="image" src="https://github.com/user-attachments/assets/bf60f5ba-7221-4177-8d64-946907aed5ff">
* **mrhsession** - _string_ - Can be used to pass APM session for event correlation `mrhsession: abc123`
### 2. `/ssh/host/:host`
## Config File Options
`config.json` contains several options which may be specified to customize to your needs, vs editing the javascript directly. This is JSON format so mind your spacing, brackets, etc...
- **URL:** `http(s)://your-webssh2-server/ssh/host/:host`
- **Authentication:** HTTP Basic Auth
- **Features:**
- Quick connections to specific hosts
- Optional `port` parameter (e.g., `?port=2222`)
* **listen.ip** - _string_ - IP address node should listen on for client connections, defaults to `127.0.0.1`
## Deprecation Notice
* **listen.port** - _integer_ - Port node should listen on for client connections, defaults to `2222`
Several configuration options and GET parameters have been deprecated. For a list of removed options and required actions, please refer to [DEPRECATED.md](./DEPRECATED.md).
* **user.name** - _string_ - Specify user name to authenticate with. In normal cases this should be left to the default `null` setting.
## Tips
* **user.password** - _string_ - Specify password to authenticate with. In normal cases this should be left to the default `null` setting.
- To add custom JavaScript, modify `./src/client.htm`, `./src/index.js`, or add your file to `webpack.*.js`.
- For security, use HTTPS when transmitting credentials via HTTP Basic Auth.
- Terminal settings for `/ssh/host/:host` can be customized after login via `Menu | Settings` and persist across sessions.
- You can enable debug from the console by passing the `DEBUG` environment variable to your start script: `DEBUG=webssh*,-webssh2:ssh2 npm run start`. The `webssh2:ssh2` namespace is very chatty and shows all of the SSH protocol information, the `-webssh2:ssh2` excludes that namespace from the line above, otherwise `DEBUG=webssh*` will capture all of the WebSSH2 specific bits. You may also debug Socket.IO and Express related events with `engine`, `socket` and `express` namespaces, or go for broke and debug everything with `DEBUG=*`.
* **ssh.host** - _string_ - Specify host to connect to. May be either hostname or IP address. Defaults to `null`.
For more detailed information on configuration and usage, please refer to the full documentation or open an issue on GitHub.
* **ssh.port** - _integer_ - Specify SSH port to connect to, defaults to `22`
## Support
If you like what I do, and want to support me you can [buy me a coffee](https://www.buymeacoffee.com/billchurch)!
* **ssh.term** - _string_ - Specify terminal emulation to use, defaults to `xterm-color`
* **ssh.readyTimeout** - _integer_ - How long (in milliseconds) to wait for the SSH handshake to complete. **Default:** 20000.
* **ssh.keepaliveInterval** - _integer_ - How often (in milliseconds) to send SSH-level keepalive packets to the server (in a similar way as OpenSSH's ServerAliveInterval config option). Set to 0 to disable. **Default:** 120000.
* **ssh.keepaliveCountMax** - _integer_ - How many consecutive, unanswered SSH-level keepalive packets that can be sent to the server before disconnection (similar to OpenSSH's ServerAliveCountMax config option). **Default:** 10.
* **terminal.cursorBlink** - _boolean_ - Cursor blinks (true), does not (false) **Default:** true.
* **terminal.scrollback** - _integer_ - Lines in the scrollback buffer. **Default:** 10000.
* **terminal.tabStopWidth** - _integer_ - Tab stops at _n_ characters **Default:** 8.
* **terminal.bellStyle** - _string_ - Style of terminal bell: (sound|none). **Default:** "sound".
* **header.text** - _string_ - Specify header text, defaults to `My Header` but may also be set to `null`. When set to `null` no header bar will be displayed on the client.
* **header.background** - _string_ - Header background, defaults to `green`.
* **session.name** - _string_ - Name of session ID cookie. it's not a horrible idea to make this something unique.
* **session.secret** - _string_ - Secret key for cookie encryption. You should change this in production.
* **options.challengeButton** - _boolean_ - Challenge button. This option, which is still under development, allows the user to resend the password to the server (in cases of step-up authentication for things like `sudo` or a router `enable` command.
* **options.allowreauth** - _boolean_ - Reauth button. This option creates an option to provide a button to create a new session with new credentials. See [issue 51](../../issues/51) and [pull 85](../../pull/85) for more detail.
* **algorithms** - _object_ - This option allows you to explicitly override the default transport layer algorithms used for the connection. Each value must be an array of valid algorithms for that category. The order of the algorithms in the arrays are important, with the most favorable being first. Valid keys:
* **kex** - _array_ - Key exchange algorithms.
* Default values:
1. ecdh-sha2-nistp256
2. ecdh-sha2-nistp384
3. ecdh-sha2-nistp521
4. diffie-hellman-group-exchange-sha256
5. diffie-hellman-group14-sha1
* Supported values:
* ecdh-sha2-nistp256
* ecdh-sha2-nistp384
* ecdh-sha2-nistp521
* diffie-hellman-group-exchange-sha256
* diffie-hellman-group14-sha1
* diffie-hellman-group-exchange-sha1
* diffie-hellman-group1-sha1
* **cipher** - _array_ - Ciphers.
* Default values:
1. aes128-ctr
2. aes192-ctr
3. aes256-ctr
4. aes128-gcm
5. aes128-gcm@openssh.com
6. aes256-gcm
7. aes256-gcm@openssh.com
8. aes256-cbc **legacy cipher for backward compatibility, should removed :+1:**
* Supported values:
* aes128-ctr
* aes192-ctr
* aes256-ctr
* aes128-gcm
* aes128-gcm@openssh.com
* aes256-gcm
* aes256-gcm@openssh.com
* aes256-cbc
* aes192-cbc
* aes128-cbc
* blowfish-cbc
* 3des-cbc
* arcfour256
* arcfour128
* cast128-cbc
* arcfour
* **hmac** - _array_ - (H)MAC algorithms.
* Default values:
1. hmac-sha2-256
2. hmac-sha2-512
3. hmac-sha1 **legacy hmac for backward compatibility, should removed :+1:**
* Supported values:
* hmac-sha2-256
* hmac-sha2-512
* hmac-sha1
* hmac-md5
* hmac-sha2-256-96
* hmac-sha2-512-96
* hmac-ripemd160
* hmac-sha1-96
* hmac-md5-96
* **compress** - _array_ - Compression algorithms.
* Default values:
1. none
2. zlib@openssh.com
3. zlib
* Supported values:
* none
* zlib@openssh.com
* zlib
* **serverlog.client** - _boolean_ - Enables client command logging on server log (console.log). Very simple at this point, buffers data from client until it receives a line-feed then dumps buffer to console.log with session information for tracking. Will capture anything send from client, including passwords, so use for testing only... Default: false. Example:
* _serverlog.client: GcZDThwA4UahDiKO2gkMYd7YPIfVAEFW/mnf0NUugLMFRHhsWAAAA host: 192.168.99.80 command: ls -lat_
* **serverlog.server** - _boolean_ - not implemented, default: false.
* **accesslog** - _boolean_ - http style access logging to console.log, default: false
# Experimental client-side logging
Clicking `Start logging` on the status bar will log all data to the client. A `Download log` option will appear after starting the logging. You may download at any time to the client. You may stop logging at any time my pressing the `Logging - STOP LOG`. Note that clicking the `Start logging` option again will cause the current log to be overwritten, so be sure to download first.
# Example:
http://localhost:2222/ssh/host/192.168.1.1?port=2244&header=My%20Header&headerBackground=red
# 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).
[![Buy Me A Coffee](https://www.buymeacoffee.com/assets/img/custom_images/orange_img.png)](https://www.buymeacoffee.com/billchurch)

138
SERVER_API.md Normal file
View file

@ -0,0 +1,138 @@
# WebSSH2 Server API Documentation
## Overview
The WebSSH2 server provides a WebSocket interface for establishing SSH connections. This API documentation outlines the events and data structures used for communication between the client and the server.
Currently this implementation requires Socket.IO v2.2.0 due to this instance targeting node 6.9.1 for a legacy project. Future releases will not have this limitation.
## Connection
The server uses Socket.IO for real-time communication. Connect to the WebSocket server at the same host and port as the HTTP server, with the path `/ssh/socket.io`.
## Events
### Server to Client Events
1. `authentication`
- Emitted to request authentication or provide authentication results.
- Payload:
```javascript
{
action: string, // "request_auth" or "auth_result"
success?: boolean, // Only present for "auth_result"
message?: string // Error message if authentication fails
}
```
2. `permissions`
- Emitted after successful authentication to provide allowed actions.
- Payload:
```javascript
{
autoLog: boolean,
allowReplay: boolean,
allowReconnect: boolean,
allowReauth: boolean
}
```
3. `getTerminal`
- Emitted to request terminal specifications from the client.
- Payload: `true`
4. `data`
- Emitted when there's output from the SSH session.
- Payload: `string` (UTF-8 encoded terminal output)
5. `ssherror`
- Emitted when an SSH-related error occurs.
- Payload: `string` (Error message)
6. `updateUI`
- Emitted to update specific UI elements.
- Payload:
```javascript
{
element: string, // UI element identifier
value: any // New value for the element
}
```
### Client to Server Events
1. `authenticate`
- Emit this event to provide authentication credentials.
- Payload:
```javascript
{
username: string,
password: string,
host: string,
port: number,
term?: string // Optional terminal type
}
```
2. `terminal`
- Emit this event to provide terminal specifications.
- Payload:
```javascript
{
term: string, // e.g., "xterm-256color"
cols: number,
rows: number
}
```
3. `data`
- Emit this event to send user input to the SSH session.
- Payload: `string` (UTF-8 encoded user input)
4. `resize`
- Emit this event when the terminal size changes.
- Payload:
```javascript
{
cols: number,
rows: number
}
```
5. `control`
- Emit this event for special control commands.
- Payload: `string` ("replayCredentials" or "reauth")
6. `disconnect`
- Emit this event to close the connection.
- No payload required
## Authentication Flow
1. The server emits `authentication` with `action: "request_auth"`.
2. The client emits `authenticate` with credentials.
3. The server may emit `authentication` with `action: "keyboard-interactive"` for additional authentication steps.
4. The server emits `authentication` with `action: "auth_result"` and `success: true/false`.
## Establishing SSH Session
1. After successful authentication, the server emits `getTerminal`.
2. The client emits `terminal` with terminal specifications.
3. The server establishes the SSH connection and starts emitting `data` events with terminal output.
4. The client can now send `data` events with user input.
## Error Handling
- The client should listen for `ssherror` events and handle them appropriately (e.g., displaying error messages to the user).
## UI Updates
- The client should listen for `updateUI` events and update the corresponding UI elements.
## Best Practices
1. Handle connection errors and implement reconnection logic.
2. Implement proper error handling and user feedback.
3. Securely manage authentication credentials.
4. Handle terminal resizing appropriately.
5. Implement support for special control commands (replay credentials, reauthentication).

70
app/app.js Normal file
View file

@ -0,0 +1,70 @@
// server
// app/app.js
const express = require("express")
const config = require("./config")
const socketHandler = require("./socket")
const sshRoutes = require("./routes")
const { applyMiddleware } = require("./middleware")
const { createServer, startServer } = require("./server")
const { configureSocketIO } = require("./io")
const { handleError, ConfigError } = require("./errors")
const { createNamespacedDebug } = require("./logger")
const { DEFAULTS, MESSAGES } = require("./constants")
const debug = createNamespacedDebug("app")
/**
* Creates and configures the Express application
* @returns {Object} An object containing the app and sessionMiddleware
*/
function createApp() {
const app = express()
try {
// Resolve the correct path to the webssh2_client module
const clientPath = DEFAULTS.WEBSSH2_CLIENT_PATH
// Apply middleware
const { sessionMiddleware } = applyMiddleware(app, config)
// Serve static files from the webssh2_client module with a custom prefix
app.use("/ssh/assets", express.static(clientPath))
// Use the SSH routes
app.use("/ssh", sshRoutes)
return { app: app, sessionMiddleware: sessionMiddleware }
} catch (err) {
throw new ConfigError(
`${MESSAGES.EXPRESS_APP_CONFIG_ERROR}: ${err.message}`
)
}
}
/**
* Initializes and starts the server
* @returns {Object} An object containing the server, io, and app instances
*/
function initializeServer() {
try {
const { app, sessionMiddleware } = createApp()
const server = createServer(app)
const io = configureSocketIO(server, sessionMiddleware, config)
// Set up Socket.IO listeners
socketHandler(io, config)
// Start the server
startServer(server, config)
debug("Server initialized")
return { server: server, io: io, app: app }
} catch (err) {
handleError(err)
process.exit(1)
}
}
module.exports = { initializeServer: initializeServer, config: config }

View file

@ -1,25 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<title>WebSSH2</title>
<style>
html, body {background-color: #000;height: 100%;margin: 0;}.dropup-content {display: none;}
</style>
<link rel="stylesheet" href="/webssh2.css" />
</head>
<body>
<div class="box">
<div id="header"></div>
<div id="terminal-container" class="terminal"></div>
<div id="bottomdiv">
<div class="dropup" id="menu">
<i class="fas fa-bars fa-fw"></i> Menu
<div id="dropupContent" class="dropup-content"></div>
</div>
<div id="footer"></div>
<div id="status"></div>
</div>
</div>
<script src="/webssh2.bundle.js" defer></script>
</body>
</html>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

File diff suppressed because one or more lines are too long

View file

@ -1,294 +0,0 @@
/**
* Copyright (c) 2014 The xterm.js authors. All rights reserved.
* Copyright (c) 2012-2013, Christopher Jeffrey (MIT License)
* https://github.com/chjj/term.js
* @license MIT
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*
* Originally forked from (with the author's permission):
* Fabrice Bellard's javascript vt100 for jslinux:
* http://bellard.org/jslinux/
* Copyright (c) 2011 Fabrice Bellard
* The original design remains. The terminal itself
* has been extended to include xterm CSI codes, among
* other features.
*/
/**
* Default styles for xterm.js
*/
.xterm {
font-feature-settings: "liga" 0;
position: relative;
user-select: none;
-ms-user-select: none;
-webkit-user-select: none;
}
.xterm.focus,
.xterm:focus {
outline: none;
}
.xterm .xterm-helpers {
position: absolute;
top: 0;
/**
* The z-index of the helpers must be higher than the canvases in order for
* IMEs to appear on top.
*/
z-index: 10;
}
.xterm .xterm-helper-textarea {
/*
* HACK: to fix IE's blinking cursor
* Move textarea out of the screen to the far left, so that the cursor is not visible.
*/
position: absolute;
opacity: 0;
left: -9999em;
top: 0;
width: 0;
height: 0;
z-index: -10;
/** Prevent wrapping so the IME appears against the textarea at the correct position */
white-space: nowrap;
overflow: hidden;
resize: none;
}
.xterm .composition-view {
/* TODO: Composition position got messed up somewhere */
background: #000;
color: #FFF;
display: none;
position: absolute;
white-space: nowrap;
z-index: 1;
}
.xterm .composition-view.active {
display: block;
}
.xterm .xterm-viewport {
/* On OS X this is required in order for the scroll bar to appear fully opaque */
background-color: #000;
overflow-y: scroll;
cursor: default;
position: absolute;
right: 0;
left: 0;
top: 0;
bottom: 0;
}
.xterm .xterm-screen {
position: relative;
}
.xterm .xterm-screen canvas {
position: absolute;
left: 0;
top: 0;
}
.xterm .xterm-scroll-area {
visibility: hidden;
}
.xterm-char-measure-element {
display: inline-block;
visibility: hidden;
position: absolute;
top: 0;
left: -9999em;
line-height: normal;
}
.xterm {
cursor: text;
}
.xterm.enable-mouse-events {
/* When mouse events are enabled (eg. tmux), revert to the standard pointer cursor */
cursor: default;
}
.xterm.xterm-cursor-pointer {
cursor: pointer;
}
.xterm.column-select.focus {
/* Column selection mode */
cursor: crosshair;
}
.xterm .xterm-accessibility,
.xterm .xterm-message {
position: absolute;
left: 0;
top: 0;
bottom: 0;
right: 0;
z-index: 100;
color: transparent;
}
.xterm .live-region {
position: absolute;
left: -9999px;
width: 1px;
height: 1px;
overflow: hidden;
}
.xterm-dim {
opacity: 0.5;
}
.xterm-underline {
text-decoration: underline;
}
body, html {
font-family: helvetica, sans-serif, arial;
font-size: 1em;
color: #111;
background-color: rgb(0, 0, 0);
color: rgb(240, 240, 240);
height: 100%;
margin: 0;
}
#header {
color: rgb(240, 240, 240);
background-color: rgb(0, 128, 0);
width: 100%;
border-color: white;
border-style: none none solid none;
border-width: 1px;
text-align: center;
flex: 0 1 auto;
z-index: 99;
height:19px;
display: none;
}
.box {
display: block;
height: 100%;
}
#terminal-container {
display: block;
width: calc(100% - 1 px);
margin: 0 auto;
padding: 2px;
height: calc(100% - 19px);
}
#terminal-container .terminal {
background-color: #000000;
color: #fafafa;
padding: 2px;
height: calc(100% - 19px);
}
#terminal-container .terminal:focus .terminal-cursor {
background-color: #fafafa;
}
#bottomdiv {
position: fixed;
left: 0;
bottom: 0;
width: 100%;
background-color: rgb(50, 50, 50);
border-color: white;
border-style: solid none none none;
border-width: 1px;
z-index: 99;
height: 19px;
}
#footer {
display: inline-block;
color: rgb(240, 240, 240);
background-color: rgb(50, 50, 50);
padding-left: 5px;
padding-right: 5px;
border-color: white;
border-style: none none none solid;
border-width: 1px;
text-align: left;
}
#status {
display: inline-block;
color: rgb(240, 240, 240);
background-color: rgb(50, 50, 50);
padding-left: 10px;
padding-right: 10px;
border-color: white;
border-style: none solid none solid;
border-width: 1px;
text-align: left;
z-index: 100;
}
#menu {
display: inline-block;
font-size: 16px;
color: rgb(255, 255, 255);
padding-left: 10px;
z-index: 100;
}
#menu:hover .dropup-content {
display: block;
}
#logBtn, #credentialsBtn, #reauthBtn {
color: #000;
}
.dropup {
position: relative;
display: inline-block;
cursor: pointer;
}
.dropup-content {
display: none;
position: absolute;
background-color: #f1f1f1;
font-size: 16px;
min-width: 160px;
bottom: 18px;
z-index: 101;
}
.dropup-content a {
color: #777;
padding: 12px 16px;
text-decoration: none;
display: block;
}
.dropup-content a:hover {
background-color: #ccc
}
.dropup:hover .dropup-content {
display: block;
}
.dropup:click .dropup-content {
display: block;
}
.dropup:hover .dropbtn {
background-color: #3e8e41;
}

View file

@ -1 +0,0 @@
Customizations and modifications to the client (browser) go here. Then run "npm run build" to integrate into ../public (where client files are served from). Note that ../public is a flat directory structure. ../public directory is deleted and refreshed eatch thime "npm run build" is run.

View file

@ -1,25 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<title>WebSSH2</title>
<style>
html, body {background-color: #000;height: 100%;margin: 0;}.dropup-content {display: none;}
</style>
<link rel="stylesheet" href="/webssh2.css" />
</head>
<body>
<div class="box">
<div id="header"></div>
<div id="terminal-container" class="terminal"></div>
<div id="bottomdiv">
<div class="dropup" id="menu">
<i class="fas fa-bars fa-fw"></i> Menu
<div id="dropupContent" class="dropup-content"></div>
</div>
<div id="footer"></div>
<div id="status"></div>
</div>
</div>
<script src="/webssh2.bundle.js" defer></script>
</body>
</html>

View file

@ -1,123 +0,0 @@
body, html {
font-family: helvetica, sans-serif, arial;
font-size: 1em;
color: #111;
background-color: rgb(0, 0, 0);
color: rgb(240, 240, 240);
height: 100%;
margin: 0;
}
#header {
color: rgb(240, 240, 240);
background-color: rgb(0, 128, 0);
width: 100%;
border-color: white;
border-style: none none solid none;
border-width: 1px;
text-align: center;
flex: 0 1 auto;
z-index: 99;
height:19px;
display: none;
}
.box {
display: block;
height: 100%;
}
#terminal-container {
display: block;
width: calc(100% - 1 px);
margin: 0 auto;
padding: 2px;
height: calc(100% - 19px);
}
#terminal-container .terminal {
background-color: #000000;
color: #fafafa;
padding: 2px;
height: calc(100% - 19px);
}
#terminal-container .terminal:focus .terminal-cursor {
background-color: #fafafa;
}
#bottomdiv {
position: fixed;
left: 0;
bottom: 0;
width: 100%;
background-color: rgb(50, 50, 50);
border-color: white;
border-style: solid none none none;
border-width: 1px;
z-index: 99;
height: 19px;
}
#footer {
display: inline-block;
color: rgb(240, 240, 240);
background-color: rgb(50, 50, 50);
padding-left: 5px;
padding-right: 5px;
border-color: white;
border-style: none none none solid;
border-width: 1px;
text-align: left;
}
#status {
display: inline-block;
color: rgb(240, 240, 240);
background-color: rgb(50, 50, 50);
padding-left: 10px;
padding-right: 10px;
border-color: white;
border-style: none solid none solid;
border-width: 1px;
text-align: left;
z-index: 100;
}
#menu {
display: inline-block;
font-size: 16px;
color: rgb(255, 255, 255);
padding-left: 10px;
z-index: 100;
}
#menu:hover .dropup-content {
display: block;
}
#logBtn, #credentialsBtn, #reauthBtn {
color: #000;
}
.dropup {
position: relative;
display: inline-block;
cursor: pointer;
}
.dropup-content {
display: none;
position: absolute;
background-color: #f1f1f1;
font-size: 16px;
min-width: 160px;
bottom: 18px;
z-index: 101;
}
.dropup-content a {
color: #777;
padding: 12px 16px;
text-decoration: none;
display: block;
}
.dropup-content a:hover {
background-color: #ccc
}
.dropup:hover .dropup-content {
display: block;
}
.dropup:click .dropup-content {
display: block;
}
.dropup:hover .dropbtn {
background-color: #3e8e41;
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

View file

@ -1,243 +0,0 @@
'use strict'
import * as io from 'socket.io-client'
import * as Terminal from 'xterm/dist/xterm'
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/dist/xterm.css')
require('../css/style.css')
Terminal.applyAddon(fit)
/* 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
var 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 terminalContainer = document.getElementById('terminal-container')
term.open(terminalContainer)
term.focus()
term.fit()
window.addEventListener('resize', resizeScreen, false)
function resizeScreen () {
term.fit()
socket.emit('resize', { cols: term.cols, rows: term.rows })
}
if (document.location.pathname) {
var parts = document.location.pathname.split('/')
var base = parts.slice(0, parts.length - 1).join('/') + '/'
var resource = base.substring(1) + 'socket.io'
socket = io.connect(null, {
resource: resource
})
} else {
socket = io.connect()
}
term.on('data', 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)
})
socket.on('error', function (err) {
if (!errorExists) {
status.style.backgroundColor = 'red'
status.innerHTML = 'ERROR: ' + err
}
})
socket.on('reauth', function () {
(allowreauth) && reauthSession()
})
term.on('title', 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 = '/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()
}

160
app/config.js Normal file
View file

@ -0,0 +1,160 @@
// server
// app/config.js
const path = require("path")
const fs = require("fs")
const readConfig = require("read-config-ng")
const { deepMerge, validateConfig } = require("./utils")
const { generateSecureSecret } = require("./crypto-utils")
const { createNamespacedDebug } = require("./logger")
const { ConfigError, handleError } = require("./errors")
const { DEFAULTS } = require("./constants")
const debug = createNamespacedDebug("config")
const defaultConfig = {
listen: {
ip: "0.0.0.0",
port: DEFAULTS.LISTEN_PORT
},
http: {
origins: ["*:*"]
},
user: {
name: null,
password: null
},
ssh: {
host: null,
port: DEFAULTS.SSH_PORT,
term: DEFAULTS.SSH_TERM,
readyTimeout: 20000,
keepaliveInterval: 120000,
keepaliveCountMax: 10,
alwaysSendKeyboardInteractivePrompts: false
},
header: {
text: null,
background: "green"
},
options: {
challengeButton: true,
autoLog: false,
allowReauth: true,
allowReconnect: true,
allowReplay: 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"]
},
session: {
secret: process.env.WEBSSH_SESSION_SECRET || generateSecureSecret(),
name: "webssh2.sid"
}
}
function getConfigPath() {
const nodeRoot = path.dirname(require.main.filename)
return path.join(nodeRoot, "config.json")
}
function loadConfig() {
const configPath = getConfigPath()
try {
if (fs.existsSync(configPath)) {
const providedConfig = readConfig.sync(configPath)
const mergedConfig = deepMerge(
JSON.parse(JSON.stringify(defaultConfig)),
providedConfig
)
if (process.env.PORT) {
mergedConfig.listen.port = parseInt(process.env.PORT, 10)
debug("Using PORT from environment: %s", mergedConfig.listen.port)
}
const validatedConfig = validateConfig(mergedConfig)
debug("Merged and validated configuration")
return validatedConfig
}
debug("Missing config.json for webssh. Using default config")
return defaultConfig
} catch (err) {
const error = new ConfigError(
`Problem loading config.json for webssh: ${err.message}`
)
handleError(error)
return defaultConfig
}
}
/**
* Configuration for the application.
*
* @returns {Object} config
* @property {Object} listen - Configuration for listening IP and port.
* @property {string} listen.ip - The IP address to listen on.
* @property {number} listen.port - The port number to listen on.
* @property {Object} http - Configuration for HTTP settings.
* @property {string[]} http.origins - The allowed origins for HTTP requests.
* @property {Object} user - Configuration for user settings.
* @property {string|null} user.name - The name of the user.
* @property {string|null} user.password - The password of the user.
* @property {Object} ssh - Configuration for SSH settings.
* @property {string|null} ssh.host - The SSH host.
* @property {number} ssh.port - The SSH port.
* @property {string} ssh.term - The SSH terminal type.
* @property {number} ssh.readyTimeout - The SSH ready timeout.
* @property {number} ssh.keepaliveInterval - The SSH keepalive interval.
* @property {number} ssh.keepaliveCountMax - The SSH keepalive count maximum.
* @property {Object} header - Configuration for header settings.
* @property {string|null} header.text - The header text.
* @property {string} header.background - The header background color.
* @property {Object} options - Configuration for options settings.
* @property {boolean} options.challengeButton - Whether to show the challenge button.
* @property {boolean} options.autoLog - Whether to automatically log.
* @property {boolean} options.allowReauth - Whether to allow reauthentication.
* @property {boolean} options.allowReconnect - Whether to allow reconnection.
* @property {boolean} options.allowReplay - Whether to allow replay.
* @property {Object} algorithms - Configuration for algorithms settings.
* @property {string[]} algorithms.kex - The key exchange algorithms.
* @property {string[]} algorithms.cipher - The cipher algorithms.
* @property {string[]} algorithms.hmac - The HMAC algorithms.
* @property {string[]} algorithms.compress - The compression algorithms.
* @property {Object} session - Configuration for session settings.
* @property {string} session.secret - The session secret.
* @property {string} session.name - The session name.
*/
const config = loadConfig()
function getCorsConfig() {
return {
origin: config.http.origins,
methods: ["GET", "POST"],
credentials: true
}
}
// Extend the config object with the getCorsConfig function
config.getCorsConfig = getCorsConfig
module.exports = config

104
app/configSchema.js Normal file
View file

@ -0,0 +1,104 @@
/**
* Schema for validating the config
*/
const configSchema = {
type: "object",
properties: {
listen: {
type: "object",
properties: {
ip: { type: "string", format: "ipv4" },
port: { type: "integer", minimum: 1, maximum: 65535 }
},
required: ["ip", "port"]
},
http: {
type: "object",
properties: {
origins: {
type: "array",
items: { type: "string" }
}
},
required: ["origins"]
},
user: {
type: "object",
properties: {
name: { type: ["string", "null"] },
password: { type: ["string", "null"] }
},
required: ["name", "password"]
},
ssh: {
type: "object",
properties: {
host: { type: ["string", "null"] },
port: { type: "integer", minimum: 1, maximum: 65535 },
term: { type: "string" },
readyTimeout: { type: "integer" },
keepaliveInterval: { type: "integer" },
keepaliveCountMax: { type: "integer" }
},
required: [
"host",
"port",
"term",
"readyTimeout",
"keepaliveInterval",
"keepaliveCountMax"
]
},
header: {
type: "object",
properties: {
text: { type: ["string", "null"] },
background: { type: "string" }
},
required: ["text", "background"]
},
options: {
type: "object",
properties: {
challengeButton: { type: "boolean" },
autoLog: { type: "boolean" },
allowReauth: { type: "boolean" },
allowReconnect: { type: "boolean" },
allowReplay: { type: "boolean" }
},
required: ["challengeButton", "allowReauth", "allowReplay"]
},
algorithms: {
type: "object",
properties: {
kex: {
type: "array",
items: { type: "string" }
},
cipher: {
type: "array",
items: { type: "string" }
},
hmac: {
type: "array",
items: { type: "string" }
},
compress: {
type: "array",
items: { type: "string" }
}
},
required: ["kex", "cipher", "hmac", "compress"]
},
session: {
type: "object",
properties: {
secret: { type: "string" },
name: { type: "string" }
},
required: ["secret", "name"]
}
},
required: ["listen", "http", "user", "ssh", "header", "options", "algorithms"]
}
module.exports = configSchema

61
app/connectionHandler.js Normal file
View file

@ -0,0 +1,61 @@
// server
// app/connectionHandler.js
const fs = require("fs")
const path = require("path")
const { createNamespacedDebug } = require("./logger")
const { HTTP, MESSAGES, DEFAULTS } = require("./constants")
const { modifyHtml } = require("./utils")
const debug = createNamespacedDebug("connectionHandler")
/**
* Handle reading the file and processing the response.
* @param {string} filePath - The path to the HTML file.
* @param {Object} config - The configuration object to inject into the HTML.
* @param {Object} res - The Express response object.
*/
function handleFileRead(filePath, config, res) {
// eslint-disable-next-line consistent-return
fs.readFile(filePath, "utf8", (err, data) => {
if (err) {
return res
.status(HTTP.INTERNAL_SERVER_ERROR)
.send(MESSAGES.CLIENT_FILE_ERROR)
}
const modifiedHtml = modifyHtml(data, config)
res.send(modifiedHtml)
})
}
/**
* Handle the connection request and send the modified client HTML.
* @param {Object} req - The Express request object.
* @param {Object} res - The Express response object.
*/
function handleConnection(req, res) {
debug("Handling connection req.path:", req.path)
const clientPath = path.resolve(
__dirname,
"..",
"node_modules",
"webssh2_client",
"client",
"public"
)
const tempConfig = {
socket: {
url: `${req.protocol}://${req.get("host")}`,
path: "/ssh/socket.io"
},
autoConnect: req.path.startsWith("/host/") // Automatically connect if path starts with /host/
}
const filePath = path.join(clientPath, DEFAULTS.CLIENT_FILE)
handleFileRead(filePath, tempConfig, res)
}
module.exports = handleConnection

63
app/constants.js Normal file
View file

@ -0,0 +1,63 @@
// server
// app/constants.js
const path = require("path")
/**
* Error messages
*/
const MESSAGES = {
INVALID_CREDENTIALS: "Invalid credentials format",
SSH_CONNECTION_ERROR: "SSH CONNECTION ERROR",
SHELL_ERROR: "SHELL ERROR",
CONFIG_ERROR: "CONFIG_ERROR",
UNEXPECTED_ERROR: "An unexpected error occurred",
EXPRESS_APP_CONFIG_ERROR: "Failed to configure Express app",
CLIENT_FILE_ERROR: "Error loading client file",
FAILED_SESSION_SAVE: "Failed to save session",
CONFIG_VALIDATION_ERROR: "Config validation error"
}
/**
* Default values
*/
const DEFAULTS = {
SSH_PORT: 22,
LISTEN_PORT: 2222,
SSH_TERM: "xterm-color",
IO_PING_TIMEOUT: 60000, // 1 minute
IO_PING_INTERVAL: 25000, // 25 seconds
IO_PATH: "/ssh/socket.io",
WEBSSH2_CLIENT_PATH: path.resolve(
__dirname,
"..",
"node_modules",
"webssh2_client",
"client",
"public"
),
CLIENT_FILE: "client.htm"
}
/**
* HTTP Related
*/
const HTTP = {
OK: 200,
UNAUTHORIZED: 401,
INTERNAL_SERVER_ERROR: 500,
AUTHENTICATE: "WWW-Authenticate",
REALM: 'Basic realm="WebSSH2"',
AUTH_REQUIRED: "Authentication required.",
COOKIE: "basicauth",
PATH: "/ssh/host/",
SAMESITE: "Strict",
SESSION_SID: "webssh2_sid",
CREDS_CLEARED: "Credentials cleared."
}
module.exports = {
MESSAGES,
DEFAULTS,
HTTP
}

16
app/crypto-utils.js Normal file
View file

@ -0,0 +1,16 @@
// server
// app/crypto-utils.js
const crypto = require("crypto")
/**
* Generates a secure random session secret
* @returns {string} A random 32-byte hex string
*/
function generateSecureSecret() {
return crypto.randomBytes(32).toString("hex")
}
module.exports = {
generateSecureSecret
}

74
app/errors.js Normal file
View file

@ -0,0 +1,74 @@
// server
// app/errors.js
const util = require("util")
const { logError, createNamespacedDebug } = require("./logger")
const { HTTP, MESSAGES } = require("./constants")
const debug = createNamespacedDebug("errors")
/**
* Custom error for WebSSH2
* @param {string} message - The error message
* @param {string} code - The error code
*/
function WebSSH2Error(message, code) {
Error.captureStackTrace(this, this.constructor)
this.name = this.constructor.name
this.message = message
this.code = code
}
util.inherits(WebSSH2Error, Error)
/**
* Custom error for configuration issues
* @param {string} message - The error message
*/
function ConfigError(message) {
WebSSH2Error.call(this, message, MESSAGES.CONFIG_ERROR)
}
util.inherits(ConfigError, WebSSH2Error)
/**
* Custom error for SSH connection issues
* @param {string} message - The error message
*/
function SSHConnectionError(message) {
WebSSH2Error.call(this, message, MESSAGES.SSH_CONNECTION_ERROR)
}
util.inherits(SSHConnectionError, WebSSH2Error)
/**
* Handles an error by logging it and optionally sending a response
* @param {Error} err - The error to handle
* @param {Object} [res] - The response object (if in an Express route)
*/
function handleError(err, res) {
if (err instanceof WebSSH2Error) {
logError(err.message, err)
debug(err.message)
if (res) {
res
.status(HTTP.INTERNAL_SERVER_ERROR)
.json({ error: err.message, code: err.code })
}
} else {
logError(MESSAGES.UNEXPECTED_ERROR, err)
debug(`handleError: ${MESSAGES.UNEXPECTED_ERROR}: %O`, err)
if (res) {
res
.status(HTTP.INTERNAL_SERVER_ERROR)
.json({ error: MESSAGES.UNEXPECTED_ERROR })
}
}
}
module.exports = {
WebSSH2Error: WebSSH2Error,
ConfigError: ConfigError,
SSHConnectionError: SSHConnectionError,
handleError: handleError
}

View file

@ -1,29 +0,0 @@
'use strict'
/* jshint esversion: 6, asi: true, node: true */
/*
* index.js
*
* WebSSH2 - Web to SSH2 gateway
* Bill Church - https://github.com/billchurch/WebSSH2 - May 2017
*
*/
var config = require('./server/app').config
var server = require('./server/app').server
server.listen({ host: config.listen.ip, port: config.listen.port
})
console.log('WebSSH2 service listening on ' + config.listen.ip + ':' + config.listen.port)
server.on('error', function (err) {
if (err.code === 'EADDRINUSE') {
config.listen.port++
console.warn('WebSSH2 Address in use, retrying on port ' + config.listen.port)
setTimeout(function () {
server.listen(config.listen.port)
}, 250)
} else {
console.log('WebSSH2 server.listen ERROR: ' + err.code)
}
})

36
app/io.js Normal file
View file

@ -0,0 +1,36 @@
const socketIo = require("socket.io")
const sharedsession = require("express-socket.io-session")
const { createNamespacedDebug } = require("./logger")
const { DEFAULTS } = require("./constants")
const debug = createNamespacedDebug("app")
/**
* Configures Socket.IO with the given server
* @param {http.Server} server - The HTTP server instance
* @param {Function} sessionMiddleware - The session middleware
* @param {Object} config - The configuration object
* @returns {import('socket.io').Server} The Socket.IO server instance
*/
function configureSocketIO(server, sessionMiddleware, config) {
const io = socketIo(server, {
serveClient: false,
path: DEFAULTS.IO_PATH,
pingTimeout: DEFAULTS.IO_PING_TIMEOUT,
pingInterval: DEFAULTS.IO_PING_INTERVAL,
cors: config.getCorsConfig()
})
// Share session with io sockets
io.use(
sharedsession(sessionMiddleware, {
autoSave: true
})
)
debug("IO configured")
return io
}
module.exports = { configureSocketIO }

30
app/logger.js Normal file
View file

@ -0,0 +1,30 @@
// server
// app/logger.js
const createDebug = require("debug")
/**
* Creates a debug function for a specific namespace
* @param {string} namespace - The debug namespace
* @returns {Function} The debug function
*/
function createNamespacedDebug(namespace) {
return createDebug(`webssh2:${namespace}`)
}
/**
* Logs an error message
* @param {string} message - The error message
* @param {Error} [error] - The error object
*/
function logError(message, error) {
console.error(message)
if (error) {
console.error(`ERROR: ${error}`)
}
}
module.exports = {
createNamespacedDebug: createNamespacedDebug,
logError: logError
}

77
app/middleware.js Normal file
View file

@ -0,0 +1,77 @@
// server
// app/middleware.js
const createDebug = require("debug")
const session = require("express-session")
const bodyParser = require("body-parser")
const debug = createDebug("webssh2:middleware")
const { HTTP } = require("./constants")
/**
* Creates and configures session middleware
* @param {Object} config - The configuration object
* @returns {Function} The session middleware
*/
function createSessionMiddleware(config) {
return session({
secret: config.session.secret,
resave: false,
saveUninitialized: true,
name: config.session.name
})
}
/**
* Creates body parser middleware
* @returns {Function[]} Array of body parser middleware
*/
function createBodyParserMiddleware() {
return [bodyParser.urlencoded({ extended: true }), bodyParser.json()]
}
/**
* Creates cookie-setting middleware
* @returns {Function} The cookie-setting middleware
*/
function createCookieMiddleware() {
return (req, res, next) => {
if (req.session.sshCredentials) {
const cookieData = {
host: req.session.sshCredentials.host,
port: req.session.sshCredentials.port
}
res.cookie(HTTP.COOKIE, JSON.stringify(cookieData), {
httpOnly: false,
path: HTTP.PATH,
sameSite: HTTP.SAMESITE
})
}
next()
}
}
/**
* Applies all middleware to the Express app
* @param {express.Application} app - The Express application
* @param {Object} config - The configuration object
* @returns {Object} An object containing the session middleware
*/
function applyMiddleware(app, config) {
const sessionMiddleware = createSessionMiddleware(config)
app.use(sessionMiddleware)
app.use(createBodyParserMiddleware())
app.use(createCookieMiddleware())
debug("applyMiddleware")
return { sessionMiddleware }
}
module.exports = {
applyMiddleware,
createSessionMiddleware,
createBodyParserMiddleware,
createCookieMiddleware
}

View file

@ -1,84 +0,0 @@
{
"name": "webssh2",
"version": "0.2.9",
"ignore": [
".gitignore"
],
"bin": "./index.js",
"description": "A Websocket to SSH2 gateway using term.js, socket.io, ssh2, and express",
"homepage": "https://github.com/billchurch/WebSSH2",
"keywords": "ssh webssh terminal webterminal",
"license": "SEE LICENSE IN FILE - LICENSE",
"private": false,
"repository": {
"type": "git",
"url": "git+https://github.com/billchurch/WebSSH2.git"
},
"contributors": [
{
"name": "Bill Church",
"email": "wmchurch@gmail.com"
}
],
"engines": {
"node": ">= 6"
},
"bugs": {
"url": "https://github.com/billchurch/WebSSH2/issues"
},
"dependencies": {
"basic-auth": "~2.0.1",
"colors": "~1.3.2",
"compression": "~1.7.3",
"debug": "~4.1.0",
"express": "~4.16.4",
"express-session": "~1.15.6",
"morgan": "~1.9.1",
"read-config": "git+https://github.com/billchurch/nodejs-read-config.git",
"socket.io": "~2.2.0",
"ssh2": "~0.6.1",
"validator": "~10.9.0"
},
"scripts": {
"start": "node index.js",
"build": "webpack --progress --colors --config scripts/webpack.prod.js",
"builddev": "webpack --progress --colors --config scripts/webpack.dev.js",
"analyze": "webpack --json --config scripts/webpack.prod.js | webpack-bundle-size-analyzer",
"test": "snyk test",
"watch": "nodemon index.js",
"standard": "standard --verbose --fix | snazzy",
"cleanmac": "find . -name '.DS_Store' -type f -delete"
},
"standard": {
"ignore": [
"client/public/webssh2.bundle.js",
"bigip/*",
"screenshots/*",
"bin/*",
"build/*",
"workspace/*"
]
},
"devDependencies": {
"@fortawesome/fontawesome-svg-core": "^1.2.12",
"@fortawesome/free-solid-svg-icons": "^5.6.3",
"clean-webpack-plugin": "^1.0.0",
"copy-webpack-plugin": "^4.6.0",
"cross-env": "^5.2.0",
"css-loader": "^2.1.0",
"extract-text-webpack-plugin": "^4.0.0-beta.0",
"file-loader": "^3.0.1",
"nodaemon": "0.0.5",
"postcss-discard-comments": "^4.0.1",
"snazzy": "^8.0.0",
"standard": "^12.0.1",
"style-loader": "^0.23.1",
"uglifyjs-webpack-plugin": "^2.1.1",
"url-loader": "^1.1.2",
"webpack": "^4.28.4",
"webpack-cli": "^3.2.1",
"webpack-merge": "^4.2.1",
"webpack-stream": "^5.2.1",
"xterm": "^3.10.1"
}
}

87
app/routes.js Normal file
View file

@ -0,0 +1,87 @@
// server
// app/routes.js
const express = require("express")
const basicAuth = require("basic-auth")
const validator = require("validator")
const {
getValidatedHost,
getValidatedPort,
maskSensitiveData,
validateSshTerm
} = require("./utils")
const handleConnection = require("./connectionHandler")
const { createNamespacedDebug } = require("./logger")
const { ConfigError, handleError } = require("./errors")
const { HTTP } = require("./constants")
const debug = createNamespacedDebug("routes")
const router = express.Router()
// eslint-disable-next-line consistent-return
function auth(req, res, next) {
debug("auth: Basic Auth")
const credentials = basicAuth(req)
if (!credentials) {
res.setHeader(HTTP.AUTHENTICATE, HTTP.REALM)
return res.status(HTTP.UNAUTHORIZED).send(HTTP.AUTH_REQUIRED)
}
// Validate and sanitize credentials
req.session.sshCredentials = {
username: validator.escape(credentials.name),
password: credentials.pass // We don't sanitize the password as it might contain special characters
}
req.session.usedBasicAuth = true // Set this flag when Basic Auth is used
next()
}
// Scenario 1: No auth required, uses websocket authentication instead
router.get("/", (req, res) => {
debug("router.get./: Accessed / route")
handleConnection(req, res)
})
// Scenario 2: Auth required, uses HTTP Basic Auth
router.get("/host/:host", auth, (req, res) => {
debug(`router.get.host: /ssh/host/${req.params.host} route`)
try {
const host = getValidatedHost(req.params.host)
const port = getValidatedPort(req.query.port)
// Validate and sanitize sshterm parameter if it exists
const sshterm = validateSshTerm(req.query.sshterm)
req.session.sshCredentials = req.session.sshCredentials || {}
req.session.sshCredentials.host = host
req.session.sshCredentials.port = port
if (req.query.sshterm) {
req.session.sshCredentials.term = sshterm
}
req.session.usedBasicAuth = true
// Sanitize and log the sshCredentials object
const sanitizedCredentials = maskSensitiveData(
JSON.parse(JSON.stringify(req.session.sshCredentials))
)
debug("/ssh/host/ Credentials: ", sanitizedCredentials)
handleConnection(req, res, { host: host })
} catch (err) {
const error = new ConfigError(`Invalid configuration: ${err.message}`)
handleError(error, res)
}
})
// Clear credentials route
router.get("/clear-credentials", (req, res) => {
req.session.sshCredentials = null
res.status(HTTP.OK).send(HTTP.CREDENTIALS_CLEARED)
})
router.get("/force-reconnect", (req, res) => {
req.session.sshCredentials = null
res.status(HTTP.UNAUTHORIZED).send(HTTP.AUTH_REQUIRED)
})
module.exports = router

View file

@ -1,41 +0,0 @@
const webpack = require('webpack')
const path = require('path')
const CleanWebpackPlugin = require('clean-webpack-plugin')
const CopyWebpackPlugin = require('copy-webpack-plugin')
const ExtractTextPlugin = require('extract-text-webpack-plugin')
module.exports = {
context: path.resolve('__dirname', '../'),
entry: {
webssh2: './client/src/js/index.js'
},
plugins: [
new CleanWebpackPlugin(['client/public'], {
root: path.resolve('__dirname', '../'),
verbose: true
}),
new CopyWebpackPlugin([
'./client/src/client.htm',
'./client/src/favicon.ico'
]),
new ExtractTextPlugin('[name].css')
],
output: {
filename: '[name].bundle.js',
path: path.resolve(__dirname, '../client/public')
},
module: {
rules: [
{
test: /\.css$/,
use: ExtractTextPlugin.extract({
fallback: 'style-loader',
use: [
{
loader: 'css-loader'
}
]
})
}
]
}
}

View file

@ -1,9 +0,0 @@
const merge = require('webpack-merge')
const common = require('./webpack.common.js')
module.exports = merge(common, {
devtool: 'inline-source-map',
devServer: {
contentBase: '../client/public'
}
})

View file

@ -1,18 +0,0 @@
const merge = require('webpack-merge')
const UglifyJSPlugin = require('uglifyjs-webpack-plugin')
const common = require('./webpack.common.js')
module.exports = merge(common, {
plugins: [
new UglifyJSPlugin({
uglifyOptions: {
ie8: false,
dead_code: true,
output: {
comments: false,
beautify: false
}
}
})
]
})

37
app/server.js Normal file
View file

@ -0,0 +1,37 @@
const http = require("http")
// const { createNamespacedDebug } = require("./logger")
// const debug = createNamespacedDebug("server")
/**
* Creates and configures the HTTP server
* @param {express.Application} app - The Express application instance
* @returns {http.Server} The HTTP server instance
*/
function createServer(app) {
return http.createServer(app)
}
/**
* Handles server errors
* @param {Error} err - The error object
*/
function handleServerError(err) {
console.error("HTTP Server ERROR: %O", err)
}
/**
* Starts the server
* @param {http.Server} server - The server instance
* @param {Object} config - The configuration object
*/
function startServer(server, config) {
server.listen(config.listen.port, config.listen.ip, () => {
console.log(
`startServer: listening on ${config.listen.ip}:${config.listen.port}`
)
})
server.on("error", handleServerError)
}
module.exports = { createServer, startServer }

View file

@ -1,193 +0,0 @@
'use strict'
/* jshint esversion: 6, asi: true, node: true */
// app.js
var path = require('path')
var fs = require('fs')
var nodeRoot = path.dirname(require.main.filename)
var configPath = path.join(nodeRoot, 'config.json')
var publicPath = path.join(nodeRoot, 'client', 'public')
console.log('WebSSH2 service reading config from: ' + configPath)
var express = require('express')
var logger = require('morgan')
// sane defaults if config.json or parts are missing
let config = {
'listen': {
'ip': '0.0.0.0',
'port': 2222
},
'user': {
'name': null,
'password': null
},
'ssh': {
'host': null,
'port': 22,
'term': 'xterm-color',
'readyTimeout': 20000,
'keepaliveInterval': 120000,
'keepaliveCountMax': 10
},
'terminal': {
'cursorBlink': true,
'scrollback': 10000,
'tabStopWidth': 8,
'bellStyle': 'sound'
},
'header': {
'text': null,
'background': 'green'
},
'session': {
'name': 'WebSSH2',
'secret': 'mysecret'
},
'options': {
'challengeButton': true,
'allowreauth': true
},
'algorithms': {
'kex': [
'ecdh-sha2-nistp256',
'ecdh-sha2-nistp384',
'ecdh-sha2-nistp521',
'diffie-hellman-group-exchange-sha256',
'diffie-hellman-group14-sha1'
],
'cipher': [
'aes128-ctr',
'aes192-ctr',
'aes256-ctr',
'aes128-gcm',
'aes128-gcm@openssh.com',
'aes256-gcm',
'aes256-gcm@openssh.com',
'aes256-cbc'
],
'hmac': [
'hmac-sha2-256',
'hmac-sha2-512',
'hmac-sha1'
],
'compress': [
'none',
'zlib@openssh.com',
'zlib'
]
},
'serverlog': {
'client': false,
'server': false
},
'accesslog': false,
'verify': false
}
// test if config.json exists, if not provide error message but try to run
// anyway
try {
if (fs.existsSync(configPath)) {
console.log('ephemeral_auth service reading config from: ' + configPath)
config = require('read-config')(configPath)
} else {
console.error('\n\nERROR: Missing config.json for webssh. Current config: ' + JSON.stringify(config))
console.error('\n See config.json.sample for details\n\n')
}
} catch (err) {
console.error('\n\nERROR: Missing config.json for webssh. Current config: ' + JSON.stringify(config))
console.error('\n See config.json.sample for details\n\n')
console.error('ERROR:\n\n ' + err)
}
var session = require('express-session')({
secret: config.session.secret,
name: config.session.name,
resave: true,
saveUninitialized: false,
unset: 'destroy'
})
var app = express()
var compression = require('compression')
var server = require('http').Server(app)
var myutil = require('./util')
var validator = require('validator')
var io = require('socket.io')(server, { serveClient: false })
var socket = require('./socket')
var expressOptions = require('./expressOptions')
// express
app.use(compression({ level: 9 }))
app.use(session)
app.use(myutil.basicAuth)
if (config.accesslog) app.use(logger('common'))
app.disable('x-powered-by')
// static files
app.use(express.static(publicPath, expressOptions))
app.get('/reauth', function (req, res, next) {
var 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>')
})
app.get('/ssh/host/:host?', function (req, res, next) {
res.sendFile(path.join(path.join(publicPath, 'client.htm')))
// capture, assign, and validated variables
req.session.ssh = {
host: (validator.isIP(req.params.host + '') && req.params.host) ||
(validator.isFQDN(req.params.host) && req.params.host) ||
(/^(([a-z]|[A-Z]|[0-9]|[!^(){}\-_~])+)?\w$/.test(req.params.host) &&
req.params.host) || config.ssh.host,
port: (validator.isInt(req.query.port + '', { min: 1, max: 65535 }) &&
req.query.port) || config.ssh.port,
header: {
name: req.query.header || config.header.text,
background: req.query.headerBackground || config.header.background
},
algorithms: config.algorithms,
keepaliveInterval: config.ssh.keepaliveInterval,
keepaliveCountMax: config.ssh.keepaliveCountMax,
term: (/^(([a-z]|[A-Z]|[0-9]|[!^(){}\-_~])+)?\w$/.test(req.query.sshterm) &&
req.query.sshterm) || config.ssh.term,
terminal: {
cursorBlink: (validator.isBoolean(req.query.cursorBlink + '') ? myutil.parseBool(req.query.cursorBlink) : config.terminal.cursorBlink),
scrollback: (validator.isInt(req.query.scrollback + '', { min: 1, max: 200000 }) && req.query.scrollback) ? req.query.scrollback : config.terminal.scrollback,
tabStopWidth: (validator.isInt(req.query.tabStopWidth + '', { min: 1, max: 100 }) && req.query.tabStopWidth) ? req.query.tabStopWidth : config.terminal.tabStopWidth,
bellStyle: ((req.query.bellStyle) && (['sound', 'none'].indexOf(req.query.bellStyle) > -1)) ? req.query.bellStyle : config.terminal.bellStyle
},
allowreplay: config.options.challengeButton || (validator.isBoolean(req.headers.allowreplay + '') ? myutil.parseBool(req.headers.allowreplay) : false),
allowreauth: config.options.allowreauth || false,
mrhsession: ((validator.isAlphanumeric(req.headers.mrhsession + '') && req.headers.mrhsession) ? req.headers.mrhsession : 'none'),
serverlog: {
client: config.serverlog.client || false,
server: config.serverlog.server || false
},
readyTimeout: (validator.isInt(req.query.readyTimeout + '', { min: 1, max: 300000 }) &&
req.query.readyTimeout) || config.ssh.readyTimeout
}
if (req.session.ssh.header.name) validator.escape(req.session.ssh.header.name)
if (req.session.ssh.header.background) validator.escape(req.session.ssh.header.background)
})
// express error handling
app.use(function (req, res, next) {
res.status(404).send("Sorry can't find that!")
})
app.use(function (err, req, res, next) {
console.error(err.stack)
res.status(500).send('Something broke!')
})
// socket.io
// expose express session with socket.request.session
io.use(function (socket, next) {
(socket.request.res) ? session(socket.request, socket.request.res, next)
: next(next)
})
// bring up socket
io.on('connection', socket)
module.exports = { server: server, config: config }

View file

@ -1,11 +0,0 @@
module.exports = {
dotfiles: 'ignore',
etag: false,
extensions: ['htm', 'html'],
index: false,
maxAge: '1s',
redirect: false,
setHeaders: function (res, path, stat) {
res.set('x-timestamp', Date.now())
}
}

View file

@ -1,173 +0,0 @@
'use strict'
/* jshint esversion: 6, asi: true, node: true */
// socket.js
// private
var debug = require('debug')
var debugWebSSH2 = require('debug')('WebSSH2')
var SSH = require('ssh2').Client
// var fs = require('fs')
// var hostkeys = JSON.parse(fs.readFileSync('./hostkeyhashes.json', 'utf8'))
var termCols, termRows
var menuData = '<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>'
// public
module.exports = function socket (socket) {
// if websocket connection arrives without an express session, kill it
if (!socket.request.session) {
socket.emit('401 UNAUTHORIZED')
debugWebSSH2('SOCKET: No Express Session / REJECTED')
socket.disconnect(true)
return
}
var conn = new SSH()
socket.on('geometry', function socketOnGeometry (cols, rows) {
termCols = cols
termRows = rows
})
conn.on('banner', function connOnBanner (data) {
// need to convert to cr/lf for proper formatting
data = data.replace(/\r?\n/g, '\r\n')
socket.emit('data', data.toString('utf-8'))
})
conn.on('ready', function connOnReady () {
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)
socket.emit('menu', menuData)
socket.emit('allowreauth', socket.request.session.ssh.allowreauth)
socket.emit('setTerminalOpts', socket.request.session.ssh.terminal)
socket.emit('title', 'ssh://' + socket.request.session.ssh.host)
if (socket.request.session.ssh.header.background) socket.emit('headerBackground', socket.request.session.ssh.header.background)
if (socket.request.session.ssh.header.name) socket.emit('header', socket.request.session.ssh.header.name)
socket.emit('footer', 'ssh://' + socket.request.session.username + '@' + socket.request.session.ssh.host + ':' + socket.request.session.ssh.port)
socket.emit('status', 'SSH CONNECTION ESTABLISHED')
socket.emit('statusBackground', 'green')
socket.emit('allowreplay', socket.request.session.ssh.allowreplay)
conn.shell({
term: socket.request.session.ssh.term,
cols: termCols,
rows: termRows
}, function connShell (err, stream) {
if (err) {
SSHerror('EXEC ERROR' + err)
conn.end()
return
}
// poc to log commands from client
if (socket.request.session.ssh.serverlog.client) var dataBuffer
socket.on('data', function socketOnData (data) {
stream.write(data)
// 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) {
case 'replayCredentials':
if (socket.request.session.ssh.allowreplay) {
stream.write(socket.request.session.userpassword + '\n')
}
/* falls through */
default:
console.log('controlData: ' + controlData)
}
})
socket.on('resize', function socketOnResize (data) {
stream.setWindow(data.rows, data.cols)
})
socket.on('disconnecting', function socketOnDisconnecting (reason) { debugWebSSH2('SOCKET DISCONNECTING: ' + reason) })
socket.on('disconnect', function socketOnDisconnect (reason) {
debugWebSSH2('SOCKET DISCONNECT: ' + reason)
err = { message: reason }
SSHerror('CLIENT SOCKET DISCONNECT', err)
conn.end()
// socket.request.session.destroy()
})
socket.on('error', function socketOnError (err) {
SSHerror('SOCKET ERROR', err)
conn.end()
})
stream.on('data', function streamOnData (data) { socket.emit('data', data.toString('utf-8')) })
stream.on('close', function streamOnClose (code, signal) {
err = { message: ((code || signal) ? (((code) ? 'CODE: ' + code : '') + ((code && signal) ? ' ' : '') + ((signal) ? 'SIGNAL: ' + signal : '')) : undefined) }
SSHerror('STREAM CLOSE', err)
conn.end()
})
stream.stderr.on('data', function streamStderrOnData (data) {
console.log('STDERR: ' + data)
})
})
})
conn.on('end', function connOnEnd (err) { SSHerror('CONN END BY HOST', err) })
conn.on('close', function connOnClose (err) { SSHerror('CONN CLOSE', err) })
conn.on('error', function connOnError (err) { SSHerror('CONN ERROR', err) })
conn.on('keyboard-interactive', function connOnKeyboardInteractive (name, instructions, instructionsLang, prompts, finish) {
debugWebSSH2('conn.on(\'keyboard-interactive\')')
finish([socket.request.session.userpassword])
})
if (socket.request.session.username && socket.request.session.userpassword && socket.request.session.ssh) {
// console.log('hostkeys: ' + hostkeys[0].[0])
conn.connect({
host: socket.request.session.ssh.host,
port: socket.request.session.ssh.port,
username: socket.request.session.username,
password: socket.request.session.userpassword,
tryKeyboard: true,
algorithms: socket.request.session.ssh.algorithms,
readyTimeout: socket.request.session.ssh.readyTimeout,
keepaliveInterval: socket.request.session.ssh.keepaliveInterval,
keepaliveCountMax: socket.request.session.ssh.keepaliveCountMax,
debug: debug('ssh2')
})
} else {
debugWebSSH2('Attempt to connect without session.username/password or session varialbles defined, potentially previously abandoned client session. disconnecting websocket client.\r\nHandshake information: \r\n ' + JSON.stringify(socket.handshake))
socket.emit('ssherror', 'WEBSOCKET ERROR - Refresh the browser and try again')
socket.request.session.destroy()
socket.disconnect(true)
}
/**
* 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
*/
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)
}
}

View file

@ -1,30 +0,0 @@
'use strict'
/* jshint esversion: 6, asi: true, node: true */
// util.js
// private
require('colors') // allow for color property extensions in log messages
var debug = require('debug')('WebSSH2')
var Auth = require('basic-auth')
exports.basicAuth = function basicAuth (req, res, next) {
var myAuth = Auth(req)
if (myAuth && myAuth.pass !== '') {
req.session.username = myAuth.name
req.session.userpassword = myAuth.pass
debug('myAuth.name: ' + myAuth.name.yellow.bold.underline +
' and password ' + ((myAuth.pass) ? 'exists'.yellow.bold.underline
: 'is blank'.underline.red.bold))
next()
} else {
res.statusCode = 401
debug('basicAuth credential request (401)')
res.setHeader('WWW-Authenticate', 'Basic realm="WebSSH"')
res.end('Username and password required for web SSH service.')
}
}
// takes a string, makes it boolean (true if the string is true, false otherwise)
exports.parseBool = function parseBool (str) {
return (str.toLowerCase() === 'true')
}

328
app/socket.js Normal file
View file

@ -0,0 +1,328 @@
// server
// app/socket.js
const validator = require("validator")
const EventEmitter = require("events")
const SSHConnection = require("./ssh")
const { createNamespacedDebug } = require("./logger")
const { SSHConnectionError, handleError } = require("./errors")
const debug = createNamespacedDebug("socket")
const {
isValidCredentials,
maskSensitiveData,
validateSshTerm
} = require("./utils")
const { MESSAGES } = require("./constants")
class WebSSH2Socket extends EventEmitter {
constructor(socket, config) {
super()
this.socket = socket
this.config = config
this.ssh = new SSHConnection(config)
this.sessionState = {
authenticated: false,
username: null,
password: null,
host: null,
port: null,
term: null,
cols: null,
rows: null
}
this.initializeSocketEvents()
}
initializeSocketEvents() {
debug(`io.on connection: ${this.socket.id}`)
if (
this.socket.handshake.session.usedBasicAuth &&
this.socket.handshake.session.sshCredentials
) {
const creds = this.socket.handshake.session.sshCredentials
debug(
`handleConnection: ${this.socket.id}, Host: ${creds.host}: HTTP Basic Credentials Exist, creds: %O`,
maskSensitiveData(creds)
)
this.handleAuthenticate(creds)
} else if (!this.sessionState.authenticated) {
debug(`handleConnection: ${this.socket.id}, emitting request_auth`)
this.socket.emit("authentication", { action: "request_auth" })
}
this.ssh.on("keyboard-interactive", data => {
this.handleKeyboardInteractive(data)
})
this.socket.on("authenticate", creds => {
this.handleAuthenticate(creds)
})
this.socket.on("terminal", data => {
this.handleTerminal(data)
})
this.socket.on("disconnect", reason => {
this.handleConnectionClose(reason)
})
}
handleKeyboardInteractive(data) {
const self = this
debug(`handleKeyboardInteractive: ${this.socket.id}, %O`, data)
// Send the keyboard-interactive request to the client
this.socket.emit(
"authentication",
Object.assign(
{
action: "keyboard-interactive"
},
data
)
)
// Set up a one-time listener for the client's response
this.socket.once("authentication", clientResponse => {
const maskedclientResponse = maskSensitiveData(clientResponse, {
properties: ["responses"]
})
debug(
"handleKeyboardInteractive: Client response masked %O",
maskedclientResponse
)
if (clientResponse.action === "keyboard-interactive") {
// Forward the client's response to the SSH connection
self.ssh.emit("keyboard-interactive-response", clientResponse.responses)
}
})
}
handleAuthenticate(creds) {
debug(`handleAuthenticate: ${this.socket.id}, %O`, maskSensitiveData(creds))
if (isValidCredentials(creds)) {
this.sessionState.term = validateSshTerm(creds.term)
? creds.term
: this.config.ssh.term
this.initializeConnection(creds)
} else {
debug(`handleAuthenticate: ${this.socket.id}, CREDENTIALS INVALID`)
this.socket.emit("authentication", {
success: false,
message: "Invalid credentials format"
})
}
}
initializeConnection(creds) {
const self = this
debug(
`initializeConnection: ${this.socket.id}, INITIALIZING SSH CONNECTION: Host: ${creds.host}, creds: %O`,
maskSensitiveData(creds)
)
this.ssh
.connect(creds)
.then(() => {
this.sessionState = Object.assign({}, this.sessionState, {
authenticated: true,
username: creds.username,
password: creds.password,
host: creds.host,
port: creds.port
})
const authResult = { action: "auth_result", success: true }
this.socket.emit("authentication", authResult)
const permissions = {
autoLog: this.config.options.autoLog || false,
allowReplay: this.config.options.allowReplay || false,
allowReconnect: this.config.options.allowReconnect || false,
allowReauth: this.config.options.allowReauth || false
}
this.socket.emit("permissions", permissions)
this.updateElement("footer", `ssh://${creds.host}:${creds.port}`)
if (this.config.header && this.config.header.text !== null) {
this.updateElement("header", this.config.header.text)
}
this.socket.emit("getTerminal", true)
})
.catch(err => {
debug(
`initializeConnection: SSH CONNECTION ERROR: ${this.socket.id}, Host: ${creds.host}, Error: ${err.message}`
)
handleError(new SSHConnectionError(`${err.message}`))
this.socket.emit("ssherror", `${err.message}`)
})
}
/**
* Handles terminal data.
* @param {Object} data - The terminal data.
*/
handleTerminal(data) {
const { term, rows, cols } = data
if (term && validateSshTerm(term)) this.sessionState.term = term
if (rows && validator.isInt(rows.toString()))
this.sessionState.rows = parseInt(rows, 10)
if (cols && validator.isInt(cols.toString()))
this.sessionState.cols = parseInt(cols, 10)
this.createShell()
}
/**
* Creates a new SSH shell session.
*/
createShell() {
this.ssh
.shell({
term: this.sessionState.term,
cols: this.sessionState.cols,
rows: this.sessionState.rows
})
.then(stream => {
stream.on("data", data => {
this.socket.emit("data", data.toString("utf-8"))
})
// stream.stderr.on("data", data => debug(`STDERR: ${data}`)) // needed for shell.exec
stream.on("close", (code, signal) => {
debug("close: SSH Stream closed")
this.handleConnectionClose(code, signal)
})
stream.on("end", () => {
debug("end: SSH Stream ended")
})
stream.on("error", (err) => {
debug("error: SSH Stream error %O", err)
})
this.socket.on("data", data => {
stream.write(data)
})
this.socket.on("control", controlData => {
this.handleControl(controlData)
})
this.socket.on("resize", data => {
this.handleResize(data)
})
})
.catch(err => this.handleError("createShell: ERROR", err))
}
handleResize(data) {
const { rows, cols } = data
if (rows && validator.isInt(rows.toString()))
this.sessionState.rows = parseInt(rows, 10)
if (cols && validator.isInt(cols.toString()))
this.sessionState.cols = parseInt(cols, 10)
this.ssh.resizeTerminal(this.sessionState.rows, this.sessionState.cols)
}
/**
* Handles control commands.
* @param {string} controlData - The control command received.
*/
handleControl(controlData) {
if (
validator.isIn(controlData, ["replayCredentials", "reauth"]) &&
this.ssh.stream
) {
if (controlData === "replayCredentials") {
this.replayCredentials()
} else if (controlData === "reauth") {
this.handleReauth()
}
} else {
console.warn(
`handleControl: Invalid control command received: ${controlData}`
)
}
}
/**
* Replays stored credentials.
*/
replayCredentials() {
if (this.config.options.allowReplay && this.ssh.stream) {
this.ssh.stream.write(`${this.sessionState.password}\n`)
}
}
/**
* Handles reauthentication.
*/
handleReauth() {
if (this.config.options.allowReauth) {
this.clearSessionCredentials()
this.socket.emit("authentication", { action: "reauth" })
}
}
/**
* Handles errors.
* @param {string} context - The error context.
* @param {Error} err - The error object.
*/
handleError(context, err) {
const errorMessage = err ? `: ${err.message}` : ""
handleError(new SSHConnectionError(`SSH ${context}${errorMessage}`))
this.socket.emit("ssherror", `SSH ${context}${errorMessage}`)
this.handleConnectionClose()
}
/**
* Updates a UI element on the client side.
* @param {string} element - The element to update.
* @param {any} value - The new value for the element.
*/
updateElement(element, value) {
this.socket.emit("updateUI", { element, value })
}
/**
* Handles the closure of the connection.
* @param {string} reason - The reason for the closure.
*/
handleConnectionClose(code, signal) {
this.ssh.end()
debug(
`handleConnectionClose: ${this.socket.id}, Code: ${code}, Signal: ${signal}`
)
this.socket.disconnect(true)
}
/**
* Clears session credentials.
*/
clearSessionCredentials() {
if (this.socket.handshake.session.sshCredentials) {
this.socket.handshake.session.sshCredentials.username = null
this.socket.handshake.session.sshCredentials.password = null
}
this.socket.handshake.session.usedBasicAuth = false
this.sessionState.authenticated = false
this.sessionState.username = null
this.sessionState.password = null
this.socket.handshake.session.save(err => {
if (err)
console.error(
`clearSessionCredentials: ${MESSAGES.FAILED_SESSION_SAVE} ${this.socket.id}:`,
err
)
})
}
}
module.exports = function(io, config) {
io.on("connection", socket => new WebSSH2Socket(socket, config))
}

205
app/ssh.js Normal file
View file

@ -0,0 +1,205 @@
// server
// app/ssh.js
const SSH = require("ssh2").Client
const EventEmitter = require("events")
const { createNamespacedDebug } = require("./logger")
const { SSHConnectionError, handleError } = require("./errors")
const { maskSensitiveData } = require("./utils")
const debug = createNamespacedDebug("ssh")
/**
* SSHConnection class handles SSH connections and operations.
* @extends EventEmitter
*/
class SSHConnection extends EventEmitter {
/**
* Create an SSHConnection.
* @param {Object} config - Configuration object for the SSH connection.
*/
constructor(config) {
super()
this.config = config
this.conn = null
this.stream = null
this.creds = null
}
/**
* Connects to the SSH server using the provided credentials.
* @param {Object} creds - The credentials object containing host, port, username, and password.
* @returns {Promise<SSH>} - A promise that resolves with the SSH connection instance.
*/
connect(creds) {
this.creds = creds
debug("connect: %O", maskSensitiveData(creds))
return new Promise((resolve, reject) => {
if (this.conn) {
this.conn.end()
}
this.conn = new SSH()
const sshConfig = this.getSSHConfig(creds)
this.conn.on("ready", () => {
debug(`connect: ready: ${creds.host}`)
resolve(this.conn)
})
this.conn.on("end", () => {
debug(`connect: end: `)
})
this.conn.on("close", () => {
debug(`connect: close: `)
})
this.conn.on("error", err => {
const error = new SSHConnectionError(`${err.message}`)
handleError(error)
reject(error)
})
this.conn.on(
"keyboard-interactive",
(name, instructions, lang, prompts, finish) => {
this.handleKeyboardInteractive(
name,
instructions,
lang,
prompts,
finish
)
}
)
this.conn.connect(sshConfig)
})
}
/**
* Handles keyboard-interactive authentication prompts.
* @param {string} name - The name of the authentication request.
* @param {string} instructions - The instructions for the keyboard-interactive prompt.
* @param {string} lang - The language of the prompt.
* @param {Array<Object>} prompts - The list of prompts provided by the server.
* @param {Function} finish - The callback to complete the keyboard-interactive authentication.
*/
handleKeyboardInteractive(name, instructions, lang, prompts, finish) {
debug("handleKeyboardInteractive: Keyboard-interactive auth %O", prompts)
// Check if we should always send prompts to the client
if (this.config.ssh.alwaysSendKeyboardInteractivePrompts) {
this.sendPromptsToClient(name, instructions, prompts, finish)
return
}
const responses = []
let shouldSendToClient = false
for (let i = 0; i < prompts.length; i += 1) {
if (
prompts[i].prompt.toLowerCase().includes("password") &&
this.creds.password
) {
responses.push(this.creds.password)
} else {
shouldSendToClient = true
break
}
}
if (shouldSendToClient) {
this.sendPromptsToClient(name, instructions, prompts, finish)
} else {
finish(responses)
}
}
/**
* Sends prompts to the client for keyboard-interactive authentication.
*
* @param {string} name - The name of the authentication method.
* @param {string} instructions - The instructions for the authentication.
* @param {Array<{ prompt: string, echo: boolean }>} prompts - The prompts to be sent to the client.
* @param {Function} finish - The callback function to be called when the client responds.
*/
sendPromptsToClient(name, instructions, prompts, finish) {
this.emit("keyboard-interactive", {
name: name,
instructions: instructions,
prompts: prompts.map(p => ({ prompt: p.prompt, echo: p.echo }))
})
this.once("keyboard-interactive-response", responses => {
finish(responses)
})
}
/**
* Generates the SSH configuration object based on credentials.
* @param {Object} creds - The credentials object containing host, port, username, and password.
* @returns {Object} - The SSH configuration object.
*/
getSSHConfig(creds) {
return {
host: creds.host,
port: creds.port,
username: creds.username,
password: creds.password,
tryKeyboard: true,
algorithms: this.config.ssh.algorithms,
readyTimeout: this.config.ssh.readyTimeout,
keepaliveInterval: this.config.ssh.keepaliveInterval,
keepaliveCountMax: this.config.ssh.keepaliveCountMax,
debug: createNamespacedDebug("ssh2")
}
}
/**
* Opens an interactive shell session over the SSH connection.
* @param {Object} [options] - Optional parameters for the shell.
* @returns {Promise<Object>} - A promise that resolves with the SSH shell stream.
*/
shell(options) {
return new Promise((resolve, reject) => {
this.conn.shell(options, (err, stream) => {
if (err) {
reject(err)
} else {
this.stream = stream
resolve(stream)
}
})
})
}
/**
* Resizes the terminal window for the current SSH session.
* @param {number} rows - The number of rows for the terminal.
* @param {number} cols - The number of columns for the terminal.
*/
resizeTerminal(rows, cols) {
if (this.stream) {
this.stream.setWindow(rows, cols)
}
}
/**
* Ends the SSH connection and stream.
*/
end() {
if (this.stream) {
this.stream.end()
this.stream = null
}
if (this.conn) {
this.conn.end()
this.conn = null
}
}
}
module.exports = SSHConnection

193
app/utils.js Normal file
View file

@ -0,0 +1,193 @@
// server
// /app/utils.js
const validator = require("validator")
const Ajv = require("ajv")
const maskObject = require("jsmasker")
const { createNamespacedDebug } = require("./logger")
const { DEFAULTS, MESSAGES } = require("./constants")
const configSchema = require("./configSchema")
const debug = createNamespacedDebug("utils")
/**
* Deep merges two objects
* @param {Object} target - The target object to merge into
* @param {Object} source - The source object to merge from
* @returns {Object} The merged object
*/
function deepMerge(target, source) {
const output = Object.assign({}, target) // Avoid mutating target directly
Object.keys(source).forEach(key => {
if (Object.hasOwnProperty.call(source, key)) {
if (
source[key] instanceof Object &&
!Array.isArray(source[key]) &&
source[key] !== null
) {
output[key] = deepMerge(output[key] || {}, source[key])
} else {
output[key] = source[key]
}
}
})
return output
}
/**
* Determines if a given host is an IP address or a hostname.
* If it's a hostname, it escapes it for safety.
*
* @param {string} host - The host string to validate and escape.
* @returns {string} - The original IP or escaped hostname.
*/
function getValidatedHost(host) {
let validatedHost
if (validator.isIP(host)) {
validatedHost = host
} else {
validatedHost = validator.escape(host)
}
return validatedHost
}
/**
* Validates and sanitizes a port value.
* If no port is provided, defaults to port 22.
* If a port is provided, checks if it is a valid port number (1-65535).
* If the port is invalid, defaults to port 22.
*
* @param {string} [portInput] - The port string to validate and parse.
* @returns {number} - The validated port number.
*/
function getValidatedPort(portInput) {
const defaultPort = DEFAULTS.SSH_PORT
const port = defaultPort
debug("getValidatedPort: input: %O", portInput)
if (portInput) {
if (validator.isInt(portInput, { min: 1, max: 65535 })) {
return parseInt(portInput, 10)
}
}
debug(
"getValidatedPort: port not specified or is invalid, setting port to: %O",
port
)
return port
}
/**
* Checks if the provided credentials object is valid.
*
* @param {Object} creds - The credentials object.
* @param {string} creds.username - The username.
* @param {string} creds.password - The password.
* @param {string} creds.host - The host.
* @param {number} creds.port - The port.
* @returns {boolean} - Returns true if the credentials are valid, otherwise false.
*/
function isValidCredentials(creds) {
return !!(
creds &&
typeof creds.username === "string" &&
typeof creds.password === "string" &&
typeof creds.host === "string" &&
typeof creds.port === "number"
)
}
/**
* Validates and sanitizes the SSH terminal name using validator functions.
* Allows alphanumeric characters, hyphens, and periods.
* Returns null if the terminal name is invalid or not provided.
*
* @param {string} [term] - The terminal name to validate.
* @returns {string|null} - The sanitized terminal name if valid, null otherwise.
*/
function validateSshTerm(term) {
debug(`validateSshTerm: %O`, term)
if (!term) {
return null
}
const validatedSshTerm =
validator.isLength(term, { min: 1, max: 30 }) &&
validator.matches(term, /^[a-zA-Z0-9.-]+$/)
return validatedSshTerm ? term : null
}
/**
* Validates the given configuration object.
*
* @param {Object} config - The configuration object to validate.
* @throws {Error} If the configuration object fails validation.
* @returns {Object} The validated configuration object.
*/
function validateConfig(config) {
const ajv = new Ajv()
const validate = ajv.compile(configSchema)
const valid = validate(config)
if (!valid) {
throw new Error(
`${MESSAGES.CONFIG_VALIDATION_ERROR}: ${ajv.errorsText(validate.errors)}`
)
}
return config
}
/**
* Modify the HTML content by replacing certain placeholders with dynamic values.
* @param {string} html - The original HTML content.
* @param {Object} config - The configuration object to inject into the HTML.
* @returns {string} - The modified HTML content.
*/
function modifyHtml(html, config) {
debug("modifyHtml")
const modifiedHtml = html.replace(
/(src|href)="(?!http|\/\/)/g,
'$1="/ssh/assets/'
)
return modifiedHtml.replace(
"window.webssh2Config = null;",
`window.webssh2Config = ${JSON.stringify(config)};`
)
}
/**
* Masks sensitive information in an object
* @param {Object} obj - The object to mask
* @param {Object} [options] - Optional configuration for masking
* @param {string[]} [options.properties=['password', 'key', 'secret', 'token']] - The properties to be masked
* @param {number} [options.maskLength=8] - The length of the generated mask
* @param {number} [options.minLength=5] - The minimum length of the generated mask
* @param {number} [options.maxLength=15] - The maximum length of the generated mask
* @param {string} [options.maskChar='*'] - The character used for masking
* @param {boolean} [options.fullMask=false] - Whether to use a full mask for all properties
* @returns {Object} The masked object
*/
function maskSensitiveData(obj, options) {
const defaultOptions = {}
debug("maskSensitiveData")
const maskingOptions = Object.assign({}, defaultOptions, options || {})
const maskedObject = maskObject(obj, maskingOptions)
return maskedObject
}
module.exports = {
deepMerge,
getValidatedHost,
getValidatedPort,
isValidCredentials,
maskSensitiveData,
modifyHtml,
validateConfig,
validateSshTerm
}

View file

@ -3,35 +3,35 @@
"ip": "0.0.0.0",
"port": 2222
},
"http": {
"origins": ["*.*"]
},
"user": {
"name": null,
"password": null
"password": null,
"privatekey": null
},
"ssh": {
"host": null,
"port": 22,
"localAddress": null,
"localPort": null,
"term": "xterm-color",
"readyTimeout": 20000,
"keepaliveInterval": 120000,
"keepaliveCountMax": 10
},
"terminal": {
"cursorBlink": true,
"scrollback": 10000,
"tabStopWidth": 8,
"bellStyle": "sound"
"keepaliveCountMax": 10,
"allowedSubnets": []
},
"header": {
"text": null,
"background": "green"
},
"session": {
"name": "WebSSH2",
"secret": "mysecret"
},
"options": {
"challengeButton": true,
"allowreauth": true
"autoLog": false,
"allowReauth": true,
"allowReconnect": true,
"allowReplay": true
},
"algorithms": {
"kex": [
@ -61,11 +61,5 @@
"zlib@openssh.com",
"zlib"
]
},
"serverlog": {
"client": false,
"server": false
},
"accesslog": false,
"verify": false
}
}

BIN
images/Screenshot.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1,019 KiB

BIN
images/orthrus.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 103 KiB

BIN
images/orthrus.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 150 KiB

BIN
images/orthrus2.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 118 KiB

43
images/orthrus2.txt Normal file
View file

@ -0,0 +1,43 @@
                                                                                  
                       .     ..                                                   
    .:rsXXr;,.       .sAsrirsA2:        .,;irri;,.                                
  .i5352XA535X;......,525555225r.......;rsrri;irss:                               
 .s3Ar;,,,isX23333552;A22AAAA2A;;rsXA235Xr;,.,,::is:                              
 ;2sii,:r2MS&BH3X2355irAAAAAAXs:;iii;iXM@BS3X;.,::;r,                             
.isr;..rAAS9#352rrisAA:sXXXXsr;:i;:::;ii39#3si,..:;i:        .,:;ii;,.            
.,,.  ,rr.X9H55rs..:sXX:isiri::;;r..i;iiX#H,;i:. ..,.      ,s3G##3s;,.            
      ,r2.s9H5Ar2..iMr5X;,.,,:i;A3:.XA;i2#h.rX,.      ., .rG9#Shi.                
  ..,:iXh5G9#h2sh2AH5X22Asi;iii;sGM3HX;XS993h2r;:,.   r3,XBSGG3X.                 
 .;iiiiirMB9##H5255AX2222Ariiiii;isXss5S##9Msrrrii;,. XSrGSGG522;                 
 ::.....,3#SSSSSGMM32222AAsiiiiiis23MS#SS##5,.....,r. ;SMGGGM2222s:.  .           
 :MX;;s2HSGGGGGSMX3GM2AAAA2ri;;i5G3XhSSGGGGSM2si;s3X. .XGGGGH5AAA22X;.;r.         
 .ih3s2hMHHHHMh2A3Mhh5AA25h5XXXA33h3A2hHGGGHMh5s5h2,   .i5hMHM2AAAA22Arhr.        
   :riAXXXXXXXA222XsrsA25MGH522XssXA2AXXAAXXXXAXii.     .;rX2552XXXXXAA55,..      
     ,;sXXXssr;:,,,:rA25hGGGH522X;,.,:;irsXXXsr:.       .;rrsXXAAXrssssA2;s;      
       ......,:,;rX2525hGGGGGH5222Asi,,i......             .....:XsrsrrsAr2;      
            .X5AA52225hHHHHHHGH52225AXAhr.                      .rsrrrrXAAA.      
            ih2522225MHHHHHHHHGH322222225,                     ,isrriirAAA:       
           ,52222253MHHHHHHHHHHHH352225iX;,....,,::;;;;;:,..:;rsriiiirAAX:.       
           :r55255hHHHHHHHGHHHHHHM3222s;iXAA22255555555MSH5AriiiiiirXAXrsr.       
            ;rA53MHHHHMMHG55HHMhhhAXXssXXXssXAA222AAA2hGSSS#G2isXAAAAXsi:.        
            .,i235HHMMhM3ssH32X2hhAsXA2AXXXssssXXXXXAhGGh2AA5HMrsAXXXr,           
            .X5ssr5MMM5AAXsXX2hHH5223hhh2XsssssssssX3HH2rrisXAhH;,.....           
            :SGM332A3hhhMM3hHGGGh22hM2A5hXssssssssX3HHMrrrAhriiA5,                
            i#HHHMMh3hMMMMh3HHHG322M3AX53XrssssssX23HHMsri2MsiiiXr.               
            iSHH55MMM33MMM2XGHHHhAA5h333AXisssXXA2AAMMH2iir232XrrA.               
            :GMMAX2335X5552i3HHHH32AAAAXXArAAA222AAshMMM2riirrirX5,               
            .AHM3sir222AAA2XihMMMHM355552ssXA2222AArsHMMMh2AXX2355:               
             ,hMh5XrisA2AA22Xi3MMMMMMMM3AsXrr2Xr;:,,,2MMMMMMMMMh55i               
              sHMh22s:,:::irsrihMMMMMhh32Asi:,....:;.,Ahhhhhhhh35A2,              
              :MMMh2AXi;:;,....XMMMMMhh3AAXi...,,:r;  .r23333335AAAAi,..          
              .5Mhh3AAAAXr,    ;MMMMMMh52X;;;;iirrXX,. .,iX2222AAAAA222:          
              .XMhhh5AA2i;.    ,hMMMMMh52s,isXXXXXXAAX:  ..,;rAAAAAAAA2,          
              .XMhhh5AA2:      .5Mhhhhh22r..,:sXXXXXX2;      .sAAAAAAAX.          
              .XMhhh5AA2,      .5hhhhh322i    ;XXXXXXA,      .s2AAAAAAr.          
              .Ahhhh2AAX.      .5hhhhh5A2:   .iXXXXXAs.      .A5552AA2;           
              :hhh332A2r.      :hhhhh32A2,   ,XXXXXXA;       :333332A2:           
         .,;:,s55335AA2:  .,,;;rA253h5AAA,,,,iXXXXXXA,  ..,;:r22535AAX.           
       .iXG55HM3225AAAX. :5XM#25Mh525AAArrrsssssXXXAi. ,sXHhAhh5A22XAi.           
       :sA5i22222AAAAX: .r5i55i55222AAAX;siXXXXAAAAr.  ;Xs5rs522AAAAr.            
       .,,;::;;;;;;;:.  ..:::;,:;;;;;;:,::::;;;;;;,.   .:,;:,;;;;;;:.             
                                                                                  

BIN
images/orthrus2.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 219 KiB

BIN
images/orthrus2a.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 81 KiB

27
index.js Normal file
View file

@ -0,0 +1,27 @@
#!/usr/bin/env node
// server
// index.js
/**
* index.js
*
* WebSSH2 - Web to SSH2 gateway
* Bill Church - https://github.com/billchurch/WebSSH2 - May 2017
*/
const { initializeServer } = require("./app/app")
/**
* Main function to start the application
*/
function main() {
initializeServer()
}
// Run the application
main()
// For testing purposes, export the functions
module.exports = {
initializeServer
}

7
jsconfig.json Normal file
View file

@ -0,0 +1,7 @@
{
"compilerOptions": {
"module": "CommonJS",
"target": "ES6"
},
"exclude": ["node_modules"]
}

9724
package-lock.json generated Normal file

File diff suppressed because it is too large Load diff

95
package.json Normal file
View file

@ -0,0 +1,95 @@
{
"name": "webssh2-server",
"version": "0.2.20",
"ignore": [
".gitignore"
],
"bin": {
"webssh2-server": "./index.js"
},
"description": "A Websocket to SSH2 gateway using xterm.js, socket.io, ssh2",
"homepage": "https://github.com/billchurch/WebSSH2",
"keywords": [
"ssh",
"webssh",
"terminal",
"webterminal"
],
"license": "SEE LICENSE IN FILE - LICENSE",
"private": false,
"repository": {
"type": "git",
"url": "git+https://github.com/billchurch/WebSSH2.git"
},
"contributors": [
"Bill Church <wmchurch@gmail.com>"
],
"engines": {
"node": ">= 6"
},
"bugs": {
"url": "https://github.com/billchurch/WebSSH2/issues"
},
"dependencies": {
"ajv": "^8.17.1",
"basic-auth": "^2.0.1",
"body-parser": "^1.20.3",
"debug": "^4.3.7",
"express": "^4.21.1",
"express-session": "^1.18.1",
"express-socket.io-session": "^1.3.5",
"jsmasker": "^1.2.0",
"read-config-ng": "~3.0.7",
"socket.io": "~4.8.0",
"ssh2": "~1.16.0",
"validator": "^13.12.0",
"webssh2_client": "^1.0.0"
},
"scripts": {
"start": "node index.js",
"lint": "eslint app",
"lint:fix": "eslint app --fix",
"watch": "NODE_ENV=development DEBUG=webssh* nodemon index.js -w app/ -w index.js -w config.json -w package.json",
"test": "jest",
"release": "standard-version -a -s --release-as patch --commit-all",
"release:dry-run": "standard-version -a -s --release-as patch --dry-run",
"publish:dry-run": "npm publish --dry-run",
"publish:npm": "npm publish",
"pretest": "npm run lint",
"ci": "npm run test",
"release:major": "npm run release -- --release-as major",
"release:minor": "npm run release -- --release-as minor",
"release:patch": "npm run release -- --release-as patch"
},
"jest": {
"testEnvironment": "node",
"testMatch": [
"**/tests/**/*.test.js"
]
},
"standard": {
"ignore": [
"bin/*",
"build/*"
]
},
"devDependencies": {
"eslint": "^8.57.1",
"eslint-config-airbnb-base": "^15.0.0",
"eslint-config-prettier": "^9.1.0",
"eslint-plugin-import": "^2.31.0",
"eslint-plugin-jest": "^28.8.3",
"eslint-plugin-node": "^11.1.0",
"eslint-plugin-prettier": "^5.2.1",
"jest": "^29.7.0",
"nodemon": "^3.1.7",
"prettier": "^3.3.3",
"prettier-eslint": "^16.3.0",
"standard-version": "^9.5.0"
},
"main": "index.js",
"directories": {
"test": "tests"
},
"author": ""
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 129 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 131 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 186 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 187 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 130 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.5 MiB

View file

@ -1,42 +0,0 @@
#!/bin/bash
## Syncs from BIG-IP and builds a release based on version in extensions/ephemeral_auth/package.json
#
source ./scripts/env.sh
source ./scripts/util.sh
./scripts/pull.sh
if [ $? -ne 0 ]; then
# failure
tput bel;tput bel;tput bel;tput bel
echo -e "\n${fgLtRed}Pull command failed. Giving up.${fgLtWhi}\n"
echo ${output}
exit 255
fi
# get version of package from package.json
package_version=$(jq -r ".version" workspace/extensions/webssh2/package.json)
# creates new workspace name with version
webssh_workspace_name=$webssh_workspace_name-$package_version
echoNotice "Creating workspace package"
runCommand "ssh -o ClearAllForwardings=yes $webssh_ilxhost /bin/tar --exclude='./extensions/webssh2/config.json' -czf - -C /var/ilx/workspaces/Common/$webssh_workspace_name . > Build/Release/$webssh_package_name-$package_version.tgz"
echoNotice "Creating SHA256 hash"
runCommand "shasum -a 256 Build/Release/$webssh_package_name-$package_version.tgz > Build/Release/$webssh_package_name-$package_version.tgz.sha256"
echoNotice "Copying to current"
runCommand "cp Build/Release/$webssh_package_name-$package_version.tgz $webssh_pua_location/$webssh_package_name-current.tgz && \
cp Build/Release/$webssh_package_name-$package_version.tgz.sha256 $webssh_pua_location/$webssh_package_name-current.tgz.sha256"
echoNotice "Deleting any '.DS_Store' files"
runCommand "find . -name '.DS_Store' -type f -delete"
echo -e "\nWorkspace packages located at:\n"
echo " Build/Release/$webssh_package_name-$package_version.tgz"
echo " Build/Release/$webssh_package_name-$package_version.tgz.sha256"
echo " $webssh_pua_location/$webssh_package_name-current.tgz"
echo " $webssh_pua_location/$webssh_package_name-current.tgz.sha256"
echo -e "\n👍 Build Complete 👍\n"
exit 0

View file

@ -1,6 +0,0 @@
#!/bin/sh
#webssh_ilxhost=root@192.168.30.209
webssh_ilxhost=root@192.168.30.203
webssh_workspace_name=webssh2
webssh_package_name=BIG-IP-ILX-WebSSH2
webssh_pua_location=./bin

View file

@ -1,30 +0,0 @@
#!/bin/bash
#
# ./scripts/pull.sh
#
# bill@f5.com
#
# Pulls an ILX workspace from a BIG-IP and syncs to ./workspace, excludes
# ./workspace/extensions/ephemeral_auth/node_modules.
#
source ./scripts/env.sh
source ./scripts/util.sh
# get version of package from package.json
PACKAGE_VERSION=$(jq -r ".version" workspace/extensions/webssh2/package.json 2>&1)
# creates new workspace name with version
webssh_workspace_name=$webssh_workspace_name-$PACKAGE_VERSION
echo "Pull ${fgLtCya}$webssh_workspace_name${fgLtWhi} from ${fgLtCya}$webssh_ilxhost${fgLtWhi}"
# check to see if the workspace actually exists before attempting to copy over
echoNotice "Checking for existing workspace ${fgLtCya}$webssh_workspace_name${fgLtWhi}"
runCommand "ssh -o ClearAllForwardings=yes $webssh_ilxhost tmsh list ilx workspace $webssh_workspace_name one-line 2>&1"
echoNotice "Pulling ${fgLtCya}$webssh_workspace_name${fgLtWhi} from ${fgLtCya}$webssh_ilxhost${fgLtWhi}"
runCommand "rsync -e 'ssh -o ClearAllForwardings=yes -ax' -avq --include=\"extensions/ephemeral_auth/node_modules/f5-*\" --exclude=\".DS_Store\" --exclude=\"extensions/ephemeral_auth/node_modules/*\" $webssh_ilxhost:/var/ilx/workspaces/Common/$webssh_workspace_name/. workspace/. 2>&1"
echo -e "\n👍 Pull complete 👍\n"
exit 0

View file

@ -1,61 +0,0 @@
#!/bin/bash
#
# ./scripts/push.sh
#
# bill@f5.com
#
# Pushes ./workspace to a BIG-IP ILX workspace
#
source ./scripts/env.sh
source ./scripts/util.sh
# get version of package from package.json
PACKAGE_VERSION=$(jq -r ".version" workspace/extensions/webssh2/package.json 2>&1)
# creates new workspace name with version
webssh_workspace_name=$webssh_workspace_name-$PACKAGE_VERSION
echo "Push ${fgLtCya}$webssh_workspace_name${fgLtWhi} to ${fgLtCya}$webssh_ilxhost${fgLtWhi}"
echoNotice "Checking $webssh_ilxhost for workspace $webssh_workspace_name"
output=$(ssh -o ClearAllForwardings=yes $webssh_ilxhost tmsh list ilx workspace $webssh_workspace_name one-line 2>&1)
result="$?" 2>&1
if [ $result -ne 0 ]; then
echo "❌"
echoNotice "Attempting to create workspace"
runCommand "ssh -o ClearAllForwardings=yes $webssh_ilxhost \"tmsh create ilx workspace $webssh_workspace_name node-version 6.9.1\" 2>&1"
else
echo "✅"
fi
echoNotice "Pushing ./workspace to $webssh_ilxhost at $webssh_workspace_name"
runCommand "rsync -e 'ssh -o ClearAllForwardings=yes -ax' -avq --delete --exclude='.DS_Store' --exclude extensions/webssh2/node_modules workspace/. $webssh_ilxhost:/var/ilx/workspaces/Common/$webssh_workspace_name/."
echoNotice "Installing node modules at $webssh_workspace_name on $webssh_ilxhost"
runCommand "ssh -o ClearAllForwardings=yes $webssh_ilxhost \"cd /var/ilx/workspaces/Common/$webssh_workspace_name/extensions/webssh2; npm i --production\" 2>&1"
echoNotice "Setting permissions at $webssh_workspace_name on $webssh_ilxhost"
runCommand "ssh -o ClearAllForwardings=yes $webssh_ilxhost \"chown -R root.sdm /var/ilx/workspaces/Common/$webssh_workspace_name/; \
chmod -R ug+rwX,o-w /var/ilx/workspaces/Common/$webssh_workspace_name/; \
chmod u+rw,go-w /var/ilx/workspaces/Common/$webssh_workspace_name/version; \
chmod u+rw,go-w /var/ilx/workspaces/Common/$webssh_workspace_name/node_version\" 2>&1"
echoNotice "Deleting $webssh_workspace_name/node_modules/.bin on $webssh_ilxhost"
runCommand "ssh -o ClearAllForwardings=yes $webssh_ilxhost \"cd /var/ilx/workspaces/Common/$webssh_workspace_name/extensions/webssh2; rm -rf node_modules/.bin\" 2>&1"
# switch plugin to new workspace
echoNotice "Checking to see if plugin exists"
output=$(ssh -o ClearAllForwardings=yes $webssh_ilxhost tmsh list ilx plugin WebSSH_plugin one-line 2>&1)
result="$?" 2>&1
if [ $result -ne 0 ]; then
echo "❌"
echoNotice "Attempting to create plugin"
runCommand "ssh -o ClearAllForwardings=yes $webssh_ilxhost tmsh create ilx plugin WebSSH_plugin from-workspace $webssh_workspace_name extensions { webssh2 { concurrency-mode single ilx-logging enabled } } 2>&1"
else
echo "✅"
echoNotice "Switching plugin to $webssh_workspace_name"
runCommand "ssh -o ClearAllForwardings=yes $webssh_ilxhost tmsh modify ilx plugin WebSSH_plugin from-workspace $webssh_workspace_name extensions { webssh2 { concurrency-mode single ilx-logging enabled } } 2>&1"
fi
echo -e "\n👍 Push complete 👍\n"
exit 0

View file

@ -1,74 +0,0 @@
#!/bin/bash
# Utility functions / scripts
echoNotice () { echo -e -n "\n$@... "; }
fgLtRed=$(tput bold;tput setaf 1)
fgLtGrn=$(tput bold;tput setaf 2)
fgLtYel=$(tput bold;tput setaf 3)
fgLtBlu=$(tput bold;tput setaf 4)
fgLtMag=$(tput bold;tput setaf 5)
fgLtCya=$(tput bold;tput setaf 6)
fgLtWhi=$(tput bold;tput setaf 7)
fgLtGry=$(tput bold;tput setaf 8)
echo ${fgLtWhi}
# check for jq and try to install...
output=$(which jq 2>&1)
if [[ $? -ne 0 ]]; then
echo -e "You need to install jq: https://stedolan.github.io/jq\n"
echo -e "If you have *brew* you can install with:\n"
echo -e " brew install jq\n"
echo -n "Do you want me to try and install that for you (Y/n)? "
read -n1 yesno
echo
if [[ ("$yesno" != "y") ]]; then
echo -e "\nUnable to continue, install jq first.\n\n"
exit 255
else
which brew
if [[ $? -ne 0 ]]; then
echo -e "\nYou're a mess... You don't even have brew installed...\nMaybe you should check it out\n"
echo -e " https://brew.sh/\n\n"
exit 255
fi
echo
brew install jq
if [[ $? -ne 0 ]]; then
echo -e "\nLooks like that failed, I can't do everything... Quitting, install jq...\n"
exit 255
fi
fi
fi
# checks the output of a command to get the status and report/handle failure
checkOutput() {
if [ $result -eq 0 ]; then
# success
#echo "${fgLtGrn}[OK]${fgLtWhi}"
echo "✅"
return
else
# failure
tput bel;tput bel;tput bel;tput bel
#echo "${fgLtRed}[FAILED]${fgLtWhi}"
echo "❌"
echo -e "\nPrevious command failed in ${script_path}/${scriptname} with error level: ${result}"
echo -e "\nCommand:\n"
echo " ${command}"
echo -e "\nSTDOUT/STDERR:\n"
echo ${output}
exit 255
fi
}
# run a comand and check call checkOutput
runCommand() {
# $1 command
command=$@
output=$((eval $command) 2>&1)
result="$?" 2>&1
prevline=$(($LINENO-2))
checkOutput
}

View file

@ -1,29 +0,0 @@
#!/bin/bash
## displays and optionally changes version of product
source ./scripts/env.sh
source ./scripts/util.sh
echo
# get current version of workspace, ask to change or rebuild
webssh_ilx_ver=$(jq -r ".version" ./workspace/extensions/webssh2/package.json 2>&1)
if [[ $? -ne 0 ]]; then exit; echo "error reading ILX irule version";fi
echo "Current version of $webssh_workspace_name is: $webssh_ilx_ver"
echo -n "If you want to change this version, enter it now otherwise press enter to retain: "
read newver
echo
if [[ ("$newver" != "") ]]; then
echo "Updating version of ILX to: $newver"
export newver
jq --arg newver "$newver" '.version = $newver' < ./workspace/extensions/webssh2/package.json > ./workspace/extensions/webssh2/package.json.new
if [[ $? -ne 0 ]]; then exit; echo "error changing version - ilx";fi
mv ./workspace/extensions/webssh2/package.json.new ./workspace/extensions/webssh2/package.json
else
echo "No changes made"
fi

View file

@ -0,0 +1,11 @@
// server
// tests/crypto-utils.test.js
const { generateSecureSecret } = require("../app/crypto-utils")
describe("generateSecureSecret", () => {
it("should generate a 64-character hex string", () => {
const secret = generateSecureSecret()
expect(secret).toMatch(/^[0-9a-f]{64}$/)
})
})

88
tests/errors.test.js Normal file
View file

@ -0,0 +1,88 @@
const {
WebSSH2Error,
ConfigError,
SSHConnectionError,
handleError
} = require("../app/errors")
const { logError } = require("../app/logger")
const { HTTP, MESSAGES } = require("../app/constants")
jest.mock("../app/logger", () => ({
logError: jest.fn(),
createNamespacedDebug: jest.fn(() => jest.fn())
}))
describe("errors", () => {
afterEach(() => {
jest.clearAllMocks()
})
describe("WebSSH2Error", () => {
it("should create a WebSSH2Error with correct properties", () => {
const error = new WebSSH2Error("Test error", "TEST_CODE")
expect(error).toBeInstanceOf(Error)
expect(error.name).toBe("WebSSH2Error")
expect(error.message).toBe("Test error")
expect(error.code).toBe("TEST_CODE")
})
})
describe("ConfigError", () => {
it("should create a ConfigError with correct properties", () => {
const error = new ConfigError("Config error")
expect(error).toBeInstanceOf(WebSSH2Error)
expect(error.name).toBe("ConfigError")
expect(error.message).toBe("Config error")
expect(error.code).toBe(MESSAGES.CONFIG_ERROR)
})
})
describe("SSHConnectionError", () => {
it("should create a SSHConnectionError with correct properties", () => {
const error = new SSHConnectionError("SSH connection error")
expect(error).toBeInstanceOf(WebSSH2Error)
expect(error.name).toBe("SSHConnectionError")
expect(error.message).toBe("SSH connection error")
expect(error.code).toBe(MESSAGES.SSH_CONNECTION_ERROR)
})
})
describe("handleError", () => {
const mockRes = {
status: jest.fn(() => mockRes),
json: jest.fn()
}
it("should handle WebSSH2Error correctly", () => {
const error = new WebSSH2Error("Test error", "TEST_CODE")
handleError(error, mockRes)
expect(logError).toHaveBeenCalledWith("Test error", error)
expect(mockRes.status).toHaveBeenCalledWith(HTTP.INTERNAL_SERVER_ERROR)
expect(mockRes.json).toHaveBeenCalledWith({
error: "Test error",
code: "TEST_CODE"
})
})
it("should handle generic Error correctly", () => {
const error = new Error("Generic error")
handleError(error, mockRes)
expect(logError).toHaveBeenCalledWith(MESSAGES.UNEXPECTED_ERROR, error)
expect(mockRes.status).toHaveBeenCalledWith(HTTP.INTERNAL_SERVER_ERROR)
expect(mockRes.json).toHaveBeenCalledWith({
error: MESSAGES.UNEXPECTED_ERROR
})
})
it("should not send response if res is not provided", () => {
const error = new Error("No response error")
handleError(error)
expect(logError).toHaveBeenCalledWith(MESSAGES.UNEXPECTED_ERROR, error)
expect(mockRes.status).not.toHaveBeenCalled()
expect(mockRes.json).not.toHaveBeenCalled()
})
})
})

48
tests/logger.test.js Normal file
View file

@ -0,0 +1,48 @@
// server
// tests/logger.test.js
const createDebug = require("debug")
const { createNamespacedDebug, logError } = require("../app/logger")
jest.mock("debug")
describe("logger", () => {
beforeEach(() => {
jest.clearAllMocks()
console.error = jest.fn()
})
describe("createNamespacedDebug", () => {
it("should create a debug function with the correct namespace", () => {
const mockDebug = jest.fn()
createDebug.mockReturnValue(mockDebug)
const result = createNamespacedDebug("test")
expect(createDebug).toHaveBeenCalledWith("webssh2:test")
expect(result).toBe(mockDebug)
})
})
describe("logError", () => {
it("should log an error message without an error object", () => {
const message = "Test error message"
logError(message)
expect(console.error).toHaveBeenCalledWith(message)
expect(console.error).toHaveBeenCalledTimes(1)
})
it("should log an error message with an error object", () => {
const message = "Test error message"
const error = new Error("Test error")
logError(message, error)
expect(console.error).toHaveBeenCalledWith(message)
expect(console.error).toHaveBeenCalledWith("ERROR: Error: Test error")
expect(console.error).toHaveBeenCalledTimes(2)
})
})
})

View file

@ -0,0 +1,31 @@
# Use the Debian Bullseye Slim image as the base
FROM debian:bullseye-slim
# Install the necessary packages
RUN apt-get update && \
apt-get install -y --no-install-recommends \
openssh-server htop mc sudo bash bash-completion readline-common && \
apt-get clean && \
rm -rf /var/lib/apt/lists/*
# Configure SSH server
RUN mkdir /var/run/sshd && \
sed -i 's/^ChallengeResponseAuthentication no/ChallengeResponseAuthentication yes/' /etc/ssh/sshd_config && \
echo 'PermitRootLogin yes' >> /etc/ssh/sshd_config && \
echo 'Port 4444' >> /etc/ssh/sshd_config && \
echo 'UsePAM yes' >> /etc/ssh/sshd_config && \
echo 'AuthenticationMethods keyboard-interactive' >> /etc/ssh/sshd_config
COPY --chmod=755 orthrus2.sh /etc/profile.d/ascii-art.sh
# Add a test user with a password
RUN useradd -m -s /bin/bash testuser && \
echo "testuser:testpassword" | chpasswd
COPY --chown=testuser:testuser --chmod=755 color-test.sh /home/testuser/color-test.sh
# Expose port 4444
EXPOSE 4444
# Start the SSH server
CMD ["/usr/sbin/sshd", "-D", "-e"]

View file

@ -0,0 +1,58 @@
# Keyboard Interactive SSH Server
A test SSH server that uses keyboard-interactive authentication and listens on port 4444:
```Dockerfile
# Use the Debian Bullseye Slim image as the base
FROM debian:bullseye-slim
# Install the necessary packages
# Use the Debian Bullseye Slim image as the base
FROM debian:bullseye-slim
# Install the necessary packages
RUN apt-get update && \
apt-get install htop -y --no-install-recommends \
openssh-server && \
apt-get clean && \
rm -rf /var/lib/apt/lists/*
# Configure SSH server
RUN mkdir /var/run/sshd && \
sed -i 's/^ChallengeResponseAuthentication no/ChallengeResponseAuthentication yes/' /etc/ssh/sshd_config && \
echo 'PermitRootLogin yes' >> /etc/ssh/sshd_config && \
echo 'Port 4444' >> /etc/ssh/sshd_config && \
echo 'UsePAM yes' >> /etc/ssh/sshd_config && \
echo 'AuthenticationMethods keyboard-interactive' >> /etc/ssh/sshd_config
# Add a test user with a password
RUN useradd -m testuser && \
echo "testuser:testpassword" | chpasswd
# Expose port 4444
EXPOSE 4444
# Start the SSH server
CMD ["/usr/sbin/sshd", "-D", "-e"]
```
### Instructions:
1. **Build the Docker image**:
```bash
docker build -t keyboard-ssh-server .
```
2. **Run the container**:
```bash
docker run --rm -p 4444:4444 --name keyboard-ssh-server keyboard-ssh-server
```
This Dockerfile sets up an SSH server that listens on port 4444 and uses keyboard-interactive authentication. The `testuser` has been created with the password `testpassword`.
You can connect to this SSH server using the following command:
```bash
ssh -p 4444 testuser@localhost
```
You'll be prompted for a password as part of the keyboard-interactive authentication process.

View file

@ -0,0 +1,28 @@
#!/bin/bash
# Print 256-color test pattern in xterm-256color terminal
echo -e "\n256 Color Test Pattern\n"
# Colors 0-15: Standard colors
echo -e "Standard colors:"
for i in {0..15}; do
printf "\e[48;5;${i}m%4s\e[0m" "$i"
done
echo -e "\n"
# Colors 16-231: Color cube
echo -e "Color cube:"
for i in {16..231}; do
printf "\e[48;5;${i}m%4s\e[0m" "$i"
if (( (i - 15) % 6 == 0 )); then
echo
fi
done
echo -e "\n"
# Colors 232-255: Grayscale
echo -e "Grayscale colors:"
for i in {232..255}; do
printf "\e[48;5;${i}m%4s\e[0m" "$i"
done
echo -e "\n"

View file

@ -0,0 +1,47 @@
#!/bin/bash
echo -e "\e[31m"
cat << 'EOF'
                                                                              
                     ..    ...            ..                                  
   .:iXAAXr;,.      .A2XsrsA2X.       .:irsrr;,.                              
  ,X35AssA552r:,,,,,:522555222,,,,,,,;rrii;;irss,                             
 .22si,,,:sA5MMMh335;A22AAA2As:rrsX2hM5Xi,.,,::is,                            
.iArr:.i5S9@BM3sA555riAAAAAXs;:iii;;r2B&BGA;.,;;i;.                           
.ir;, :Xrs99h52s;,iX2;rXXXsr;:i;:,:;ii29Ssii,..,:;.      .:isA2Xi,.           
...   ;s;.H#55rX,.:AsA;;;:;::;;A, ;s;iX#3.rr,.  ...    .i3S9#3r,..            
     .;AX;G#h2r3i,XHs5Ai:,:;iii#AiA3;i3#M:2r,.     .r.,5B#SHA.                
 .,;irsAMB99ShA2hh3XA222Xiiiii;s3h5sr599B#3AXri;,  ;9iA9SSM5X.                
 ,:,,,,.;S####Gh35222222Ariiiii;rXX5G###9X,,,,,,:, :S5MSGH2A2s,               
 ;X,.,:r3SSSSSSS3hH3AAAAAX;ii;r5h5H#SSSSSMs:,.,:2: .XSGGGH22A22s:..:.         
 ,5MAAMSSGGGGGM5AhHM5AAA23Xrrr2Mh5AhGSSSSSSH3s5M2.  .XMHHHh2AAA22X;AX.        
  .sXrXXA2222AA2352AAA25hG322AAA255AAA2522AXXrXs.    .;X2335AXXXXAA2h; .      
   ..;XAAAXXXsri:::iX25hGGG322X;:::irrsXXAAAX;..     .irsXA22AsssssX2s:i.     
      ..,,,,.,.,;rA552hGGGGG3222Xi:..:,.,,,..         ..,,,,:rAsssssAsss.     
            :2XX25225hHHHHHGG322552sX3i                      .XsrrrsAA2;      
           .55552225MHHHHHHHGHh52225255.                    .isrrrrXA2s.      
          .s5252253MHHHHHHHHHHHh52225rX:,.....,,::;;:,...,:isriiiis2Ar.       
          .iA5555hHHHHHHHGHHHHHHh522X;isAAA222555555hGH5AriiiiiisAAXrs:.      
           .sX53MHHHHMHGH2hGHhhh5XXssXXXssXA2222AAA3GSSS#G2isXAAAAXr;,.       
           ..;X32HHHMhM5i2M5AA3h2XXA2AXXXssssXXXXA5GGh2AA2HMrsXXXsi.          
            iMXss2MMM5A2sXXA3HGh223Mhh5XXsssssssX2HG3rrrsXA3M;......          
           .5#Hhh5A3hhMHMhMGGGH223H2A2hXssssssss2HHHXirXhriiA2.               
           .HGHHMHMhhMMMH5hHHGM223M2A55XrsssssXA5MHHAiiXHArirXi               
           .MHH52MMM55hhhX2GHHH5A25335XXiXXXXA22X5HHhsirX2AsisX.              
           .AGM5sX235A252AiMHHHH52AAAAAXs2222222XXHMHhXriiirs22.              
            :MMhAriX22A2A2XrhMMMHMhhh32rssA222Asr:5MMMMh5553h52,              
             sHM32Xi;rXXXA2XrhMMMMMMM32XXirr;:..,.:3MMMMMMMhh55;              
             ,hMM32Ai,,,,,:;:rMMMMMMM32Asi.....,r. :Ahhhhhh352A2:.            
             .AMMh5AAXsri.   ,3MMMMMh32Xr;,,::;iX.  .iA555552AAA2Xr;.         
              rMhhh2AAAsi.   .AMMMMMM52s:irssssXAXi,  .:rXAAAAAAAA2s.         
              iMhhh5AA2,.     sHMMMMh52r.,;sXXXXXA2X.   ..,sAAAAAA2i.         
              iMhhh5AAA.      rMhhhhh22i   .rXXXXXAr.     .sAAAAAA2:          
              rMhhh5AAX.      rMhhhh3A2;   .rXXXXX2:      .X5522AAA,          
             .Ahhh32A2r.     .XMhhhh5A2,   :XXXXXAX.      ,33335AAX.          
         .,,.;53h35AA2:  ...,:A553h32AA,...rXXXXXAi.  ..,,i55335AAr.          
       ,;Ah23h52232AAX. :AsMMA535252AAsirrssssXXAA, .ir3MX532A5AXA:           
      .rs3XX3522AAAAX: .X2shsX35522AAX;siXXXAAAAX;. ,AshXs3522AAAi.           
      .,,;;:;;;;;;;;.  .,;:;;:ii;;;;;,:::;;;;;;;,.  .::;;:;;;;;;,.            
                                                      .                       
EOF
echo -e "\e[0m"

View file

@ -0,0 +1,41 @@
                                                                              
                     ..    ...            ..                                  
   .:iXAAXr;,.      .A2XsrsA2X.       .:irsrr;,.                              
  ,X35AssA552r:,,,,,:522555222,,,,,,,;rrii;;irss,                             
 .22si,,,:sA5MMMh335;A22AAA2As:rrsX2hM5Xi,.,,::is,                            
.iArr:.i5S9@BM3sA555riAAAAAXs;:iii;;r2B&BGA;.,;;i;.                           
.ir;, :Xrs99h52s;,iX2;rXXXsr;:i;:,:;ii29Ssii,..,:;.      .:isA2Xi,.           
...   ;s;.H#55rX,.:AsA;;;:;::;;A, ;s;iX#3.rr,.  ...    .i3S9#3r,..            
     .;AX;G#h2r3i,XHs5Ai:,:;iii#AiA3;i3#M:2r,.     .r.,5B#SHA.                
 .,;irsAMB99ShA2hh3XA222Xiiiii;s3h5sr599B#3AXri;,  ;9iA9SSM5X.                
 ,:,,,,.;S####Gh35222222Ariiiii;rXX5G###9X,,,,,,:, :S5MSGH2A2s,               
 ;X,.,:r3SSSSSSS3hH3AAAAAX;ii;r5h5H#SSSSSMs:,.,:2: .XSGGGH22A22s:..:.         
 ,5MAAMSSGGGGGM5AhHM5AAA23Xrrr2Mh5AhGSSSSSSH3s5M2.  .XMHHHh2AAA22X;AX.        
  .sXrXXA2222AA2352AAA25hG322AAA255AAA2522AXXrXs.    .;X2335AXXXXAA2h; .      
   ..;XAAAXXXsri:::iX25hGGG322X;:::irrsXXAAAX;..     .irsXA22AsssssX2s:i.     
      ..,,,,.,.,;rA552hGGGGG3222Xi:..:,.,,,..         ..,,,,:rAsssssAsss.     
            :2XX25225hHHHHHGG322552sX3i                      .XsrrrsAA2;      
           .55552225MHHHHHHHGHh52225255.                    .isrrrrXA2s.      
          .s5252253MHHHHHHHHHHHh52225rX:,.....,,::;;:,...,:isriiiis2Ar.       
          .iA5555hHHHHHHHGHHHHHHh522X;isAAA222555555hGH5AriiiiiisAAXrs:.      
           .sX53MHHHHMHGH2hGHhhh5XXssXXXssXA2222AAA3GSSS#G2isXAAAAXr;,.       
           ..;X32HHHMhM5i2M5AA3h2XXA2AXXXssssXXXXA5GGh2AA2HMrsXXXsi.          
            iMXss2MMM5A2sXXA3HGh223Mhh5XXsssssssX2HG3rrrsXA3M;......          
           .5#Hhh5A3hhMHMhMGGGH223H2A2hXssssssss2HHHXirXhriiA2.               
           .HGHHMHMhhMMMH5hHHGM223M2A55XrsssssXA5MHHAiiXHArirXi               
           .MHH52MMM55hhhX2GHHH5A25335XXiXXXXA22X5HHhsirX2AsisX.              
           .AGM5sX235A252AiMHHHH52AAAAAXs2222222XXHMHhXriiirs22.              
            :MMhAriX22A2A2XrhMMMHMhhh32rssA222Asr:5MMMMh5553h52,              
             sHM32Xi;rXXXA2XrhMMMMMMM32XXirr;:..,.:3MMMMMMMhh55;              
             ,hMM32Ai,,,,,:;:rMMMMMMM32Asi.....,r. :Ahhhhhh352A2:.            
             .AMMh5AAXsri.   ,3MMMMMh32Xr;,,::;iX.  .iA555552AAA2Xr;.         
              rMhhh2AAAsi.   .AMMMMMM52s:irssssXAXi,  .:rXAAAAAAAA2s.         
              iMhhh5AA2,.     sHMMMMh52r.,;sXXXXXA2X.   ..,sAAAAAA2i.         
              iMhhh5AAA.      rMhhhhh22i   .rXXXXXAr.     .sAAAAAA2:          
              rMhhh5AAX.      rMhhhh3A2;   .rXXXXX2:      .X5522AAA,          
             .Ahhh32A2r.     .XMhhhh5A2,   :XXXXXAX.      ,33335AAX.          
         .,,.;53h35AA2:  ...,:A553h32AA,...rXXXXXAi.  ..,,i55335AAr.          
       ,;Ah23h52232AAX. :AsMMA535252AAsirrssssXXAA, .ir3MX532A5AXA:           
      .rs3XX3522AAAAX: .X2shsX35522AAX;siXXXAAAAX;. ,AshXs3522AAAi.           
      .,,;;:;;;;;;;;.  .,;:;;:ii;;;;;,:::;;;;;;;,.  .::;;:;;;;;;,.            
                                                      .                       

95
tests/socket.test.js Normal file
View file

@ -0,0 +1,95 @@
// server
// tests/socket.test.js
const EventEmitter = require("events")
const socketHandler = require("../app/socket")
// const WebSSH2Socket = require("../app/socket")
jest.mock("../app/ssh")
describe("socketHandler", () => {
let io
let socket
let config
beforeEach(() => {
socket = new EventEmitter()
socket.id = "test-socket-id"
socket.handshake = {
session: {}
}
socket.emit = jest.fn()
io = {
on: jest.fn(function(event, callback) {
if (event === "connection") {
callback(socket)
}
})
}
config = {
ssh: {
term: "xterm-color"
},
options: {
allowreauth: true
}
}
socketHandler(io, config)
})
afterEach(() => {
jest.clearAllMocks()
})
test("should set up connection listener on io", () => {
expect(io.on).toHaveBeenCalledWith("connection", expect.any(Function))
})
test("should set up authenticate event listener on socket", () => {
expect(socket.listeners("authenticate")).toHaveLength(1)
})
test("should set up terminal event listener on socket", () => {
expect(socket.listeners("terminal")).toHaveLength(1)
})
test("should set up disconnect event listener on socket", () => {
expect(socket.listeners("disconnect")).toHaveLength(1)
})
test("should emit request_auth when not authenticated", () => {
expect(socket.emit).toHaveBeenCalledWith("authentication", {
action: "request_auth"
})
})
test("should handle authenticate event", () => {
const creds = {
username: "testuser",
password: "testpass",
host: "testhost",
port: 22
}
socket.emit("authenticate", creds)
// build out later
})
test("should handle terminal event", () => {
const terminalData = {
term: "xterm",
rows: 24,
cols: 80
}
socket.emit("terminal", terminalData)
// build out later
})
test("should handle disconnect event", () => {
const reason = "test-reason"
socket.emit("disconnect", reason)
// build out later
})
})

160
tests/ssh.test.js Normal file
View file

@ -0,0 +1,160 @@
// server
// tests/ssh.test.js
const SSH2 = require("ssh2")
const SSHConnection = require("../app/ssh")
const { SSHConnectionError } = require("../app/errors")
const { maskSensitiveData } = require("../app/utils")
jest.mock("ssh2")
jest.mock("../app/logger", () => ({
createNamespacedDebug: jest.fn(() => jest.fn()),
logError: jest.fn()
}))
jest.mock("../app/utils", () => ({
maskSensitiveData: jest.fn(data => data)
}))
jest.mock("../app/errors", () => ({
SSHConnectionError: jest.fn(function(message) {
this.message = message
}),
handleError: jest.fn()
}))
describe("SSHConnection", () => {
let sshConnection
let mockConfig
let mockSSH2Client
beforeEach(() => {
mockConfig = {
ssh: {
algorithms: {
kex: ["algo1", "algo2"],
cipher: ["cipher1", "cipher2"],
serverHostKey: ["ssh-rsa", "ssh-dss"],
hmac: ["hmac1", "hmac2"],
compress: ["none", "zlib"]
},
readyTimeout: 20000,
keepaliveInterval: 60000,
keepaliveCountMax: 10
}
}
sshConnection = new SSHConnection(mockConfig)
mockSSH2Client = {
on: jest.fn(),
connect: jest.fn(),
shell: jest.fn(),
end: jest.fn()
}
SSH2.Client.mockImplementation(() => mockSSH2Client)
})
afterEach(() => {
jest.clearAllMocks()
})
describe("connect", () => {
// ... previous tests ...
it("should handle connection errors", () => {
const mockCreds = {
host: "example.com",
port: 22,
username: "user",
password: "pass"
}
mockSSH2Client.on.mockImplementation((event, callback) => {
if (event === "error") {
callback(new Error("Connection failed"))
}
})
return sshConnection.connect(mockCreds).catch(error => {
expect(error).toBeInstanceOf(SSHConnectionError)
expect(error.message).toBe("SSH Connection error: Connection failed")
})
})
})
describe("shell", () => {
beforeEach(() => {
sshConnection.conn = mockSSH2Client
})
it("should open a shell successfully", () => {
const mockStream = {
on: jest.fn(),
stderr: { on: jest.fn() }
}
mockSSH2Client.shell.mockImplementation((options, callback) => {
callback(null, mockStream)
})
return sshConnection.shell().then(result => {
expect(result).toBe(mockStream)
expect(sshConnection.stream).toBe(mockStream)
})
})
it("should handle shell errors", () => {
mockSSH2Client.shell.mockImplementation((options, callback) => {
callback(new Error("Shell error"))
})
return sshConnection.shell().catch(error => {
expect(error.message).toBe("Shell error")
})
})
})
describe("resizeTerminal", () => {
it("should resize the terminal if stream exists", () => {
const mockStream = {
setWindow: jest.fn()
}
sshConnection.stream = mockStream
sshConnection.resizeTerminal(80, 24)
expect(mockStream.setWindow).toHaveBeenCalledWith(80, 24)
})
it("should not resize if stream does not exist", () => {
sshConnection.stream = null
sshConnection.resizeTerminal(80, 24)
// No error should be thrown
})
})
describe("end", () => {
it("should end the stream and connection", () => {
const mockStream = {
end: jest.fn()
}
sshConnection.stream = mockStream
sshConnection.conn = mockSSH2Client
sshConnection.end()
expect(mockStream.end).toHaveBeenCalled()
expect(mockSSH2Client.end).toHaveBeenCalled()
expect(sshConnection.stream).toBeNull()
expect(sshConnection.conn).toBeNull()
})
it("should handle ending when stream and connection do not exist", () => {
sshConnection.stream = null
sshConnection.conn = null
sshConnection.end()
// No error should be thrown
})
})
})

222
tests/utils.test.js Normal file
View file

@ -0,0 +1,222 @@
// server
// tests/utils.test.js
const {
deepMerge,
getValidatedHost,
getValidatedPort,
isValidCredentials,
maskSensitiveData,
modifyHtml,
validateConfig,
validateSshTerm
} = require("../app/utils")
describe("utils", () => {
describe("deepMerge", () => {
it("should merge two objects deeply", () => {
const obj1 = { a: { b: 1 }, c: 2 }
const obj2 = { a: { d: 3 }, e: 4 }
const result = deepMerge(obj1, obj2)
expect(result).toEqual({ a: { b: 1, d: 3 }, c: 2, e: 4 })
})
})
describe("getValidatedHost", () => {
it("should return IP address unchanged", () => {
expect(getValidatedHost("192.168.1.1")).toBe("192.168.1.1")
})
it("should escape hostname", () => {
expect(getValidatedHost("example.com")).toBe("example.com")
expect(getValidatedHost("<script>alert('xss')</script>")).toBe(
"&lt;script&gt;alert(&#x27;xss&#x27;)&lt;&#x2F;script&gt;"
)
})
})
describe("getValidatedPort", () => {
it("should return valid port number", () => {
expect(getValidatedPort("22")).toBe(22)
expect(getValidatedPort("8080")).toBe(8080)
})
it("should return default port for invalid input", () => {
expect(getValidatedPort("invalid")).toBe(22)
expect(getValidatedPort("0")).toBe(22)
expect(getValidatedPort("65536")).toBe(22)
})
})
describe("isValidCredentials", () => {
it("should return true for valid credentials", () => {
const validCreds = {
username: "user",
password: "pass",
host: "example.com",
port: 22
}
expect(isValidCredentials(validCreds)).toBe(true)
})
it("should return false for invalid credentials", () => {
expect(isValidCredentials(null)).toBe(false)
expect(isValidCredentials({})).toBe(false)
expect(isValidCredentials({ username: "user" })).toBe(false)
})
})
describe("maskSensitiveData", () => {
it("should mask simple password property", () => {
const testObj = { username: "user", password: "secret123" }
const maskedObj = maskSensitiveData(testObj)
console.log("maskedObj.password.length: ", maskedObj.password.length)
expect(maskedObj.username).toBe("user")
expect(maskedObj.password).not.toBe("secret123")
expect(maskedObj.password.length).toBeGreaterThanOrEqual(3)
expect(maskedObj.password.length).toBeLessThanOrEqual(9)
})
it("should mask array elements when property is specified", () => {
const testObj = {
action: "keyboard-interactive",
responses: ["sensitive_password", "another_sensitive_value"]
}
const maskedObj = maskSensitiveData(testObj, {
properties: ["responses"]
})
expect(maskedObj.action).toBe("keyboard-interactive")
expect(Array.isArray(maskedObj.responses)).toBe(true)
expect(maskedObj.responses).toHaveLength(2)
expect(maskedObj.responses[0]).not.toBe("sensitive_password")
expect(maskedObj.responses[1]).not.toBe("another_sensitive_value")
expect(maskedObj.responses[0]).toHaveLength(8)
expect(maskedObj.responses[1]).toHaveLength(8)
})
it("should not mask non-specified properties", () => {
const testObj = {
username: "user",
password: "secret",
data: ["public_info", "not_sensitive"]
}
const maskedObj = maskSensitiveData(testObj, {
properties: ["password"]
})
expect(maskedObj.username).toBe("user")
expect(maskedObj.password).not.toBe("secret")
expect(maskedObj.data).toEqual(["public_info", "not_sensitive"])
})
it("should handle nested objects", () => {
const testObj = {
user: {
name: "John",
credentials: {
password: "topsecret",
token: "abcdef123456"
}
}
}
const maskedObj = maskSensitiveData(testObj)
expect(maskedObj.user.name).toBe("John")
expect(maskedObj.user.credentials.password).not.toBe("topsecret")
expect(maskedObj.user.credentials.token).not.toBe("abcdef123456")
})
})
describe("modifyHtml", () => {
it("should modify HTML content", () => {
const html = "window.webssh2Config = null;"
const config = { key: "value" }
const content = `window.webssh2Config = ${JSON.stringify(config)};`
const modified = modifyHtml(html, config)
expect(modified).toContain('window.webssh2Config = {"key":"value"};')
})
})
describe("validateConfig", () => {
it("should validate correct config", () => {
const validConfig = {
listen: {
ip: "0.0.0.0",
port: 2222
},
http: {
origins: ["http://localhost:8080"]
},
user: {
name: null,
password: null,
privatekey: null
},
ssh: {
host: null,
port: 22,
localAddress: null,
localPort: null,
term: "xterm-color",
readyTimeout: 20000,
keepaliveInterval: 120000,
keepaliveCountMax: 10,
allowedSubnets: []
},
header: {
text: null,
background: "green"
},
options: {
challengeButton: true,
autoLog: false,
allowReauth: true,
allowReconnect: true,
allowReplay: 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"]
}
}
expect(() => validateConfig(validConfig)).not.toThrow()
})
it("should throw error for invalid config", () => {
const invalidConfig = {}
expect(() => validateConfig(invalidConfig)).toThrow()
})
})
describe("validateSshTerm", () => {
it("should return valid SSH term", () => {
expect(validateSshTerm("xterm")).toBe("xterm")
expect(validateSshTerm("xterm-256color")).toBe("xterm-256color")
})
it("should return null for invalid SSH term", () => {
expect(validateSshTerm("")).toBe(null)
expect(validateSshTerm("<script>alert('xss')</script>")).toBe(null)
})
})
})