├── .csslintrc ├── .devcontainer ├── devcontainer.json └── scripts │ └── tools.sh ├── .dockerignore ├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.yml │ ├── custom.md │ └── feature_request.md └── workflows │ ├── action-test.yml │ ├── docker-multiplatform-tag.yml │ ├── docker-multiplatform.yml │ └── release.yml ├── .gitignore ├── .release-please-manifest.json ├── .travis.yml ├── BUILDING.md ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── ChangeLog.md ├── Dockerfile ├── LICENSE ├── Makefile ├── README.md ├── SECURITY.md ├── app ├── .eslintrc.json ├── .npmrc ├── .prettierrc ├── .snyk ├── CHANGELOG.md ├── LICENSE ├── bun.lockb ├── client │ ├── public │ │ ├── client.htm │ │ ├── favicon.ico │ │ ├── webssh2.bundle.js │ │ └── webssh2.css │ ├── src │ │ ├── README.md │ │ ├── client.htm │ │ ├── css │ │ │ └── style.css │ │ ├── favicon.ico │ │ └── js │ │ │ └── index.ts │ └── tsconfig.json ├── config.json.sample ├── index.js ├── jsconfig.json ├── package-lock.json ├── package.json ├── scripts │ ├── webpack.common.js │ ├── webpack.dev.js │ └── webpack.prod.js └── server │ ├── app.js │ ├── config.js │ ├── form.html │ ├── logging.js │ ├── routes.js │ ├── socket.js │ └── util.js ├── bun.lockb ├── package.json └── release-please-config.json /.csslintrc: -------------------------------------------------------------------------------- 1 | --exclude-exts=.min.css 2 | --ignore=adjoining-classes,box-model,ids,order-alphabetical,unqualified-attributes 3 | -------------------------------------------------------------------------------- /.devcontainer/devcontainer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Node.js & TypeScript", 3 | "image": "mcr.microsoft.com/devcontainers/base:jammy", 4 | 5 | "mounts": [ 6 | "source=${localEnv:HOME}${localEnv:USERPROFILE}/.ssh/personal_id_rsa.pub,target=/home/vscode/.hostssh/id_rsa.pub,readonly,type=bind,consistency=cached" 7 | ], 8 | "features": { 9 | "ghcr.io/devcontainers-contrib/features/node-asdf:0": {}, 10 | }, 11 | // Configure tool-specific properties. 12 | "customizations": { 13 | // Configure properties specific to VS Code. 14 | "vscode": { 15 | // Add the IDs of extensions you want installed when the container is created. 16 | "extensions": [ 17 | "ms-vscode-remote.remote-containers", 18 | "dbaeumer.vscode-eslint", 19 | "GitHub.copilot", 20 | "GitHub.copilot-chat", 21 | "esbenp.prettier-vscode", 22 | "rvest.vs-code-prettier-eslint", 23 | "bierner.markdown-mermaid", 24 | "stylelint.vscode-stylelint" 25 | ] 26 | } 27 | }, 28 | 29 | // Use 'forwardPorts' to make a list of ports inside the container available locally. 30 | // "forwardPorts": [], 31 | 32 | // Use 'postCreateCommand' to run commands after the container is created. 33 | "postCreateCommand": "/bin/bash ./.devcontainer/scripts/tools.sh >> ~/post-create-tools.log", 34 | 35 | // Comment out to connect as root instead. More info: https://aka.ms/vscode-remote/containers/non-root. 36 | "remoteUser": "vscode" 37 | } -------------------------------------------------------------------------------- /.devcontainer/scripts/tools.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | mkdir -p ~/.ssh && \ 4 | touch ~/.ssh/known_hosts && \ 5 | sudo tee ~/.ssh/config > /dev/null << EOF 6 | Host github.com 7 | HostName github.com 8 | PreferredAuthentications publickey 9 | IdentityFile ~/.hostssh/id_rsa.pub 10 | EOF 11 | 12 | sudo chown -R vscode:vscode ~/.ssh -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | .git 2 | .cache 3 | app/node_modules 4 | app/client/src 5 | app/scripts 6 | app/.* 7 | app/*.sample 8 | app/client/tsconfig.json 9 | app/CHANGELOG.md 10 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.yml: -------------------------------------------------------------------------------- 1 | name: Bug Report 2 | description: File a bug report 3 | title: "[Bug]: " 4 | labels: ["bug", "triage"] 5 | assignees: 6 | - billchurch 7 | body: 8 | - type: markdown 9 | attributes: 10 | value: | 11 | Depending on the type of issue, please include the follwing information: 12 | - type: textarea 13 | id: what-happened 14 | attributes: 15 | label: What happened? 16 | description: Also tell us, what did you expect to happen? 17 | placeholder: Tell us what you see! 18 | value: "A bug happened!" 19 | validations: 20 | required: true 21 | - type: input 22 | id: node_ver 23 | attributes: 24 | label: Node Version 25 | description: version of Node this problem occurs on 26 | placeholder: npm -v 27 | validations: 28 | required: true 29 | - type: input 30 | id: npm_ver 31 | attributes: 32 | label: NPM Version 33 | description: version of NPM this problem occurs on 34 | placeholder: npm -v 35 | validations: 36 | required: true 37 | - type: input 38 | id: server_ver 39 | attributes: 40 | label: Server OS Version 41 | description: Server OS Version / Distribution / Processor Architecture 42 | placeholder: uname -a;cat /etc/os-release 43 | validations: 44 | required: true 45 | - type: input 46 | id: webssh2_ver 47 | attributes: 48 | label: WebSSH2 release version 49 | description: Version of WebSSH you are using 50 | placeholder: grep version app/package.json 51 | validations: 52 | required: true 53 | - type: input 54 | id: sshhost_ver 55 | attributes: 56 | label: OS and Version of SSH server 57 | description: OS and Version of SSH server connecting to 58 | placeholder: 'on target server run: uname -a;sshd -v' 59 | validations: 60 | required: false 61 | - type: input 62 | id: browser_ver 63 | attributes: 64 | label: Browser Version 65 | description: Information from brwoser's About... or a screenshot of the about screen. 66 | placeholder: 67 | validations: 68 | required: false 69 | - type: textarea 70 | id: logs 71 | attributes: 72 | label: Relevant log output 73 | description: Please copy and paste any relevant log output. This will be automatically formatted into code, so no need for backticks. 74 | render: shell 75 | 76 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/custom.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Question 3 | about: General how-to questions 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | 11 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /.github/workflows/action-test.yml: -------------------------------------------------------------------------------- 1 | name: Manually Release Previous Tag 2 | 3 | on: 4 | workflow_dispatch: 5 | inputs: 6 | tag: 7 | description: 'Repo Branch/Tag' 8 | default: 'main' 9 | type: 'string' 10 | required: true 11 | 12 | jobs: 13 | docker: 14 | runs-on: ubuntu-latest 15 | steps: 16 | - name: 'Checkout' 17 | uses: actions/checkout@v3 18 | with: 19 | ref: ${{ inputs.tag }} 20 | - name: Prepare 21 | id: prep 22 | run: | 23 | DOCKER_IMAGE=${{ secrets.DOCKER_USERNAME }}/${GITHUB_REPOSITORY#*/} 24 | 25 | VERSION=${{ inputs.tag }} 26 | VERSION="${VERSION//v}" 27 | TAGS="${DOCKER_IMAGE}:${VERSION},${DOCKER_IMAGE}" 28 | 29 | # If the VERSION looks like a version number, assume that 30 | # this is the most recent version of the image and also 31 | # tag it 'latest'. 32 | if [[ $VERSION =~ ^[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}$ ]]; then 33 | TAGS="$TAGS,${DOCKER_IMAGE}" 34 | fi 35 | 36 | 37 | # Set output parameters. 38 | echo ::set-output name=tags::${TAGS} 39 | echo ::set-output name=docker_image::${DOCKER_IMAGE} 40 | 41 | - name: Set up QEMU 42 | uses: docker/setup-qemu-action@master 43 | with: 44 | platforms: all 45 | 46 | - name: Set up Docker Buildx 47 | id: buildx 48 | uses: docker/setup-buildx-action@master 49 | 50 | - name: Login to DockerHub 51 | if: github.event_name != 'pull_request' 52 | uses: docker/login-action@v1 53 | with: 54 | username: ${{ secrets.DOCKER_USERNAME }} 55 | password: ${{ secrets.DOCKER_PASSWORD }} 56 | 57 | - name: Build 58 | uses: docker/build-push-action@v2 59 | with: 60 | builder: ${{ steps.buildx.outputs.name }} 61 | context: . 62 | file: ./Dockerfile 63 | platforms: linux/amd64,linux/arm64,linux/ppc64le 64 | push: true 65 | tags: ${{ steps.prep.outputs.tags }} 66 | -------------------------------------------------------------------------------- /.github/workflows/docker-multiplatform-tag.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: 'Build Docker On Tag' 3 | 4 | on: 5 | push: 6 | branches: 7 | - bigip-server 8 | tags: 9 | - 'v[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}' 10 | workflow_dispatch: # Allows manual triggering from the GitHub UI 11 | 12 | jobs: 13 | docker: 14 | runs-on: ubuntu-latest 15 | steps: 16 | - name: 'Checkout' 17 | uses: actions/checkout@v3 18 | 19 | - name: Prepare 20 | id: prep 21 | run: | 22 | DOCKER_IMAGE=${{ secrets.DOCKER_USERNAME }}/${GITHUB_REPOSITORY#*/} 23 | 24 | # If this is a git tag, use the tag name as a docker tag 25 | if [[ $GITHUB_REF == refs/tags/* ]]; then 26 | VERSION=${GITHUB_REF#refs/tags/v} 27 | TAGS="${DOCKER_IMAGE}:${VERSION}" 28 | fi 29 | 30 | # If this is a git branch, use the branch name as a docker tag 31 | if [[ $GITHUB_REF == refs/heads/* ]]; then 32 | VERSION=${GITHUB_REF#refs/heads/} 33 | TAGS="${DOCKER_IMAGE}:${VERSION}" 34 | fi 35 | 36 | # If the VERSION looks like a version number, also tag as 'latest' 37 | if [[ $VERSION =~ ^[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}$ ]]; then 38 | TAGS="$TAGS,${DOCKER_IMAGE}:latest" 39 | fi 40 | 41 | # Set output parameters 42 | echo ::set-output name=tags::${TAGS} 43 | echo ::set-output name=docker_image::${DOCKER_IMAGE} 44 | 45 | - name: Set up QEMU 46 | uses: docker/setup-qemu-action@v3 47 | with: 48 | platforms: all 49 | 50 | - name: Set up Docker Buildx 51 | id: buildx 52 | uses: docker/setup-buildx-action@v3 53 | 54 | - name: Login to DockerHub 55 | if: github.event_name != 'pull_request' 56 | uses: docker/login-action@v2 57 | with: 58 | username: ${{ secrets.DOCKER_USERNAME }} 59 | password: ${{ secrets.DOCKER_PASSWORD }} 60 | 61 | - name: Build 62 | uses: docker/build-push-action@v4 63 | with: 64 | builder: ${{ steps.buildx.outputs.name }} 65 | context: . 66 | file: ./Dockerfile 67 | platforms: linux/amd64,linux/arm64,linux/ppc64le 68 | push: true 69 | tags: ${{ steps.prep.outputs.tags }} 70 | -------------------------------------------------------------------------------- /.github/workflows/docker-multiplatform.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: 'Build Docker Images' 3 | 4 | on: 5 | release: 6 | types: [published] 7 | 8 | jobs: 9 | docker: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - name: 'Checkout' 13 | uses: actions/checkout@v3 14 | - name: Prepare 15 | id: prep 16 | run: | 17 | DOCKER_IMAGE=${{ secrets.DOCKER_USERNAME }}/${GITHUB_REPOSITORY#*/} 18 | 19 | # If this is git tag, use the tag name as a docker tag 20 | if [[ $GITHUB_REF == refs/tags/* ]]; then 21 | VERSION=${GITHUB_REF#refs/tags/webssh2-v} 22 | TAGS="${DOCKER_IMAGE}:${VERSION}" 23 | fi 24 | 25 | # If this is git branch, use the branch name as a docker tag 26 | if [[ $GITHUB_REF == refs/heads/* ]]; then 27 | VERSION=${GITHUB_REF#refs/heads/} 28 | TAGS="${DOCKER_IMAGE}:${VERSION}" 29 | fi 30 | 31 | # If the VERSION looks like a version number, assume that 32 | # this is the most recent version of the image and also 33 | # tag it 'latest'. This is done by just specifying the ${DOCKER_IMAGE} 34 | # without a tag. 35 | if [[ $VERSION =~ ^[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}$ ]]; then 36 | TAGS="$TAGS,${DOCKER_IMAGE}" 37 | fi 38 | 39 | # Set output parameters. 40 | echo ::set-output name=tags::${TAGS} 41 | echo ::set-output name=docker_image::${DOCKER_IMAGE} 42 | 43 | - name: Set up QEMU 44 | uses: docker/setup-qemu-action@master 45 | with: 46 | platforms: all 47 | 48 | - name: Set up Docker Buildx 49 | id: buildx 50 | uses: docker/setup-buildx-action@master 51 | 52 | - name: Login to DockerHub 53 | if: github.event_name != 'pull_request' 54 | uses: docker/login-action@v1 55 | with: 56 | username: ${{ secrets.DOCKER_USERNAME }} 57 | password: ${{ secrets.DOCKER_PASSWORD }} 58 | 59 | - name: Build 60 | uses: docker/build-push-action@v2 61 | with: 62 | builder: ${{ steps.buildx.outputs.name }} 63 | context: . 64 | file: ./Dockerfile 65 | platforms: linux/amd64,linux/arm64,linux/ppc64le 66 | push: true 67 | tags: ${{ steps.prep.outputs.tags }} -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: 'Create Release' 3 | on: 4 | push: 5 | branches: 6 | - main 7 | paths-ignore: 8 | - '.github/**' 9 | - '.devcontainer/**' 10 | - '.**' 11 | - '**.md' 12 | jobs: 13 | release: 14 | runs-on: ubuntu-latest 15 | outputs: 16 | paths_released: ${{ steps.manifest_release.outputs.paths_released }} 17 | steps: 18 | - uses: google-github-actions/release-please-action@v3 19 | id: manifest_release 20 | with: 21 | token: ${{ secrets.RELEASE_PLEASE_UAT }} 22 | command: manifest 23 | package-name: webssh2 24 | path: app 25 | default-branch: main 26 | release-type: node 27 | publish: 28 | runs-on: ubuntu-20.04 29 | needs: release 30 | strategy: 31 | fail-fast: false 32 | matrix: 33 | path: ${{fromJson(needs.release.outputs.paths_released)}} 34 | steps: 35 | - uses: actions/checkout@v2 36 | - uses: actions/setup-node@v1 37 | with: 38 | node-version: 16 39 | registry-url: 'https://registry.npmjs.org' 40 | - name: publish-to-npm 41 | env: 42 | NODE_AUTH_TOKEN: ${{secrets.NPM_TOKEN}} 43 | run: | 44 | cd ${{ matrix.path }} 45 | npm install 46 | npx lerna bootstrap 47 | npx lerna publish from-package --no-push --no-private --yes -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | #Docs 2 | docs 3 | 4 | ssl/* 5 | 6 | bigip/* 7 | 8 | config.json 9 | 10 | # Logs 11 | logs 12 | *.log 13 | npm-debug.log* 14 | 15 | # Editor preference files 16 | *.sublime-* 17 | 18 | # Runtime data 19 | pids 20 | *.pid 21 | *.seed 22 | 23 | # Directory for instrumented libs generated by jscoverage/JSCover 24 | lib-cov 25 | 26 | # Coverage directory used by tools like istanbul 27 | coverage 28 | 29 | # nyc test coverage 30 | .nyc_output 31 | 32 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 33 | .grunt 34 | 35 | # node-waf configuration 36 | .lock-wscript 37 | 38 | # Dependency directories 39 | node_modules 40 | jspm_packages 41 | 42 | # Optional npm cache directory 43 | .npm 44 | 45 | # Optional REPL history 46 | .node_repl_history 47 | 48 | #Mac Files 49 | .DS_Store 50 | 51 | Build/Release 52 | app/bob_rsa 53 | -------------------------------------------------------------------------------- /.release-please-manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "app": "0.5.0-pre-4" 3 | } -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - 14 4 | - 16 5 | before_install: 6 | - npm i -g snyk 7 | -------------------------------------------------------------------------------- /BUILDING.md: -------------------------------------------------------------------------------- 1 | # Buliding 2 | 3 | To rebuild the client files, you need at least Node v14. 4 | 5 | The source of the client files are located in `./app/client/src` 6 | 7 | `npm run build` will compile the source files there into `./app/client/public/`. This directory is considered to be volitile and is deleted every time `npm run build` is invoked. 8 | 9 | WebPack is used for building and the configuration is located in `./app/scripts` 10 | 11 | If one wishes to make changes to the javascript, the html, or the css it should be done in `./app/client/src` and then complied using `npm run build` 12 | 13 | For development purposes, you may also utilize `npm run builddev` which will not minimize the source and allow you to more easily troubleshoot while making customizations. 14 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | 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. 6 | 7 | ## Our Standards 8 | 9 | Examples of behavior that contributes to creating a positive environment include: 10 | 11 | * Using welcoming and inclusive language 12 | * Being respectful of differing viewpoints and experiences 13 | * Gracefully accepting constructive criticism 14 | * Focusing on what is best for the community 15 | * Showing empathy towards other community members 16 | 17 | Examples of unacceptable behavior by participants include: 18 | 19 | * The use of sexualized language or imagery and unwelcome sexual attention or advances 20 | * Trolling, insulting/derogatory comments, and personal or political attacks 21 | * Public or private harassment 22 | * Publishing others' private information, such as a physical or electronic address, without explicit permission 23 | * Other conduct which could reasonably be considered inappropriate in a professional setting 24 | 25 | ## Our Responsibilities 26 | 27 | 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. 28 | 29 | 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. 30 | 31 | ## Scope 32 | 33 | 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. 34 | 35 | ## Enforcement 36 | 37 | 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. 38 | 39 | 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. 40 | 41 | ## Attribution 42 | 43 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version] 44 | 45 | [homepage]: http://contributor-covenant.org 46 | [version]: http://contributor-covenant.org/version/1/4/ 47 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | Guidelines for contributing code: 2 | 3 | Make sure code passes linting from (StandardJS)[https://standardjs.com/] 4 | 5 | 6 | # Contributing 7 | 8 | When contributing to this repository, please first discuss the change you wish to make via issue, 9 | email, or any other method with the owners of this repository before making a change. 10 | 11 | Please note we have a code of conduct, please follow it in all your interactions with the project. 12 | 13 | ## Pull Request Process 14 | 15 | 1. Ensure any install or build dependencies are removed before the end of the layer when doing a 16 | build. 17 | 2. Make sure code passes linting from (StandardJS)[https://standardjs.com/] 18 | 3. Explain what you're trying to accomplish in your commits 19 | 4. Update changelog and Readme if needed. 20 | 21 | ## Code of Conduct 22 | 23 | ### Our Pledge 24 | 25 | In the interest of fostering an open and welcoming environment, we as 26 | contributors and maintainers pledge to making participation in our project and 27 | our community a harassment-free experience for everyone, regardless of age, body 28 | size, disability, ethnicity, gender identity and expression, level of experience, 29 | nationality, personal appearance, race, religion, or sexual identity and 30 | orientation. 31 | 32 | ### Our Standards 33 | 34 | Examples of behavior that contributes to creating a positive environment 35 | include: 36 | 37 | * Using welcoming and inclusive language 38 | * Being respectful of differing viewpoints and experiences 39 | * Gracefully accepting constructive criticism 40 | * Focusing on what is best for the community 41 | * Showing empathy towards other community members 42 | 43 | Examples of unacceptable behavior by participants include: 44 | 45 | * The use of sexualized language or imagery and unwelcome sexual attention or 46 | advances 47 | * Trolling, insulting/derogatory comments, and personal or political attacks 48 | * Public or private harassment 49 | * Publishing others' private information, such as a physical or electronic 50 | address, without explicit permission 51 | * Other conduct which could reasonably be considered inappropriate in a 52 | professional setting 53 | 54 | ### Our Responsibilities 55 | 56 | Project maintainers are responsible for clarifying the standards of acceptable 57 | behavior and are expected to take appropriate and fair corrective action in 58 | response to any instances of unacceptable behavior. 59 | 60 | Project maintainers have the right and responsibility to remove, edit, or 61 | reject comments, commits, code, wiki edits, issues, and other contributions 62 | that are not aligned to this Code of Conduct, or to ban temporarily or 63 | permanently any contributor for other behaviors that they deem inappropriate, 64 | threatening, offensive, or harmful. 65 | 66 | ### Scope 67 | 68 | This Code of Conduct applies both within project spaces and in public spaces 69 | when an individual is representing the project or its community. Examples of 70 | representing a project or community include using an official project e-mail 71 | address, posting via an official social media account, or acting as an appointed 72 | representative at an online or offline event. Representation of a project may be 73 | further defined and clarified by project maintainers. 74 | 75 | ### Enforcement 76 | 77 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 78 | reported by contacting the project team at [INSERT EMAIL ADDRESS]. All 79 | complaints will be reviewed and investigated and will result in a response that 80 | is deemed necessary and appropriate to the circumstances. The project team is 81 | obligated to maintain confidentiality with regard to the reporter of an incident. 82 | Further details of specific enforcement policies may be posted separately. 83 | 84 | Project maintainers who do not follow or enforce the Code of Conduct in good 85 | faith may face temporary or permanent repercussions as determined by other 86 | members of the project's leadership. 87 | 88 | ### Attribution 89 | 90 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 91 | available at [http://contributor-covenant.org/version/1/4][version] 92 | 93 | [homepage]: http://contributor-covenant.org 94 | [version]: http://contributor-covenant.org/version/1/4/ 95 | -------------------------------------------------------------------------------- /ChangeLog.md: -------------------------------------------------------------------------------- 1 | # Changelog Moved 2 | 3 | See [app/CHANGELOG.md](app/CHANGELOG.md) 4 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:16-alpine 2 | 3 | RUN apk update && apk add bash 4 | 5 | WORKDIR /usr/src 6 | COPY app/ /usr/src/ 7 | RUN npm ci --audit=false --bin-links=false --fund=false 8 | EXPOSE 2222/tcp 9 | ENTRYPOINT [ "/usr/local/bin/node", "index.js" ] 10 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Bill Church 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | test: 2 | cd ./app; npm run test -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # WebSSH2 2 | 3 | [![Build Status](https://travis-ci.com/billchurch/webssh2.svg?branch=main)](https://travis-ci.com/billchurch/webssh2) [![GitHub version](https://img.shields.io/github/v/release/billchurch/webssh2)](https://github.com/billchurch/webssh2/releases/latest) [![docker build images](https://github.com/billchurch/webssh2/actions/workflows/docker-multiplatform.yml/badge.svg)](https://github.com/billchurch/webssh2/actions/workflows/docker-multiplatform.yml) 4 | 5 | [![Buy Me A Coffee](https://www.buymeacoffee.com/assets/img/custom_images/orange_img.png)](https://www.buymeacoffee.com/billchurch) 6 | 7 | Web SSH Client using ssh2, socket.io, xterm.js, and express 8 | 9 | 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. 10 | 11 | WebSSH2 v0.2.0 demo 12 | 13 | # Requirements 14 | Node v14.x or above. If using ** element, modify `./src/index.js` directly, or check out `webpack.*.js` and add your custom javascript file to a task there (best option). 368 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | ## Supported Versions 4 | 5 | The following versions will get security updates. 6 | 7 | | Version | Supported | 8 | | ------- | ------------------ | 9 | | 0.4.x | :white_check_mark: | 10 | | 0.3.x | :x: | 11 | | 0.2.x | :x: | 12 | | 0.1.x | :x: | 13 | 14 | ## Reporting a Vulnerability 15 | 16 | If you find a vulnerability, simply [open an issue](../../issues/new) with the details, use the label `security`. 17 | -------------------------------------------------------------------------------- /app/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "ignorePatterns": ["**/*{.,-}min.js"], 3 | "env": { 4 | "browser": true, 5 | "es2021": true, 6 | "node": true 7 | }, 8 | "extends": [ 9 | "airbnb-base", 10 | "prettier" 11 | ], 12 | "parser": "@typescript-eslint/parser", 13 | "parserOptions": { 14 | "ecmaVersion": 12, 15 | "sourceType": "module" 16 | }, 17 | "plugins": [ 18 | "@typescript-eslint", 19 | "prettier" 20 | ], 21 | "rules": { 22 | "prettier/prettier": ["error"] 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /app/.npmrc: -------------------------------------------------------------------------------- 1 | tag-version-prefix="" 2 | -------------------------------------------------------------------------------- /app/.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 100, 3 | "singleQuote": true 4 | } -------------------------------------------------------------------------------- /app/.snyk: -------------------------------------------------------------------------------- 1 | # Snyk (https://snyk.io) policy file, patches or ignores known vulnerabilities. 2 | version: v1.22.1 3 | ignore: {} 4 | patch: {} 5 | -------------------------------------------------------------------------------- /app/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | 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. 4 | 5 | ## [0.5.0-pre-4](https://github.com/billchurch/webssh2/compare/webssh2-v0.4.7-pre-4...webssh2-v0.5.0-pre-4) (2022-08-07) 6 | 7 | 8 | ### Features 9 | 10 | * test change for release ([476b566](https://github.com/billchurch/webssh2/commit/476b566c08a84bd35aaccf847253875b2c3afb10)) 11 | 12 | ## [0.4.7-pre-4](https://github.com/billchurch/webssh2/compare/webssh2-v0.4.7-pre-3...webssh2-v0.4.7-pre-4) (2022-08-03) 13 | 14 | 15 | ### Miscellaneous Chores 16 | 17 | * release 0.4.7-pre-4 ([7d4ba87](https://github.com/billchurch/webssh2/commit/7d4ba87bc1c198600ea33ee220553ef46ea2a103)) 18 | 19 | ## [0.4.7-pre-3](https://github.com/billchurch/webssh2/compare/webssh2-v0.4.7-pre-2...webssh2-v0.4.7-pre-3) (2022-08-03) 20 | 21 | 22 | ### Miscellaneous Chores 23 | 24 | * release 0.4.7-pre-3 ([0c78c1f](https://github.com/billchurch/webssh2/commit/0c78c1f31cc6380b7f0706822fc418cfede11413)) 25 | 26 | ## [0.4.7-pre-2](https://github.com/billchurch/webssh2/compare/webssh2-v0.4.6...webssh2-v0.4.7-pre-2) (2022-08-02) 27 | 28 | 29 | ### ⚠ BREAKING CHANGES 30 | 31 | * validate referer to /reauth is valid 32 | * bump xterm to 4.18.0 33 | * consistent logging messages see #286 34 | * config system changes #284 (#285) 35 | 36 | ### Features 37 | 38 | * add additional params for POST requests [#290](https://github.com/billchurch/webssh2/issues/290) ([46c1560](https://github.com/billchurch/webssh2/commit/46c1560e3c126376e18124e14e5c7fb8c029a0a1)) 39 | * add additional vars to POST requests [#290](https://github.com/billchurch/webssh2/issues/290) ([0a4e419](https://github.com/billchurch/webssh2/commit/0a4e419fb371ae95340fa890497022a2aa9d063a)) 40 | * add fontFamily, letterSpacing, lineHeight ([97f3088](https://github.com/billchurch/webssh2/commit/97f3088780744e13a6724a4967a4896aac3f20d8)) 41 | * add fontSize option [#292](https://github.com/billchurch/webssh2/issues/292) ([5e78812](https://github.com/billchurch/webssh2/commit/5e788129744d326e78ec91bda86ed5cecfd70d3f)) 42 | * config system changes [#284](https://github.com/billchurch/webssh2/issues/284) ([#285](https://github.com/billchurch/webssh2/issues/285)) ([9c99b09](https://github.com/billchurch/webssh2/commit/9c99b0940ec726193deae3c4999d25a297874d67)) 43 | * consistent logging messages see [#286](https://github.com/billchurch/webssh2/issues/286) ([50cfcb9](https://github.com/billchurch/webssh2/commit/50cfcb97788cbd3409b4605adceef3d47e370e38)) 44 | * credentials over http post for [#290](https://github.com/billchurch/webssh2/issues/290) ([5b8f88c](https://github.com/billchurch/webssh2/commit/5b8f88cfef1745c88748277217204e6c38c7ff7e)) 45 | * reorder viewport setup at ssh handshake [#292](https://github.com/billchurch/webssh2/issues/292) ([140e1e2](https://github.com/billchurch/webssh2/commit/140e1e24b14d6b74848e9d250c2b44f806ad627d)) 46 | * validate referer to /reauth is valid ([0dcaa6e](https://github.com/billchurch/webssh2/commit/0dcaa6e15062cdc3252ce52abd9057caf4c00a30)) 47 | 48 | 49 | ### Bug Fixes 50 | 51 | * Fix the parameter passing problem of setDefaultCredentials to make it perform data initialization normally ([#288](https://github.com/billchurch/webssh2/issues/288)) ([40cbb35](https://github.com/billchurch/webssh2/commit/40cbb35616fa17c1c36520690f40ebce0b488153)) 52 | * invalid css in style.css ([ffab534](https://github.com/billchurch/webssh2/commit/ffab5345dcb568fa2bb50a96f403174ad3728286)) 53 | 54 | 55 | ### package 56 | 57 | * bump xterm to 4.18.0 ([84c09ec](https://github.com/billchurch/webssh2/commit/84c09ec8a1909e4bbd0051debdbb905276a4245e)) 58 | 59 | ### [0.4.6](https://github.com/billchurch/WebSSH2/compare/v0.2.10-0...v0.4.6) (2022-04-17) 60 | 61 | 62 | ### Features 63 | 64 | * add SIGTERM to safe shutdown feature ([675b4f5](https://github.com/billchurch/WebSSH2/commit/675b4f5a3a92b187b620684eb1ce1b7afa0e2e08)) 65 | * **auth:** ssh private key auth implemented via config.json ([#161](https://github.com/billchurch/WebSSH2/issues/161)) ([342df8e](https://github.com/billchurch/WebSSH2/commit/342df8eb9cafba52eb63b50a60e11e1431d6fbd4)) 66 | * **config:** specify local source address and port for client connections fixes [#152](https://github.com/billchurch/WebSSH2/issues/152) ([#158](https://github.com/billchurch/WebSSH2/issues/158)) ([65d6ec6](https://github.com/billchurch/WebSSH2/commit/65d6ec68452b80c42fd62534355e456ce1f16a32)) 67 | * CORS support ([b324f33](https://github.com/billchurch/WebSSH2/commit/b324f338adeb3518322941639fb83ba9370814cc)), closes [#240](https://github.com/billchurch/WebSSH2/issues/240) 68 | 69 | 70 | ### Bug Fixes 71 | 72 | * deprecated term.setOption ([d903da8](https://github.com/billchurch/WebSSH2/commit/d903da87c41882a3736683c7de497cb8bd37f885)) 73 | * dockerignore ([#272](https://github.com/billchurch/WebSSH2/issues/272)) ([8a68cca](https://github.com/billchurch/WebSSH2/commit/8a68ccaffa374584b5d9531f9dbeae616bd971f5)) 74 | * fixes default for allowreauth ([#239](https://github.com/billchurch/WebSSH2/issues/239)) ([dcfd81b](https://github.com/billchurch/WebSSH2/commit/dcfd81b454b9fe66edec489266dc35a765464c6b)), closes [#238](https://github.com/billchurch/WebSSH2/issues/238) 75 | * missing ENTRYPOINT for Dockerfile ([6a3a47a](https://github.com/billchurch/WebSSH2/commit/6a3a47a13de3cd70d603379a27e055f08a6ee62c)) 76 | * obey host ssh.host in config fixes [#190](https://github.com/billchurch/WebSSH2/issues/190) ([7b7e8e7](https://github.com/billchurch/WebSSH2/commit/7b7e8e753358ed48f52eb9aa2fc359bf758f304b)) 77 | * subnet unauthorized now emits "ssherror" which persists across websocket termination ([e796f9f](https://github.com/billchurch/WebSSH2/commit/e796f9fb5874d6557433f25e8976b7aa58fa8144)) 78 | * update config.json.sample ([#177](https://github.com/billchurch/WebSSH2/issues/177)) ([42f973b](https://github.com/billchurch/WebSSH2/commit/42f973b4796f7f50237dc8ce613e477aa89352ca)) 79 | * update read-config-ng to 3.0.5, fixes [#277](https://github.com/billchurch/WebSSH2/issues/277) ([3e82c0d](https://github.com/billchurch/WebSSH2/commit/3e82c0dc4d31d1c97a7cf98139ef8e6dc0213b22)) 80 | * update xterm.js fixes [#261](https://github.com/billchurch/WebSSH2/issues/261) ([c801ef9](https://github.com/billchurch/WebSSH2/commit/c801ef9e5826e13a403a6462241cf8a4ff456d45)) 81 | 82 | ## 0.4.5 [20220417] 83 | ### Fixes 84 | - update read-config-ng to 3.0.5, fixes [#277](../../issues/277) 85 | ## 0.4.5 [20220331] 86 | ### Fixes 87 | - Update socket.io to 4.2.0 88 | - Update read-config-ng to 3.0.4 89 | 90 | ## 0.4.4 [20211209] 91 | ### Fixes 92 | - Add ./node_modules to .dockerignore [#240](../../issues/240) thanks @UncleSamSwiss 93 | - validator to 13.7.0 [to mitigate potential Regular Expression Denial of Service (ReDoS)](https://snyk.io/vuln/SNYK-JS-VALIDATOR-1090600) 94 | - cidr-matcher should be [re-installed to pickup >json-schema@4.0.0 due to prototype pollution vulnerability](https://snyk.io/vuln/SNYK-JS-JSONSCHEMA-1920922) 95 | - Update xterm.js to 4.15.0 [#261](../../issues/261) 96 | - Replace deprecated term.setOptions with term.options 97 | ### Changes 98 | - update README.md for additional Docker methods thanks @Utopiah 99 | 100 | ## 0.4.3 [20211019] 101 | - update dependencies 102 | - ssh2 to 1.4.0 [to mitigate potential command injection in windows](https://snyk.io/vuln/SNYK-JS-SSH2-1656673) 103 | ## 0.4.2 [20210813] 104 | ### changes 105 | - update dependencies 106 | - socket.io to 4.1.1 107 | - read-config-ng to 3.0.2 108 | - debug to 4.3.1 109 | ## 0.4.1 [20210703] 110 | ### Fixes 111 | - lost comma in config.json.sample 71fe377 112 | ### Changes 113 | - bump ws@7.4.6 to [mitigate potential ReDoS vulnerability](https://github.com/websockets/ws/releases/tag/7.4.6) 114 | - dev: update CI tools 115 | - dev: update dev tools 116 | - dev: update build tools 117 | 118 | ## 0.4.0 [20210519] 119 | ### BREAKING 120 | - Disabled ssh.serverlog.client option, this disables the POC which allowed for logging of the data sent between the client/server to the console.log. 121 | - Dropping support for node versions under 14 122 | ### Changes 123 | - Removed HTML menu code from ./app/server/socket.js, the menu is now fully laid out in the ./app/client/src/index.html and the option elements are hidden by default. Not sure why it wasn't done this way from the start, but there it is. 124 | - Updated socket.io to v4.1.1 125 | - Client javascript `./app/client/src/js/index.ts` is now built on TypeScript (`npm run build` will generate javascript for client and place into `app/client/public/webssh2.bundle.js` as before) 126 | - Build environment changes 127 | - removed unused xterm-addon-search, xterm-addon-weblinks, standard, postcss-discard-comments 128 | - added prettier 2.3.0, typescript modules, socket.io-client 4.1.1, airbnb linting tools 129 | ### Added 130 | - Lookup ip address for hostname in URL, fixes #199 thanks to @zwiy 131 | - Ability to override `Authorization: Basic` header and replace with credentials specified in `config.json` fixes #243. New config.json option `user.overridebasic` 132 | ### CONTRIBUTING 133 | In this release, we're trying our best to conform to the [Airbnb Javascript Style Guide](https://airbnb.io/projects/javascript/). I'm hoping this will make contributions easier and keep the code readable. I love shortcuts more than anyone but I've found when making changes to code I've not looked at in a while, it can take me a few momements to deconstruct what was being done due to readbility issues. While I don't agree with every decision in the style guide (semi-colons, yuk), it is a good base to keep the code consistent. 134 | 135 | If you've not used it before, I recommend installing the [vscode extensions](https://blog.echobind.com/integrating-prettier-eslint-airbnb-style-guide-in-vscode-47f07b5d7d6a) for that and [Prettier](https://prettier.io/) and getting familiar. The autocorrections are great (especially if you hate dealing with semi-colons...) 136 | 137 | As of 0.4.0-testing-0, the client code is written in [TypeScript](https://www.typescriptlang.org/docs/handbook/typescript-in-5-minutes.html). It's not that much different from JavaScript, and the introduction strong typing will ultimately help to produce better code. Eventually we want to move the whole project to TypeScript but that make take a bit more time. Take a moment to look at ./app/client/src/js/index.ts to see what TypeScript looks like. 138 | ## 0.3.1 [20210513] 139 | ### BREAKING 140 | - Ability to configure CORS settings for socket.io see [#240](../../issues/240) for more information on how this may break existing deployments. Default settings in example `config.json` are currently permissive `http.origins: ["*:*"]` please note that if a `config.json` is not present, the default is `http.origins: ["localhost:2222"] 141 | ### Added 142 | - Safe Shutdown Feature - thanks to @edgarogh 143 | - Sending SIGINT or SIGTERM to node process responsible for WebSSH2 or Docker process will result in a "safe" shutdown 144 | - Timer is configured in config.safeShutdownDuration 145 | - feat: Use docker build to create multi-arch images (#202) 146 | ### Fixed 147 | - obey host ssh.host in config fixes #190 148 | ### Changed 149 | - `config.json.sample`: `allowreauth` now defaults to `false` fixes #238 150 | - update ssh2 to 0.8.8 -> 0.8.9 - [comparison at ssh2 repo](https://github.com/mscdex/ssh2/compare/v0.8.8...v0.8.9) 151 | - update xterm to 4.12.0 [comparison at xtermjs repo](https://github.com/xtermjs/xterm.js/compare/4.4.0...4.12.0) 152 | - update read-config-ng to 3.0.2 153 | - update morgan to 1.10.0 154 | - update debug to 4.3.1 155 | - update express-session to 1.17.1 156 | - update validator to 13.6.0 157 | - development tools updates (build environment requires minimum of Node 10, only needed for customization) 158 | - update @fortawesome/fontawesome-svg-core to 1.2.35 159 | - update @fortawesome/free-solid-svg-icons to 5.15.3 160 | - update copy-webpack-plugin to 8.1.1 161 | - update cross-env to 7.0.3 162 | - update css-loader to 5.2.4 163 | - update file-loader to 6.2.0 164 | - update mini-css-extract-plugin to 1.6.0 165 | - update postcss-discard-comments to 5.0.0 166 | - update snazzy to 9.0.0 167 | - update standard to 16.0.3 168 | - update standard-version to 9.3.0 169 | - update style-loader to 2.0.0 170 | - update terser-webpack-plugin to 5.1.1 171 | - update url-loader to 4.1.1 172 | - update webpack to 5.37.0 173 | - update webpack-cli to 4.7.0 174 | - update webpack-merge to 5.7.3 175 | - update webpack-stream to 6.1.2 176 | - update xterm-addon-fit to 0.5.0 177 | - update xterm-addon-search to 0.8.0 178 | - update xterm-addon-web-links to 0.4.0 179 | - update ssri from 6.0.1 to 6.0.2 [#233](../../pull/233) 180 | - update hosted-git-info from 2.8.5 to 2.8.9 [#237](../../pull/237) 181 | - update lodash from 4.17.19 to 4.17.21 [#236](../../pull/236) 182 | - update handlebars from 4.7.6 to 4.7.7 [#235](../../pull/235) 183 | - update y18n from 4.0.0 to 4.0.1 [#230](../../pull/230) 184 | - update elliptic from 6.5.3 to 6.5.4 [#228](../../pull/222833) 185 | - update ini from 1.3.5 to 1.3.8 [#217](../../pull/217) 186 | ## 0.3.0 [20200315] 187 | 🍀🍀🍀 188 | ### Added 189 | - Add configuration option to restrict connections to specified subnets thanks to @Mierdin 190 | - favicon 191 | - added module `serve-favicon` to serve favicon from root if pre-fetched by browser 192 | - added `link rel=icon` line in client.htm to serve favico.ico out of /ssh/ 193 | 194 | ### Changed 195 | - Using new repo for read-config -> read-config-ng- 196 | - removed express compression feature, added no real value. 197 | - module updates 198 | - ssh2 to 0.8.6 -> 0.8.8 - [comparison at ssh2 repo](https://github.com/mscdex/ssh2/compare/v0.8.6...v0.8.8) 199 | - xterm 4.2.0 -> 4.4.0 - [comparison at xtermjs repo](https://github.com/xtermjs/xterm.js/compare/4.2.0...4.4.0) 200 | - read-config-ng 3.0.1 - (taking over abandoned repo)n 201 | - development module updates (does not impact production, only for development and rebuilding) 202 | - fortawesome/fontawesome-svg-core 1.2.27 203 | - fortawesome/free-solid-svg-icons 5.12.1 204 | - standard-version 7.1.0 205 | - webpack 4.42.0 206 | - webpack-cli 3.3.11 207 | - terser-webpack-plugin 2.3.5 208 | - copy-webpack-plugin 5.1.1 209 | - cross-env 7.0.2 210 | - css-loader 3.4.2 211 | - file-loader 5.1.0 212 | - style-loader 1.1.3 213 | - url-loader 3.0.0 214 | 215 | ### Potentially Breaking Changes 216 | - Move all child resources to start from under /ssh 217 | - /socket.io -> /ssh/socket.io 218 | - /webssh2.css -> /ssh/webssh2.css 219 | - /webssh2.bundle.js -> /ssh/webssh2.bundle.js 220 | - /reauth -> /ssh/reauth 221 | - perhaps more 222 | 223 | ### Fixes 224 | - Typo in config.json.sample, thanks @wuchihsu, fixes #173 225 | 226 | ### Housekeeping 227 | - Removed irrelavant build scripts from /scripts 228 | 229 | ## 0.2.9 [2019-06-13] 230 | ### Changes 231 | - Missing require('fs') in `server/app.js` See issue [#135](../../issues/135) 232 | - Patched read-config to mitigate vulnerability in js-yaml 233 | - issue not exploitable on webssh2 implementation 234 | - patched anyway 235 | - sending my patch upstream to read-config, webssh2 package.json points to patched version in my repository https://github.com/billchurch/nodejs-read-config 236 | - See https://github.com/nodeca/js-yaml/issues/475 for more detail 237 | 238 | ## 0.2.8 [2019-05-25] 239 | ### Changes 240 | - 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. 241 | - fixes broken `npm run (build|builddev)` 242 | - update font-awesome fonts to 5.6.3 243 | - update webpack and dependancies 244 | - update xterm to 3.8.0 245 | 246 | ### Fixes 247 | - ILX workspace may not always import properly due to symbolic links (specifically ./node_modules/.bin). This is removed from the ILX package 248 | 249 | ## 0.2.7 [2018-11-11] 250 | ### Changes 251 | - `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) 252 | - **BREAKING** moved app files to /app, this may be a breaking change 253 | - Updated dockerfile for new app path 254 | - Updated app dependancies 255 | - xterm v3.8.0 256 | - https://github.com/xtermjs/xterm.js/releases/tag/3.8.0 257 | - basic-auth v2.0.1 258 | - https://github.com/jshttp/basic-auth/releases/tag/v2.0.1 259 | - express v4.16.4 260 | - https://github.com/expressjs/express/releases/tag/4.16.4 261 | - validator v10.9.0 262 | - https://github.com/chriso/validator.js/releases/tag/10.9.0 263 | - Updated dev dependancies 264 | - snazzy v8.0.0 265 | - standard v12.0.1 266 | - uglifyjs-webpack-plugin v2.0.1 267 | - ajv v6.5.5 268 | - copy-webpack-plugin v4.6.0 269 | - css-loader v1.0.1 270 | - nodemon v1.18.6 271 | - postcss-discard-comments v4.0.1 272 | - snyk v1.108.2 273 | - url-loader v1.1.2 274 | - webpack v4.25.1 275 | - webpack-cli v3.1.2 276 | 277 | ## 0.2.6 [2018-11-09] 278 | ### Changes 279 | - Reauth didn't work if intial auth presented was incorrect, (see issue #112) fixed thanks @vvalchev 280 | - Update node version supported to >=6 (PR #115) thanks @perlun 281 | - Update packages 282 | - developer dependencies 283 | 284 | ## 0.2.5 [2018-09-11] 285 | ### Added 286 | - Reauth function thanks to @vbeskrovny and @vvalchev (9bbc116) 287 | - Controlled by `config.json` option `options.allowreauth` true presents reauth dialog and false hides dialog 288 | 289 | ### Changed 290 | - `options.challengeButton` enabled 291 | - previously this configuration option did nothing, this now enables the Credentials button site-wide regardless of the `allowreplay` header value 292 | - Updated debug module to v4 293 | 294 | ## 0.2.4 [2018-07-18] 295 | ### Added 296 | - Browser title window now changes with xterm escape sequences (see http://tldp.org/HOWTO/Xterm-Title-3.html) 297 | - Added bellStyle options 298 | - `GET var`: **bellStyle** - _string_ - Style of terminal bell: ("sound"|"none"). **Default:** "sound". **Enforced Values:** "sound "none" 299 | - `config.json`: **terminal.bellStyle** - _string_ - Style of terminal bell: (sound|none). **Default:** "sound". 300 | - `workspace` folder on GITHUB for BIG-IP specific fixes/changes 301 | ### Changed 302 | - Updated xterm.js to 3.1.0 303 | - https://github.com/xtermjs/xterm.js/releases/tag/3.1.0 304 | - Default listen IP in `config.json` changed back to 127.0.0.1 305 | ### Fixed 306 | - ESC]0; is now removed from log files when using the browser-side logging feature 307 | 308 | ## 0.2.3 unreleased 309 | 310 | ### Fixed 311 | - ESC]0; is now removed from log files when using the browser-side logging feature 312 | 313 | ## 0.2.0 [2018-02-10] 314 | Mostly client (browser) related changes in this release 315 | 316 | ### Added 317 | - Menu system 318 | - Fontawesome icons 319 | - Resizing browser window sends resize events to terminal container as well as SSH session (pty) 320 | - New terminal options (config.json as well as GET vars) 321 | - terminal.cursorBlink - boolean - Cursor blinks (true), does not (false) Default: true. 322 | - terminal.scrollback - integer - Lines in the scrollback buffer. Default: 10000. 323 | - terminal.tabStopWidth - integer - Tab stops at n characters Default: 8. 324 | - New serverside (nodejs) terminal configuration options (cursorBlink, scrollback, tabStopWidth) 325 | - Logging of MRH session (unassigned if not present) 326 | - Express compression feature 327 | 328 | ### Changed 329 | - Updated xterm.js to 3.0.2 330 | - See https://github.com/xtermjs/xterm.js/releases/tag/3.0.2 331 | - See https://github.com/xtermjs/xterm.js/releases/tag/3.0.1 332 | - See https://github.com/xtermjs/xterm.js/releases/tag/3.0.0 333 | - Moved javascript events out of html into javascript 334 | - Changed asset packaging from grunt to Webpack to be inline with xterm.js direction 335 | - Moved logging and credentials buttons to menu system 336 | - Removed non-minified options (if you need to disable minification, modify webpack scripts and 'npm run build') 337 | 338 | ### Fixed 339 | - Resolved loss of terminal foucs when interacting with option buttons (Logging, etc...) 340 | 341 | ## 0.1.4 [2018-01-30] 342 | ### Changed 343 | - Moved socket and util out of folders into .js in root. 344 | - added keepaliveInterval and keepaliveCountMax config options 345 | 346 | ## 0.1.3 [2017-09-28] 347 | ### Changed 348 | - Upgrade to debug@3.1 to eliminate ReDoS in %o formatter 349 | - Upgrade Express to 4.15.5 for ReDOS 350 | - Upgrade basic-auth to v2.0 351 | ## 0.1.2 [2017-07-31] 352 | ### Added 353 | - ssh.readyTimeout option in config.json (time in ms, default 20000, 20sec) 354 | ### Changed 355 | - Updated xterm.js to 2.9.2 from 2.6.0 356 | - See https://github.com/sourcelair/xterm.js/releases/tag/2.9.2 357 | - See https://github.com/sourcelair/xterm.js/releases/tag/2.9.1 358 | - See https://github.com/sourcelair/xterm.js/releases/tag/2.9.0 359 | - See https://github.com/sourcelair/xterm.js/releases/tag/2.8.1 360 | - See https://github.com/sourcelair/xterm.js/releases/tag/2.8.0 361 | - See https://github.com/sourcelair/xterm.js/releases/tag/2.7.0 362 | - Updated ssh2 to 0.5.5 to keep current, no fixes impacting WebSSH2 363 | - ssh-streams to 0.1.19 from 0.1.16 364 | - Updated validator.js to 8.0.0, no fixes impacting WebSSH2 365 | - https://github.com/chriso/validator.js/releases/tag/8.0.0 366 | - Updated Express to 4.15.4, no fixes impacting WebSSH2 367 | - https://github.com/expressjs/express/releases/tag/4.15.4 368 | - Updated Express-session to 1.15.5, no fixes impacting WebSSH2 369 | - https://github.com/expressjs/session/releases/tag/v1.15.5 370 | - Updated Debug to 3.0.0, no fixes impacting WebSSH2 371 | - https://github.com/visionmedia/debug/releases/tag/3.0.0 372 | - Running in strict mode ('use strict';) 373 | 374 | 375 | ## 0.1.1 [2017-06-03] 376 | ### Added 377 | - `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) 378 | - morgan express middleware for logging 379 | ### Changed 380 | - Updated socket.io to 1.7.4 381 | - continued refactoring, breaking up `index.js` 382 | - revised error handling methods 383 | - revised session termination methods 384 | ### Fixed 385 | ### Removed 386 | - color console decorations from `util/index.js` 387 | - SanatizeHeaders function from `util/index.js` 388 | 389 | ## 0.1.0 [2017-05-27] 390 | ### Added 391 | - This ChangeLog.md file 392 | - Support for UTF-8 characters (thanks @bara666) 393 | - Snyk, Bithound, Travis CI 394 | - Cross platform improvements (path mappings) 395 | - Session fixup between Express and Socket.io 396 | - Session secret settings in `config.json` 397 | - env variable `DEBUG=ssh2` will put the `ssh2` module into debug mode 398 | - env variable `DEBUG=WebSSH2` will output additional debug messages for functions 399 | and events in the application (not including the ssh2 module debug) 400 | - using Grunt to pull js and css source files from other modules `npm run build` to rebuild these if changed or updated. 401 | - `useminified` option in `config.json` to enable using minified client side javascript (true) defaults to false (non-minified) 402 | - 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. 403 | - validation for host (v4,v6,fqdn,hostname), port (integer 2-65535), and header (sanitized) from URL input 404 | 405 | ### Changed 406 | - error handling in public/client.js 407 | - moved socket.io operations to their own file /socket/index.js, more changes like this to come (./socket/index.js) 408 | - all session based variables are now under the req.session.ssh property or socket.request.ssh (./index.js) 409 | - moved SSH algorithms to `config.json` and defined as a session variable (..session.ssh.algorithms) 410 | -- prep for future feature to define algorithms in header or some other method to enable separate ciphers per host 411 | - 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. 412 | - combined all css files to a single css in `./public/webssh2.css` 413 | - minified all css files to a single css in `./public/webssh2.min.css` 414 | - copied all unmodified source css and js to /public/src/css and /public/src/js respectively (for troubleshooting/etc) 415 | - sourcemaps of all minified code (in /public/src and /public/src/js) 416 | - renamed `client.htm` to `client-full.htm` 417 | - created `client-min.htm` to serve minified javascript 418 | - 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. 419 | 420 | ### Fixed 421 | - 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. 422 | - ensure ssh session is closed when a browser disconnects from the websocket 423 | - if headerBackground is changed, status background is changed to the same color (typo, fixed) 424 | 425 | ### Removed 426 | - Express Static References directly to module source directories due to concatenating and minifying js/css 427 | 428 | ## 0.0.5 - [2017-03-23] 429 | ### Added 430 | - Added experimental support for logging (see Readme) 431 | 432 | ### Fixed 433 | - 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. 434 | 435 | ## 0.0.4 - [2017-03-23] 436 | ### Added 437 | - Set default terminal to xterm-color 438 | - Mouse event support 439 | - New config option, config.ssh.term to set terminal 440 | 441 | ### Changed 442 | - Update to Xterm.js 2.4.0 443 | - Minor code formatting cleanup 444 | 445 | ## 0.0.3 - [2017-02-16] 446 | ### Changed 447 | - Update xterm to latest (2.3.0) 448 | ### Fixed 449 | - Fixed misspelled config.ssh.port property 450 | 451 | ## 0.0.2 - [2017-02-01] 452 | ### Changed 453 | - Moving terminal emulation to xterm.js 454 | - updating module version dependencies 455 | 456 | ### Fixed 457 | - Fixed issue with banners not being displayed properly from UNIX hosts when only lf is used 458 | 459 | ## 0.0.1 - [2016-06-28] 460 | ### Added 461 | - Initial proof of concept and release. For historical purposes only. 462 | -------------------------------------------------------------------------------- /app/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Bill Church 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /app/bun.lockb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/billchurch/webssh2/9c0ba04b31e92b7ed20e5c3509b5cbcc5447f565/app/bun.lockb -------------------------------------------------------------------------------- /app/client/public/client.htm: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | WebSSH2 5 | 8 | 9 | 10 | 11 | 12 |
13 | 14 |
15 |
16 | 25 | 26 |
27 |
28 |
29 |
30 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /app/client/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/billchurch/webssh2/9c0ba04b31e92b7ed20e5c3509b5cbcc5447f565/app/client/public/favicon.ico -------------------------------------------------------------------------------- /app/client/public/webssh2.css: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2014 The xterm.js authors. All rights reserved. 3 | * Copyright (c) 2012-2013, Christopher Jeffrey (MIT License) 4 | * https://github.com/chjj/term.js 5 | * @license MIT 6 | * 7 | * Permission is hereby granted, free of charge, to any person obtaining a copy 8 | * of this software and associated documentation files (the "Software"), to deal 9 | * in the Software without restriction, including without limitation the rights 10 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | * copies of the Software, and to permit persons to whom the Software is 12 | * furnished to do so, subject to the following conditions: 13 | * 14 | * The above copyright notice and this permission notice shall be included in 15 | * all copies or substantial portions of the Software. 16 | * 17 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 23 | * THE SOFTWARE. 24 | * 25 | * Originally forked from (with the author's permission): 26 | * Fabrice Bellard's javascript vt100 for jslinux: 27 | * http://bellard.org/jslinux/ 28 | * Copyright (c) 2011 Fabrice Bellard 29 | * The original design remains. The terminal itself 30 | * has been extended to include xterm CSI codes, among 31 | * other features. 32 | */ 33 | 34 | /** 35 | * Default styles for xterm.js 36 | */ 37 | 38 | .xterm { 39 | cursor: text; 40 | position: relative; 41 | user-select: none; 42 | -ms-user-select: none; 43 | -webkit-user-select: none; 44 | } 45 | 46 | .xterm.focus, 47 | .xterm:focus { 48 | outline: none; 49 | } 50 | 51 | .xterm .xterm-helpers { 52 | position: absolute; 53 | top: 0; 54 | /** 55 | * The z-index of the helpers must be higher than the canvases in order for 56 | * IMEs to appear on top. 57 | */ 58 | z-index: 5; 59 | } 60 | 61 | .xterm .xterm-helper-textarea { 62 | padding: 0; 63 | border: 0; 64 | margin: 0; 65 | /* Move textarea out of the screen to the far left, so that the cursor is not visible */ 66 | position: absolute; 67 | opacity: 0; 68 | left: -9999em; 69 | top: 0; 70 | width: 0; 71 | height: 0; 72 | z-index: -5; 73 | /** Prevent wrapping so the IME appears against the textarea at the correct position */ 74 | white-space: nowrap; 75 | overflow: hidden; 76 | resize: none; 77 | } 78 | 79 | .xterm .composition-view { 80 | /* TODO: Composition position got messed up somewhere */ 81 | background: #000; 82 | color: #FFF; 83 | display: none; 84 | position: absolute; 85 | white-space: nowrap; 86 | z-index: 1; 87 | } 88 | 89 | .xterm .composition-view.active { 90 | display: block; 91 | } 92 | 93 | .xterm .xterm-viewport { 94 | /* On OS X this is required in order for the scroll bar to appear fully opaque */ 95 | background-color: #000; 96 | overflow-y: scroll; 97 | cursor: default; 98 | position: absolute; 99 | right: 0; 100 | left: 0; 101 | top: 0; 102 | bottom: 0; 103 | } 104 | 105 | .xterm .xterm-screen { 106 | position: relative; 107 | } 108 | 109 | .xterm .xterm-screen canvas { 110 | position: absolute; 111 | left: 0; 112 | top: 0; 113 | } 114 | 115 | .xterm .xterm-scroll-area { 116 | visibility: hidden; 117 | } 118 | 119 | .xterm-char-measure-element { 120 | display: inline-block; 121 | visibility: hidden; 122 | position: absolute; 123 | top: 0; 124 | left: -9999em; 125 | line-height: normal; 126 | } 127 | 128 | .xterm.enable-mouse-events { 129 | /* When mouse events are enabled (eg. tmux), revert to the standard pointer cursor */ 130 | cursor: default; 131 | } 132 | 133 | .xterm.xterm-cursor-pointer, 134 | .xterm .xterm-cursor-pointer { 135 | cursor: pointer; 136 | } 137 | 138 | .xterm.column-select.focus { 139 | /* Column selection mode */ 140 | cursor: crosshair; 141 | } 142 | 143 | .xterm .xterm-accessibility:not(.debug), 144 | .xterm .xterm-message { 145 | position: absolute; 146 | left: 0; 147 | top: 0; 148 | bottom: 0; 149 | right: 0; 150 | z-index: 10; 151 | color: transparent; 152 | pointer-events: none; 153 | } 154 | 155 | .xterm .xterm-accessibility-tree:not(.debug) *::selection { 156 | color: transparent; 157 | } 158 | 159 | .xterm .xterm-accessibility-tree { 160 | user-select: text; 161 | white-space: pre; 162 | } 163 | 164 | .xterm .live-region { 165 | position: absolute; 166 | left: -9999px; 167 | width: 1px; 168 | height: 1px; 169 | overflow: hidden; 170 | } 171 | 172 | .xterm-dim { 173 | /* Dim should not apply to background, so the opacity of the foreground color is applied 174 | * explicitly in the generated class and reset to 1 here */ 175 | opacity: 1 !important; 176 | } 177 | 178 | .xterm-underline-1 { text-decoration: underline; } 179 | .xterm-underline-2 { text-decoration: double underline; } 180 | .xterm-underline-3 { text-decoration: wavy underline; } 181 | .xterm-underline-4 { text-decoration: dotted underline; } 182 | .xterm-underline-5 { text-decoration: dashed underline; } 183 | 184 | .xterm-overline { 185 | text-decoration: overline; 186 | } 187 | 188 | .xterm-overline.xterm-underline-1 { text-decoration: overline underline; } 189 | .xterm-overline.xterm-underline-2 { text-decoration: overline double underline; } 190 | .xterm-overline.xterm-underline-3 { text-decoration: overline wavy underline; } 191 | .xterm-overline.xterm-underline-4 { text-decoration: overline dotted underline; } 192 | .xterm-overline.xterm-underline-5 { text-decoration: overline dashed underline; } 193 | 194 | .xterm-strikethrough { 195 | text-decoration: line-through; 196 | } 197 | 198 | .xterm-screen .xterm-decoration-container .xterm-decoration { 199 | z-index: 6; 200 | position: absolute; 201 | } 202 | 203 | .xterm-screen .xterm-decoration-container .xterm-decoration.xterm-decoration-top-layer { 204 | z-index: 7; 205 | } 206 | 207 | .xterm-decoration-overview-ruler { 208 | z-index: 8; 209 | position: absolute; 210 | top: 0; 211 | right: 0; 212 | pointer-events: none; 213 | } 214 | 215 | .xterm-decoration-top { 216 | z-index: 2; 217 | position: relative; 218 | } 219 | 220 | body, html { 221 | font-family: helvetica, sans-serif, arial; 222 | font-size: 1em; 223 | background-color: rgb(0, 0, 0); 224 | color: rgb(240, 240, 240); 225 | height: 100%; 226 | margin: 0; 227 | } 228 | #header { 229 | color: rgb(240, 240, 240); 230 | background-color: rgb(0, 128, 0); 231 | width: 100%; 232 | border-color: white; 233 | border-style: none none solid none; 234 | border-width: 1px; 235 | text-align: center; 236 | flex: 0 1 auto; 237 | z-index: 99; 238 | height:19px; 239 | display: none; 240 | } 241 | .box { 242 | display: block; 243 | height: 100%; 244 | } 245 | #terminal-container { 246 | display: block; 247 | width: calc(100% - 1px); 248 | margin: 0 auto; 249 | padding: 2px; 250 | height: calc(100% - 19px); 251 | } 252 | #terminal-container .terminal { 253 | background-color: #000000; 254 | color: #fafafa; 255 | padding: 2px; 256 | height: calc(100% - 19px); 257 | } 258 | #terminal-container .terminal:focus .terminal-cursor { 259 | background-color: #fafafa; 260 | } 261 | #bottomdiv { 262 | position: fixed; 263 | left: 0; 264 | bottom: 0; 265 | width: 100%; 266 | background-color: rgb(50, 50, 50); 267 | border-color: white; 268 | border-style: solid none none none; 269 | border-width: 1px; 270 | z-index: 99; 271 | height: 19px; 272 | } 273 | #footer { 274 | display: inline-block; 275 | color: rgb(240, 240, 240); 276 | background-color: rgb(50, 50, 50); 277 | padding-left: 5px; 278 | padding-right: 5px; 279 | border-color: white; 280 | border-style: none none none solid; 281 | border-width: 1px; 282 | text-align: left; 283 | } 284 | #status { 285 | display: inline-block; 286 | color: rgb(240, 240, 240); 287 | background-color: rgb(50, 50, 50); 288 | padding-left: 10px; 289 | padding-right: 10px; 290 | border-color: white; 291 | border-style: none solid none solid; 292 | border-width: 1px; 293 | text-align: left; 294 | z-index: 100; 295 | } 296 | #countdown { 297 | display: none; 298 | color: rgb(240, 240, 240); 299 | background-color: rgb(50, 50, 50); 300 | padding-left: 10px; 301 | padding-right: 10px; 302 | border-color: white; 303 | border-style: none solid none solid; 304 | border-width: 1px; 305 | text-align: left; 306 | z-index: 100; 307 | } 308 | #countdown.active { 309 | display: inline-block; 310 | animation: countdown infinite alternate 200ms; 311 | } 312 | @keyframes countdown { 313 | from { 314 | background-color: rgb(255, 255, 0); 315 | } 316 | to { 317 | background-color: inherit; 318 | } 319 | } 320 | #menu { 321 | display: inline-block; 322 | font-size: 16px; 323 | color: rgb(255, 255, 255); 324 | padding-left: 10px; 325 | z-index: 100; 326 | } 327 | #menu:hover .dropup-content { 328 | display: block; 329 | } 330 | #logBtn, #credentialsBtn, #reauthBtn { 331 | color: #000; 332 | } 333 | 334 | .dropup { 335 | position: relative; 336 | display: inline-block; 337 | cursor: pointer; 338 | } 339 | .dropup-content { 340 | display: none; 341 | position: absolute; 342 | background-color: #f1f1f1; 343 | font-size: 16px; 344 | min-width: 160px; 345 | bottom: 18px; 346 | z-index: 101; 347 | } 348 | .dropup-content a { 349 | color: #777; 350 | padding: 12px 16px; 351 | text-decoration: none; 352 | display: block; 353 | } 354 | .dropup-content a:hover { 355 | background-color: #ccc 356 | } 357 | .dropup:hover .dropup-content { 358 | display: block; 359 | } 360 | .dropup:active .dropup-content { 361 | display: block; 362 | } 363 | .dropup:hover .dropbtn { 364 | background-color: #3e8e41; 365 | } 366 | 367 | -------------------------------------------------------------------------------- /app/client/src/README.md: -------------------------------------------------------------------------------- 1 | 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. 2 | -------------------------------------------------------------------------------- /app/client/src/client.htm: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | WebSSH2 5 | 8 | 9 | 10 | 11 | 12 |
13 | 14 |
15 |
16 | 25 | 26 |
27 |
28 |
29 |
30 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /app/client/src/css/style.css: -------------------------------------------------------------------------------- 1 | body, html { 2 | font-family: helvetica, sans-serif, arial; 3 | font-size: 1em; 4 | background-color: rgb(0, 0, 0); 5 | color: rgb(240, 240, 240); 6 | height: 100%; 7 | margin: 0; 8 | } 9 | #header { 10 | color: rgb(240, 240, 240); 11 | background-color: rgb(0, 128, 0); 12 | width: 100%; 13 | border-color: white; 14 | border-style: none none solid none; 15 | border-width: 1px; 16 | text-align: center; 17 | flex: 0 1 auto; 18 | z-index: 99; 19 | height:19px; 20 | display: none; 21 | } 22 | .box { 23 | display: block; 24 | height: 100%; 25 | } 26 | #terminal-container { 27 | display: block; 28 | width: calc(100% - 1px); 29 | margin: 0 auto; 30 | padding: 2px; 31 | height: calc(100% - 19px); 32 | } 33 | #terminal-container .terminal { 34 | background-color: #000000; 35 | color: #fafafa; 36 | padding: 2px; 37 | height: calc(100% - 19px); 38 | } 39 | #terminal-container .terminal:focus .terminal-cursor { 40 | background-color: #fafafa; 41 | } 42 | #bottomdiv { 43 | position: fixed; 44 | left: 0; 45 | bottom: 0; 46 | width: 100%; 47 | background-color: rgb(50, 50, 50); 48 | border-color: white; 49 | border-style: solid none none none; 50 | border-width: 1px; 51 | z-index: 99; 52 | height: 19px; 53 | } 54 | #footer { 55 | display: inline-block; 56 | color: rgb(240, 240, 240); 57 | background-color: rgb(50, 50, 50); 58 | padding-left: 5px; 59 | padding-right: 5px; 60 | border-color: white; 61 | border-style: none none none solid; 62 | border-width: 1px; 63 | text-align: left; 64 | } 65 | #status { 66 | display: inline-block; 67 | color: rgb(240, 240, 240); 68 | background-color: rgb(50, 50, 50); 69 | padding-left: 10px; 70 | padding-right: 10px; 71 | border-color: white; 72 | border-style: none solid none solid; 73 | border-width: 1px; 74 | text-align: left; 75 | z-index: 100; 76 | } 77 | #countdown { 78 | display: none; 79 | color: rgb(240, 240, 240); 80 | background-color: rgb(50, 50, 50); 81 | padding-left: 10px; 82 | padding-right: 10px; 83 | border-color: white; 84 | border-style: none solid none solid; 85 | border-width: 1px; 86 | text-align: left; 87 | z-index: 100; 88 | } 89 | #countdown.active { 90 | display: inline-block; 91 | animation: countdown infinite alternate 200ms; 92 | } 93 | @keyframes countdown { 94 | from { 95 | background-color: rgb(255, 255, 0); 96 | } 97 | to { 98 | background-color: inherit; 99 | } 100 | } 101 | #menu { 102 | display: inline-block; 103 | font-size: 16px; 104 | color: rgb(255, 255, 255); 105 | padding-left: 10px; 106 | z-index: 100; 107 | } 108 | #menu:hover .dropup-content { 109 | display: block; 110 | } 111 | #logBtn, #credentialsBtn, #reauthBtn { 112 | color: #000; 113 | } 114 | 115 | .dropup { 116 | position: relative; 117 | display: inline-block; 118 | cursor: pointer; 119 | } 120 | .dropup-content { 121 | display: none; 122 | position: absolute; 123 | background-color: #f1f1f1; 124 | font-size: 16px; 125 | min-width: 160px; 126 | bottom: 18px; 127 | z-index: 101; 128 | } 129 | .dropup-content a { 130 | color: #777; 131 | padding: 12px 16px; 132 | text-decoration: none; 133 | display: block; 134 | } 135 | .dropup-content a:hover { 136 | background-color: #ccc 137 | } 138 | .dropup:hover .dropup-content { 139 | display: block; 140 | } 141 | .dropup:active .dropup-content { 142 | display: block; 143 | } 144 | .dropup:hover .dropbtn { 145 | background-color: #3e8e41; 146 | } 147 | -------------------------------------------------------------------------------- /app/client/src/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/billchurch/webssh2/9c0ba04b31e92b7ed20e5c3509b5cbcc5447f565/app/client/src/favicon.ico -------------------------------------------------------------------------------- /app/client/src/js/index.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable import/no-extraneous-dependencies */ 2 | import { io } from 'socket.io-client'; 3 | import { Terminal } from '@xterm/xterm'; 4 | import { FitAddon } from '@xterm/addon-fit'; 5 | import { library, dom } from '@fortawesome/fontawesome-svg-core'; 6 | import { faBars, faClipboard, faDownload, faKey, faCog } from '@fortawesome/free-solid-svg-icons'; 7 | 8 | library.add(faBars, faClipboard, faDownload, faKey, faCog); 9 | dom.watch(); 10 | 11 | const debug = require('debug')('WebSSH2'); 12 | require('@xterm/xterm/css/xterm.css'); 13 | require('../css/style.css'); 14 | 15 | /* global Blob, logBtn, credentialsBtn, reauthBtn, downloadLogBtn */ // eslint-disable-line 16 | let sessionLogEnable = false; 17 | let loggedData = false; 18 | let allowreplay = false; 19 | let allowreauth = false; 20 | let sessionLog: string; 21 | let sessionFooter: any; 22 | let logDate: { 23 | getFullYear: () => any; 24 | getMonth: () => number; 25 | getDate: () => any; 26 | getHours: () => any; 27 | getMinutes: () => any; 28 | getSeconds: () => any; 29 | }; 30 | let currentDate: Date; 31 | let myFile: string; 32 | let errorExists: boolean; 33 | const term = new Terminal(); 34 | // DOM properties 35 | const logBtn = document.getElementById('logBtn'); 36 | const credentialsBtn = document.getElementById('credentialsBtn'); 37 | const reauthBtn = document.getElementById('reauthBtn'); 38 | const downloadLogBtn = document.getElementById('downloadLogBtn'); 39 | const status = document.getElementById('status'); 40 | const header = document.getElementById('header'); 41 | const footer = document.getElementById('footer'); 42 | const countdown = document.getElementById('countdown'); 43 | const fitAddon = new FitAddon(); 44 | const terminalContainer = document.getElementById('terminal-container'); 45 | term.loadAddon(fitAddon); 46 | term.open(terminalContainer); 47 | term.focus(); 48 | fitAddon.fit(); 49 | 50 | const socket = io({ 51 | path: '/ssh/socket.io', 52 | }); 53 | 54 | // reauthenticate 55 | function reauthSession () { // eslint-disable-line 56 | debug('re-authenticating'); 57 | socket.emit('control', 'reauth'); 58 | window.location.href = '/ssh/reauth'; 59 | return false; 60 | } 61 | 62 | // cross browser method to "download" an element to the local system 63 | // used for our client-side logging feature 64 | function downloadLog () { // eslint-disable-line 65 | if (loggedData === true) { 66 | myFile = `WebSSH2-${logDate.getFullYear()}${ 67 | logDate.getMonth() + 1 68 | }${logDate.getDate()}_${logDate.getHours()}${logDate.getMinutes()}${logDate.getSeconds()}.log`; 69 | // regex should eliminate escape sequences from being logged. 70 | const blob = new Blob( 71 | [ 72 | sessionLog.replace( 73 | // eslint-disable-next-line no-control-regex 74 | /[\u001b\u009b][[\]()#;?]*(?:\d{1,4}(?:;\d{0,4})*)?[0-9A-ORZcf-nqry=><;]/g, 75 | '' 76 | ), 77 | ], 78 | { 79 | // eslint-disable-line no-control-regex 80 | type: 'text/plain', 81 | } 82 | ); 83 | const elem = window.document.createElement('a'); 84 | elem.href = window.URL.createObjectURL(blob); 85 | elem.download = myFile; 86 | document.body.appendChild(elem); 87 | elem.click(); 88 | document.body.removeChild(elem); 89 | } 90 | term.focus(); 91 | } 92 | // Set variable to toggle log data from client/server to a varialble 93 | // for later download 94 | function toggleLog () { // eslint-disable-line 95 | if (sessionLogEnable === true) { 96 | sessionLogEnable = false; 97 | loggedData = true; 98 | logBtn.innerHTML = ' Start Log'; 99 | currentDate = new Date(); 100 | sessionLog = `${sessionLog}\r\n\r\nLog End for ${sessionFooter}: ${currentDate.getFullYear()}/${ 101 | currentDate.getMonth() + 1 102 | }/${currentDate.getDate()} @ ${currentDate.getHours()}:${currentDate.getMinutes()}:${currentDate.getSeconds()}\r\n`; 103 | logDate = currentDate; 104 | term.focus(); 105 | return false; 106 | } 107 | sessionLogEnable = true; 108 | loggedData = true; 109 | logBtn.innerHTML = ' Stop Log'; 110 | downloadLogBtn.style.color = '#000'; 111 | downloadLogBtn.addEventListener('click', downloadLog); 112 | currentDate = new Date(); 113 | sessionLog = `Log Start for ${sessionFooter}: ${currentDate.getFullYear()}/${ 114 | currentDate.getMonth() + 1 115 | }/${currentDate.getDate()} @ ${currentDate.getHours()}:${currentDate.getMinutes()}:${currentDate.getSeconds()}\r\n\r\n`; 116 | logDate = currentDate; 117 | term.focus(); 118 | return false; 119 | } 120 | 121 | // replay password to server, requires 122 | function replayCredentials () { // eslint-disable-line 123 | socket.emit('control', 'replayCredentials'); 124 | debug(`control: replayCredentials`); 125 | term.focus(); 126 | return false; 127 | } 128 | 129 | // draw/re-draw menu and reattach listeners 130 | // when dom is changed, listeners are abandonded 131 | function drawMenu() { 132 | logBtn.addEventListener('click', toggleLog); 133 | if (allowreauth) { 134 | reauthBtn.addEventListener('click', reauthSession); 135 | reauthBtn.style.display = 'block'; 136 | } 137 | if (allowreplay) { 138 | credentialsBtn.addEventListener('click', replayCredentials); 139 | credentialsBtn.style.display = 'block'; 140 | } 141 | if (loggedData) { 142 | downloadLogBtn.addEventListener('click', downloadLog); 143 | downloadLogBtn.style.display = 'block'; 144 | } 145 | } 146 | 147 | function resizeScreen() { 148 | fitAddon.fit(); 149 | socket.emit('resize', { cols: term.cols, rows: term.rows }); 150 | debug(`resize: ${JSON.stringify({ cols: term.cols, rows: term.rows })}`); 151 | } 152 | 153 | window.addEventListener('resize', resizeScreen, false); 154 | 155 | term.onData((data) => { 156 | socket.emit('data', data); 157 | }); 158 | 159 | socket.on('data', (data: string | Uint8Array) => { 160 | term.write(data); 161 | if (sessionLogEnable) { 162 | sessionLog += data; 163 | } 164 | }); 165 | 166 | socket.on('connect', () => { 167 | socket.emit('geometry', term.cols, term.rows); 168 | debug(`geometry: ${term.cols}, ${term.rows}`); 169 | }); 170 | 171 | socket.on( 172 | 'setTerminalOpts', 173 | (data: { 174 | cursorBlink: boolean; 175 | scrollback: number; 176 | tabStopWidth: number; 177 | bellStyle: 'none' | 'sound'; 178 | fontSize: number; 179 | fontFamily: string; 180 | letterSpacing: number; 181 | lineHeight: number; 182 | }) => { 183 | term.options = data; 184 | } 185 | ); 186 | 187 | socket.on('title', (data: string) => { 188 | document.title = data; 189 | }); 190 | 191 | socket.on('menu', () => { 192 | drawMenu(); 193 | }); 194 | 195 | socket.on('status', (data: string) => { 196 | status.innerHTML = data; 197 | }); 198 | 199 | socket.on('ssherror', (data: string) => { 200 | status.innerHTML = data; 201 | status.style.backgroundColor = 'red'; 202 | errorExists = true; 203 | }); 204 | 205 | socket.on('headerBackground', (data: string) => { 206 | header.style.backgroundColor = data; 207 | }); 208 | 209 | socket.on('header', (data: string) => { 210 | if (data) { 211 | header.innerHTML = data; 212 | header.style.display = 'block'; 213 | // header is 19px and footer is 19px, recaculate new terminal-container and resize 214 | terminalContainer.style.height = 'calc(100% - 38px)'; 215 | resizeScreen(); 216 | } 217 | }); 218 | 219 | socket.on('footer', (data: string) => { 220 | sessionFooter = data; 221 | footer.innerHTML = data; 222 | }); 223 | 224 | socket.on('statusBackground', (data: string) => { 225 | status.style.backgroundColor = data; 226 | }); 227 | 228 | socket.on('allowreplay', (data: boolean) => { 229 | if (data === true) { 230 | debug(`allowreplay: ${data}`); 231 | allowreplay = true; 232 | drawMenu(); 233 | } else { 234 | allowreplay = false; 235 | debug(`allowreplay: ${data}`); 236 | } 237 | }); 238 | 239 | socket.on('allowreauth', (data: boolean) => { 240 | if (data === true) { 241 | debug(`allowreauth: ${data}`); 242 | allowreauth = true; 243 | drawMenu(); 244 | } else { 245 | allowreauth = false; 246 | debug(`allowreauth: ${data}`); 247 | } 248 | }); 249 | 250 | socket.on('disconnect', (err: any) => { 251 | if (!errorExists) { 252 | status.style.backgroundColor = 'red'; 253 | status.innerHTML = `WEBSOCKET SERVER DISCONNECTED: ${err}`; 254 | } 255 | socket.io.reconnection(false); 256 | countdown.classList.remove('active'); 257 | }); 258 | 259 | socket.on('error', (err: any) => { 260 | if (!errorExists) { 261 | status.style.backgroundColor = 'red'; 262 | status.innerHTML = `ERROR: ${err}`; 263 | } 264 | }); 265 | 266 | socket.on('reauth', () => { 267 | if (allowreauth) { 268 | reauthSession(); 269 | } 270 | }); 271 | 272 | // safe shutdown 273 | let hasCountdownStarted = false; 274 | 275 | socket.on('shutdownCountdownUpdate', (remainingSeconds: any) => { 276 | if (!hasCountdownStarted) { 277 | countdown.classList.add('active'); 278 | hasCountdownStarted = true; 279 | } 280 | countdown.innerText = `Shutting down in ${remainingSeconds}s`; 281 | }); 282 | 283 | term.onTitleChange((title) => { 284 | document.title = title; 285 | }); 286 | -------------------------------------------------------------------------------- /app/client/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "outDir": "./built", 4 | "allowJs": true, 5 | "target": "es6", 6 | "moduleResolution": "node" 7 | }, 8 | "include": ["./src/**/*"], 9 | } -------------------------------------------------------------------------------- /app/config.json.sample: -------------------------------------------------------------------------------- 1 | { 2 | "listen": { 3 | "ip": "0.0.0.0", 4 | "port": 2224 5 | }, 6 | "socketio": { 7 | "serveClient": false, 8 | "path": "/ssh/socket.io", 9 | "origins": ["localhost:2222"], 10 | }, 11 | "user": { 12 | "name": null, 13 | "password": null, 14 | "privatekey": null, 15 | "overridebasic": false 16 | }, 17 | "ssh": { 18 | "host": null, 19 | "port": 22, 20 | "localAddress": null, 21 | "localPort": null, 22 | "term": "xterm-color", 23 | "readyTimeout": 20000, 24 | "keepaliveInterval": 120000, 25 | "keepaliveCountMax": 10, 26 | "allowedSubnets": [] 27 | }, 28 | "terminal": { 29 | "cursorBlink": true, 30 | "scrollback": 10000, 31 | "tabStopWidth": 8, 32 | "bellStyle": "sound", 33 | "fontSize": 14 34 | }, 35 | "header": { 36 | "text": null, 37 | "background": "green" 38 | }, 39 | "session": { 40 | "name": "WebSSH2", 41 | "secret": "mysecret" 42 | }, 43 | "options": { 44 | "challengeButton": true, 45 | "allowreauth": false 46 | }, 47 | "algorithms": { 48 | "kex": [ 49 | "ecdh-sha2-nistp256", 50 | "ecdh-sha2-nistp384", 51 | "ecdh-sha2-nistp521", 52 | "diffie-hellman-group-exchange-sha256", 53 | "diffie-hellman-group14-sha1" 54 | ], 55 | "cipher": [ 56 | "aes128-ctr", 57 | "aes192-ctr", 58 | "aes256-ctr", 59 | "aes128-gcm", 60 | "aes128-gcm@openssh.com", 61 | "aes256-gcm", 62 | "aes256-gcm@openssh.com", 63 | "aes256-cbc" 64 | ], 65 | "hmac": [ 66 | "hmac-sha2-256", 67 | "hmac-sha2-512", 68 | "hmac-sha1" 69 | ], 70 | "compress": [ 71 | "none", 72 | "zlib@openssh.com", 73 | "zlib" 74 | ] 75 | }, 76 | "serverlog": { 77 | "client": false, 78 | "server": false 79 | }, 80 | "accesslog": false, 81 | "verify": false, 82 | "safeShutdownDuration": 300 83 | } 84 | -------------------------------------------------------------------------------- /app/index.js: -------------------------------------------------------------------------------- 1 | /* eslint no-console: ["error", { allow: ["warn", "error"] }] */ 2 | /* jshint esversion: 6, asi: true, node: true */ 3 | /* 4 | * index.js 5 | * 6 | * WebSSH2 - Web to SSH2 gateway 7 | * Bill Church - https://github.com/billchurch/WebSSH2 - May 2017 8 | * See LICENSE file 9 | * 10 | * test change 11 | */ 12 | 13 | const { config } = require('./server/app'); 14 | const { server } = require('./server/app'); 15 | 16 | server.listen({ host: config.listen.ip, port: config.listen.port }); 17 | 18 | // eslint-disable-next-line no-console 19 | console.log(`WebSSH2 service listening on ${config.listen.ip}:${config.listen.port}`); 20 | 21 | server.on('error', (err) => { 22 | if (err.code === 'EADDRINUSE') { 23 | config.listen.port += 1; 24 | console.warn(`WebSSH2 Address in use, retrying on port ${config.listen.port}`); 25 | setTimeout(() => { 26 | server.listen(config.listen.port); 27 | }, 250); 28 | } else { 29 | // eslint-disable-next-line no-console 30 | console.log(`WebSSH2 server.listen ERROR: ${err.code}`); 31 | } 32 | }); 33 | -------------------------------------------------------------------------------- /app/jsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "ESNext", 4 | "moduleResolution": "Node", 5 | "target": "ES2020", 6 | "jsx": "preserve", 7 | "strictFunctionTypes": true 8 | }, 9 | "exclude": [ 10 | "node_modules", 11 | "**/node_modules/*" 12 | ], 13 | "include": [ 14 | "server/*.js", 15 | "index.js" 16 | ] 17 | } -------------------------------------------------------------------------------- /app/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "webssh2", 3 | "version": "0.6.0-pre-1", 4 | "ignore": [ 5 | ".gitignore" 6 | ], 7 | "bin": "./index.js", 8 | "description": "A Websocket to SSH2 gateway using term.js, socket.io, ssh2, and express", 9 | "homepage": "https://github.com/billchurch/WebSSH2", 10 | "keywords": [ 11 | "ssh", 12 | "webssh", 13 | "terminal", 14 | "webterminal" 15 | ], 16 | "license": "SEE LICENSE IN FILE - LICENSE", 17 | "private": false, 18 | "repository": { 19 | "type": "git", 20 | "url": "git+https://github.com/billchurch/WebSSH2.git" 21 | }, 22 | "contributors": [ 23 | { 24 | "name": "Bill Church", 25 | "email": "wmchurch@gmail.com" 26 | } 27 | ], 28 | "engines": { 29 | "node": ">= 14" 30 | }, 31 | "bugs": { 32 | "url": "https://github.com/billchurch/WebSSH2/issues" 33 | }, 34 | "dependencies": { 35 | "basic-auth": "~2.0.1", 36 | "cidr-matcher": "^2.1.1", 37 | "debug": "^4.3.4", 38 | "express": "^4.19.2", 39 | "express-session": "^1.18.0", 40 | "morgan": "~1.10.0", 41 | "read-config-ng": "^3.0.7", 42 | "serve-favicon": "^2.5.0", 43 | "socket.io": "^4.7.5", 44 | "ssh2": "^1.15.0", 45 | "validator": "^13.11.0", 46 | "winston": "^3.13.0" 47 | }, 48 | "scripts": { 49 | "start": "node index.js", 50 | "build": "webpack --progress --config scripts/webpack.prod.js", 51 | "builddev": "webpack --progress --config scripts/webpack.dev.js", 52 | "analyze": "webpack --json --config scripts/webpack.prod.js | webpack-bundle-size-analyzer", 53 | "test": "snyk test", 54 | "watch": "nodemon index.js", 55 | "cleanmac": "find . -name '.DS_Store' -type f -delete", 56 | "release": "standard-version" 57 | }, 58 | "devDependencies": { 59 | "@fortawesome/fontawesome-svg-core": "^6.5.2", 60 | "@fortawesome/free-solid-svg-icons": "^6.5.2", 61 | "@typescript-eslint/eslint-plugin": "^7.7.1", 62 | "@typescript-eslint/parser": "^7.7.1", 63 | "@xterm/addon-fit": "^0.10.0", 64 | "@xterm/xterm": "^5.5.0", 65 | "clean-webpack-plugin": "^4.0.0", 66 | "copy-webpack-plugin": "^12.0.2", 67 | "css-loader": "^7.1.1", 68 | "eslint": "^8.56.0", 69 | "eslint-config-airbnb-base": "^15.0.0", 70 | "eslint-config-prettier": "^9.1.0", 71 | "eslint-plugin-import": "^2.29.1", 72 | "eslint-plugin-prettier": "^5.1.3", 73 | "mini-css-extract-plugin": "^2.9.0", 74 | "nodaemon": "0.0.5", 75 | "npm-check-updates": "^16.14.20", 76 | "prettier": "^3.2.5", 77 | "snazzy": "^9.0.0", 78 | "snyk": "^1.1290.0", 79 | "socket.io-client": "^4.7.5", 80 | "source-map-loader": "^5.0.0", 81 | "standard-version": "^9.5.0", 82 | "terser-webpack-plugin": "^5.3.10", 83 | "ts-loader": "^9.5.1", 84 | "typescript": "^5.4.5", 85 | "webpack": "^5.91.0", 86 | "webpack-cli": "^5.1.4", 87 | "webpack-merge": "^5.10.0" 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /app/scripts/webpack.common.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable import/no-extraneous-dependencies */ 2 | const path = require('path'); 3 | const { CleanWebpackPlugin } = require('clean-webpack-plugin'); 4 | const CopyWebpackPlugin = require('copy-webpack-plugin'); 5 | const MiniCssExtractPlugin = require('mini-css-extract-plugin'); 6 | 7 | module.exports = { 8 | context: path.resolve('__dirname', '../'), 9 | resolve: { 10 | // Add '.ts' and '.tsx' as resolvable extensions. 11 | extensions: ['', '.webpack.js', '.web.js', '.ts', '.tsx', '.js'], 12 | }, 13 | entry: { 14 | webssh2: './client/src/js/index.ts', 15 | }, 16 | plugins: [ 17 | new CleanWebpackPlugin(), 18 | new CopyWebpackPlugin({ 19 | patterns: ['./client/src/client.htm', './client/src/favicon.ico'], 20 | }), 21 | new MiniCssExtractPlugin(), 22 | ], 23 | output: { 24 | filename: '[name].bundle.js', 25 | path: path.resolve(__dirname, '../client/public'), 26 | }, 27 | module: { 28 | rules: [ 29 | { 30 | test: /\.css$/, 31 | use: [MiniCssExtractPlugin.loader, 'css-loader'], 32 | }, 33 | // All files with a '.ts' or '.tsx' extension will be handled by 'ts-loader'. 34 | { 35 | test: /\.tsx?$/, 36 | loader: 'ts-loader', 37 | }, 38 | // All output '.js' files will have any sourcemaps re-processed by 'source-map-loader'. 39 | { 40 | test: /\.js$/, 41 | loader: 'source-map-loader', 42 | }, 43 | ], 44 | }, 45 | }; 46 | -------------------------------------------------------------------------------- /app/scripts/webpack.dev.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable import/no-extraneous-dependencies */ 2 | const merge = require('webpack-merge'); 3 | const common = require('./webpack.common.js'); 4 | 5 | module.exports = merge(common, { 6 | devtool: 'inline-source-map', 7 | devServer: { 8 | contentBase: '../client/public', 9 | }, 10 | }); 11 | -------------------------------------------------------------------------------- /app/scripts/webpack.prod.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable import/no-extraneous-dependencies */ 2 | const TerserPlugin = require('terser-webpack-plugin'); 3 | const { merge } = require('webpack-merge'); 4 | const common = require('./webpack.common.js'); 5 | 6 | module.exports = merge(common, { 7 | mode: 'production', 8 | optimization: { 9 | minimize: true, 10 | minimizer: [ 11 | new TerserPlugin({ 12 | terserOptions: { 13 | ie8: false, 14 | safari10: false, 15 | }, 16 | }), 17 | ], 18 | }, 19 | }); 20 | -------------------------------------------------------------------------------- /app/server/app.js: -------------------------------------------------------------------------------- 1 | /* jshint esversion: 6, asi: true, node: true */ 2 | /* eslint no-unused-expressions: ["error", { "allowShortCircuit": true, "allowTernary": true }], 3 | no-console: ["error", { allow: ["warn", "error", "info"] }] */ 4 | // app.js 5 | 6 | // eslint-disable-next-line import/order 7 | const config = require('./config'); 8 | const path = require('path'); 9 | 10 | const nodeRoot = path.dirname(require.main.filename); 11 | const publicPath = path.join(nodeRoot, 'client', 'public'); 12 | const express = require('express'); 13 | const logger = require('morgan'); 14 | 15 | const app = express(); 16 | const server = require('http').createServer(app); 17 | const favicon = require('serve-favicon'); 18 | const io = require('socket.io')(server, config.socketio); 19 | const session = require('express-session')(config.express); 20 | 21 | const appSocket = require('./socket'); 22 | const { setDefaultCredentials, basicAuth } = require('./util'); 23 | const { webssh2debug } = require('./logging'); 24 | const { reauth, connect, notfound, handleErrors } = require('./routes'); 25 | 26 | setDefaultCredentials(config.user); 27 | 28 | // safe shutdown 29 | let remainingSeconds = config.safeShutdownDuration; 30 | let shutdownMode = false; 31 | let shutdownInterval; 32 | let connectionCount = 0; 33 | // eslint-disable-next-line consistent-return 34 | function safeShutdownGuard(req, res, next) { 35 | if (!shutdownMode) return next(); 36 | res.status(503).end('Service unavailable: Server shutting down'); 37 | } 38 | // express 39 | app.use(safeShutdownGuard); 40 | app.use(session); 41 | if (config.accesslog) app.use(logger('common')); 42 | app.disable('x-powered-by'); 43 | app.use(favicon(path.join(publicPath, 'favicon.ico'))); 44 | app.use(express.urlencoded({ extended: true })); 45 | app.post('/ssh/host/:host?', connect); 46 | app.post('/ssh', express.static(publicPath, config.express.ssh)); 47 | app.use('/ssh', express.static(publicPath, config.express.ssh)); 48 | app.use(basicAuth); 49 | app.get('/ssh/reauth', reauth); 50 | app.get('/ssh/host/:host?', connect); 51 | app.use(notfound); 52 | app.use(handleErrors); 53 | 54 | // clean stop 55 | function stopApp(reason) { 56 | shutdownMode = false; 57 | if (reason) console.info(`Stopping: ${reason}`); 58 | clearInterval(shutdownInterval); 59 | io.close(); 60 | server.close(); 61 | } 62 | 63 | // bring up socket 64 | io.on('connection', appSocket); 65 | 66 | // socket.io 67 | // expose express session with socket.request.session 68 | io.use((socket, next) => { 69 | socket.request.res ? session(socket.request, socket.request.res, next) : next(next); // eslint disable-line 70 | }); 71 | 72 | function countdownTimer() { 73 | if (!shutdownMode) clearInterval(shutdownInterval); 74 | remainingSeconds -= 1; 75 | if (remainingSeconds <= 0) { 76 | stopApp('Countdown is over'); 77 | } else io.emit('shutdownCountdownUpdate', remainingSeconds); 78 | } 79 | 80 | const signals = ['SIGTERM', 'SIGINT']; 81 | signals.forEach((signal) => 82 | process.on(signal, () => { 83 | if (shutdownMode) stopApp('Safe shutdown aborted, force quitting'); 84 | if (!connectionCount > 0) stopApp('All connections ended'); 85 | shutdownMode = true; 86 | console.error( 87 | `\r\n${connectionCount} client(s) are still connected.\r\nStarting a ${remainingSeconds} seconds countdown.\r\nPress Ctrl+C again to force quit` 88 | ); 89 | if (!shutdownInterval) shutdownInterval = setInterval(countdownTimer, 1000); 90 | }) 91 | ); 92 | 93 | module.exports = { server, config }; 94 | 95 | const onConnection = (socket) => { 96 | connectionCount += 1; 97 | socket.on('disconnect', () => { 98 | connectionCount -= 1; 99 | if (connectionCount <= 0 && shutdownMode) { 100 | stopApp('All clients disconnected'); 101 | } 102 | }); 103 | socket.on('geometry', (cols, rows) => { 104 | // TODO need to rework how we pass settings to ssh2, this is less than ideal 105 | socket.request.session.ssh.cols = cols; 106 | socket.request.session.ssh.rows = rows; 107 | webssh2debug(socket, `SOCKET GEOMETRY: termCols = ${cols}, termRows = ${rows}`); 108 | }); 109 | }; 110 | 111 | io.on('connection', onConnection); 112 | -------------------------------------------------------------------------------- /app/server/config.js: -------------------------------------------------------------------------------- 1 | /* eslint no-unused-expressions: ["error", { "allowShortCircuit": true, "allowTernary": true }], 2 | no-console: ["error", { allow: ["warn", "error", "info"] }] */ 3 | const fs = require('fs'); 4 | const path = require('path'); 5 | const debugWebSSH2 = require('debug')('WebSSH2'); 6 | const crypto = require('crypto'); 7 | const util = require('util'); 8 | const readconfig = require('read-config-ng'); 9 | 10 | const nodeRoot = path.dirname(require.main.filename); 11 | const configPath = path.join(nodeRoot, 'config.json'); 12 | 13 | let myConfig; 14 | // establish defaults 15 | const configDefault = { 16 | listen: { 17 | ip: '0.0.0.0', 18 | port: 2222, 19 | }, 20 | socketio: { 21 | serveClient: false, 22 | path: '/ssh/socket.io', 23 | origins: ['localhost:2222'], 24 | }, 25 | express: { 26 | secret: crypto.randomBytes(20).toString('hex'), 27 | name: 'WebSSH2', 28 | resave: true, 29 | saveUninitialized: false, 30 | unset: 'destroy', 31 | ssh: { 32 | dotfiles: 'ignore', 33 | etag: false, 34 | extensions: ['htm', 'html'], 35 | index: false, 36 | maxAge: '1s', 37 | redirect: false, 38 | setHeaders(res) { 39 | res.set('x-timestamp', Date.now()); 40 | }, 41 | }, 42 | }, 43 | user: { 44 | name: null, 45 | password: null, 46 | privatekey: null, 47 | overridebasic: false, 48 | }, 49 | ssh: { 50 | host: null, 51 | port: 22, 52 | term: 'xterm-color', 53 | readyTimeout: 20000, 54 | keepaliveInterval: 120000, 55 | keepaliveCountMax: 10, 56 | allowedSubnets: [], 57 | }, 58 | terminal: { 59 | cursorBlink: true, 60 | scrollback: 10000, 61 | tabStopWidth: 8, 62 | bellStyle: 'sound', 63 | }, 64 | header: { 65 | text: null, 66 | background: 'green', 67 | }, 68 | options: { 69 | challengeButton: true, 70 | allowreauth: true, 71 | }, 72 | algorithms: { 73 | kex: [ 74 | 'ecdh-sha2-nistp256', 75 | 'ecdh-sha2-nistp384', 76 | 'ecdh-sha2-nistp521', 77 | 'diffie-hellman-group-exchange-sha256', 78 | 'diffie-hellman-group14-sha1', 79 | ], 80 | cipher: [ 81 | 'aes128-ctr', 82 | 'aes192-ctr', 83 | 'aes256-ctr', 84 | 'aes128-gcm', 85 | 'aes128-gcm@openssh.com', 86 | 'aes256-gcm', 87 | 'aes256-gcm@openssh.com', 88 | 'aes256-cbc', 89 | ], 90 | hmac: ['hmac-sha2-256', 'hmac-sha2-512', 'hmac-sha1'], 91 | compress: ['none', 'zlib@openssh.com', 'zlib'], 92 | }, 93 | serverlog: { 94 | client: false, 95 | server: false, 96 | }, 97 | accesslog: false, 98 | verify: false, 99 | safeShutdownDuration: 300, 100 | }; 101 | 102 | // test if config.json exists, if not provide error message but try to run anyway 103 | try { 104 | if (!fs.existsSync(configPath)) { 105 | console.error( 106 | `\n\nERROR: Missing config.json for WebSSH2. Current config: ${util.inspect(myConfig)}` 107 | ); 108 | console.error('\n See config.json.sample for details\n\n'); 109 | } 110 | console.info(`WebSSH2 service reading config from: ${configPath}`); 111 | const configFile = readconfig(configPath, { override: true }); 112 | // myConfig = merger.mergeObjects([configDefault, configFile]); 113 | myConfig = { ...configDefault, ...configFile }; 114 | debugWebSSH2(`\nCurrent config: ${util.inspect(myConfig)}`); 115 | } catch (err) { 116 | myConfig = configDefault; 117 | console.error( 118 | `\n\nERROR: Missing config.json for WebSSH2. Current config: ${util.inspect(myConfig)}` 119 | ); 120 | console.error('\n See config.json.sample for details\n\n'); 121 | console.error(`ERROR:\n\n ${err}`); 122 | } 123 | const config = myConfig; 124 | 125 | if (process.env.LISTEN) config.listen.ip = process.env.LISTEN; 126 | 127 | if (process.env.PORT) config.listen.port = process.env.PORT; 128 | 129 | if (process.env.SOCKETIO_ORIGINS) config.socketio.origins = process.env.SOCKETIO_ORIGINS; 130 | 131 | if (process.env.SOCKETIO_PATH) config.socketio.path = process.env.SOCKETIO_PATH; 132 | 133 | if (process.env.SOCKETIO_SERVECLIENT) 134 | config.socketio.serveClient = process.env.SOCKETIO_SERVECLIENT; 135 | 136 | module.exports = config; 137 | -------------------------------------------------------------------------------- /app/server/form.html: -------------------------------------------------------------------------------- 1 | Post Test 2 | 3 | 4 |

Credentials over HTTP POST test

5 |

This is a test to demonstrate sending credentials over POST instead of requiring HTTP Basic. If you use this, be sure to secure the app/site with HTTPS!

6 |
7 |

8 | 9 | 10 |

11 | 12 | 13 |

14 |

15 | 16 | 17 |

18 | 19 | 20 |

21 |
22 | Cursor Blink: 23 |
24 | 26 | 27 |
28 |
29 | 30 | 31 |
32 |
33 | 34 |
35 | 36 | -------------------------------------------------------------------------------- /app/server/logging.js: -------------------------------------------------------------------------------- 1 | /* eslint no-unused-expressions: ["error", { "allowShortCircuit": true, "allowTernary": true }], 2 | no-console: ["error", { allow: ["warn", "error", "info"] }] */ 3 | /* jshint esversion: 6, asi: true, node: true */ 4 | // logging.js 5 | // private 6 | 7 | const debug = require('debug'); 8 | const util = require('util'); 9 | 10 | /** 11 | * generate consistent prefix for log messages 12 | * with epress session id and socket session id 13 | * @param {object} socket Socket information 14 | */ 15 | function prefix(socket) { 16 | return `(${socket.request.sessionID}/${socket.id})`; 17 | } 18 | 19 | // public 20 | function webssh2debug(socket, msg) { 21 | debug('WebSSH2')(`${prefix(socket)} ${msg}`); 22 | } 23 | 24 | /** 25 | * audit log to console 26 | * @param {object} socket Socket information 27 | * @param {object} msg log message 28 | */ 29 | function auditLog(socket, msg) { 30 | console.info(`WebSSH2 ${prefix(socket)} AUDIT: ${msg}`); 31 | } 32 | 33 | /** 34 | * logs error to socket client (if connected) 35 | * and console. 36 | * @param {object} socket Socket information 37 | * @param {string} myFunc Function calling this function 38 | * @param {object} err error object or error message 39 | */ 40 | function logError(socket, myFunc, err) { 41 | console.error(`WebSSH2 ${prefix(socket)} ERROR: ${myFunc}: ${err}`); 42 | webssh2debug(socket, `logError: ${myFunc}: ${util.inspect(err)}`); 43 | if (!socket.request.session) return; 44 | socket.emit('ssherror', `SSH ${myFunc}: ${err}`); 45 | } 46 | 47 | module.exports = { logError, auditLog, webssh2debug }; 48 | -------------------------------------------------------------------------------- /app/server/routes.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-console */ 2 | // ssh.js 3 | const validator = require('validator'); 4 | const path = require('path'); 5 | 6 | const nodeRoot = path.dirname(require.main.filename); 7 | 8 | const publicPath = path.join(nodeRoot, 'client', 'public'); 9 | const { parseBool } = require('./util'); 10 | const config = require('./config'); 11 | 12 | exports.reauth = function reauth(req, res) { 13 | let { referer } = req.headers; 14 | if (!validator.isURL(referer, { host_whitelist: ['localhost'] })) { 15 | console.error( 16 | `WebSSH2 (${req.sessionID}) ERROR: Referrer '${referer}' for '/reauth' invalid. Setting to '/' which will probably fail.` 17 | ); 18 | referer = '/'; 19 | } 20 | res 21 | .status(401) 22 | .send( 23 | `` 24 | ); 25 | }; 26 | 27 | exports.connect = function connect(req, res) { 28 | res.sendFile(path.join(path.join(publicPath, 'client.htm'))); 29 | 30 | let { host, port } = config.ssh; 31 | let { text: header, background: headerBackground } = config.header; 32 | let { term: sshterm, readyTimeout } = config.ssh; 33 | let { 34 | cursorBlink, 35 | scrollback, 36 | tabStopWidth, 37 | bellStyle, 38 | fontSize, 39 | fontFamily, 40 | letterSpacing, 41 | lineHeight, 42 | } = config.terminal; 43 | 44 | // capture, assign, and validate variables 45 | 46 | if (req.params?.host) { 47 | if ( 48 | validator.isIP(`${req.params.host}`) || 49 | validator.isFQDN(req.params.host) || 50 | /^(([a-z]|[A-Z]|\d|[!^(){}\-_~])+)?\w$/.test(req.params.host) 51 | ) { 52 | host = req.params.host; 53 | } 54 | } 55 | 56 | if (req.method === 'POST' && req.body.username && req.body.userpassword) { 57 | req.session.username = req.body.username; 58 | req.session.userpassword = req.body.userpassword; 59 | 60 | if (req.body.port && validator.isInt(`${req.body.port}`, { min: 1, max: 65535 })) 61 | port = req.body.port; 62 | 63 | if (req.body.header) header = req.body.header; 64 | 65 | if (req.body.headerBackground) { 66 | headerBackground = req.body.headerBackground; 67 | console.log(`background: ${req.body.headerBackground}`); 68 | } 69 | 70 | if (req.body.sshterm && /^(([a-z]|[A-Z]|\d|[!^(){}\-_~])+)?\w$/.test(req.body.sshterm)) 71 | sshterm = req.body.sshterm; 72 | 73 | if (req.body.cursorBlink && validator.isBoolean(`${req.body.cursorBlink}`)) 74 | cursorBlink = parseBool(req.body.cursorBlink); 75 | 76 | if (req.body.scrollback && validator.isInt(`${req.body.scrollback}`, { min: 1, max: 200000 })) 77 | scrollback = req.body.scrollback; 78 | 79 | if (req.body.tabStopWidth && validator.isInt(`${req.body.tabStopWidth}`, { min: 1, max: 100 })) 80 | tabStopWidth = req.body.tabStopWidth; 81 | 82 | if (req.body.bellStyle && ['sound', 'none'].indexOf(req.body.bellStyle) > -1) 83 | bellStyle = req.body.bellStyle; 84 | 85 | if ( 86 | req.body.readyTimeout && 87 | validator.isInt(`${req.body.readyTimeout}`, { min: 1, max: 300000 }) 88 | ) 89 | readyTimeout = req.body.readyTimeout; 90 | 91 | if (req.body.fontSize && validator.isNumeric(`${req.body.fontSize}`)) 92 | fontSize = req.body.fontSize; 93 | 94 | if (req.body.fontFamily) fontFamily = req.body.fontFamily; 95 | 96 | if (req.body.letterSpacing && validator.isNumeric(`${req.body.letterSpacing}`)) 97 | letterSpacing = req.body.letterSpacing; 98 | 99 | if (req.body.lineHeight && validator.isNumeric(`${req.body.lineHeight}`)) 100 | lineHeight = req.body.lineHeight; 101 | } 102 | 103 | if (req.method === 'GET') { 104 | if (req.query?.port && validator.isInt(`${req.query.port}`, { min: 1, max: 65535 })) 105 | port = req.query.port; 106 | 107 | if (req.query?.header) header = req.query.header; 108 | 109 | if (req.query?.headerBackground) headerBackground = req.query.headerBackground; 110 | 111 | if (req.query?.sshterm && /^(([a-z]|[A-Z]|\d|[!^(){}\-_~])+)?\w$/.test(req.query.sshterm)) 112 | sshterm = req.query.sshterm; 113 | 114 | if (req.query?.cursorBlink && validator.isBoolean(`${req.query.cursorBlink}`)) 115 | cursorBlink = parseBool(req.query.cursorBlink); 116 | 117 | if ( 118 | req.query?.scrollback && 119 | validator.isInt(`${req.query.scrollback}`, { min: 1, max: 200000 }) 120 | ) 121 | scrollback = req.query.scrollback; 122 | 123 | if ( 124 | req.query?.tabStopWidth && 125 | validator.isInt(`${req.query.tabStopWidth}`, { min: 1, max: 100 }) 126 | ) 127 | tabStopWidth = req.query.tabStopWidth; 128 | 129 | if (req.query?.bellStyle && ['sound', 'none'].indexOf(req.query.bellStyle) > -1) 130 | bellStyle = req.query.bellStyle; 131 | 132 | if ( 133 | req.query?.readyTimeout && 134 | validator.isInt(`${req.query.readyTimeout}`, { min: 1, max: 300000 }) 135 | ) 136 | readyTimeout = req.query.readyTimeout; 137 | 138 | if (req.query?.fontSize && validator.isNumeric(`${req.query.fontSize}`)) 139 | fontSize = req.query.fontSize; 140 | 141 | if (req.query?.fontFamily) fontFamily = req.query.fontFamily; 142 | 143 | if (req.query?.lineHeight && validator.isNumeric(`${req.query.lineHeight}`)) 144 | lineHeight = req.query.lineHeight; 145 | 146 | if (req.query?.letterSpacing && validator.isNumeric(`${req.query.letterSpacing}`)) 147 | letterSpacing = req.query.letterSpacing; 148 | } 149 | 150 | req.session.ssh = { 151 | host, 152 | port, 153 | localAddress: config.ssh.localAddress, 154 | localPort: config.ssh.localPort, 155 | header: { 156 | name: header, 157 | background: headerBackground, 158 | }, 159 | algorithms: config.algorithms, 160 | keepaliveInterval: config.ssh.keepaliveInterval, 161 | keepaliveCountMax: config.ssh.keepaliveCountMax, 162 | allowedSubnets: config.ssh.allowedSubnets, 163 | term: sshterm, 164 | terminal: { 165 | cursorBlink, 166 | scrollback, 167 | tabStopWidth, 168 | bellStyle, 169 | fontSize, 170 | fontFamily, 171 | letterSpacing, 172 | lineHeight, 173 | }, 174 | cols: null, 175 | rows: null, 176 | allowreplay: 177 | config.options.challengeButton || 178 | (validator.isBoolean(`${req.headers.allowreplay}`) 179 | ? parseBool(req.headers.allowreplay) 180 | : false), 181 | allowreauth: config.options.allowreauth || false, 182 | mrhsession: 183 | validator.isAlphanumeric(`${req.headers.mrhsession}`) && req.headers.mrhsession 184 | ? req.headers.mrhsession 185 | : 'none', 186 | serverlog: { 187 | client: config.serverlog.client || false, 188 | server: config.serverlog.server || false, 189 | }, 190 | readyTimeout, 191 | }; 192 | if (req.session.ssh.header.name) validator.escape(req.session.ssh.header.name); 193 | if (req.session.ssh.header.background) validator.escape(req.session.ssh.header.background); 194 | }; 195 | 196 | exports.notfound = function notfound(_req, res) { 197 | res.status(404).send("Sorry, can't find that!"); 198 | }; 199 | 200 | exports.handleErrors = function handleErrors(err, _req, res) { 201 | console.error(err.stack); 202 | res.status(500).send('Something broke!'); 203 | }; 204 | -------------------------------------------------------------------------------- /app/server/socket.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable complexity */ 2 | /* eslint no-unused-expressions: ["error", { "allowShortCircuit": true, "allowTernary": true }], 3 | no-console: ["error", { allow: ["warn", "error"] }] */ 4 | /* jshint esversion: 6, asi: true, node: true */ 5 | // socket.js 6 | 7 | // private 8 | const debug = require('debug'); 9 | const SSH = require('ssh2').Client; 10 | const CIDRMatcher = require('cidr-matcher'); 11 | const validator = require('validator'); 12 | const dnsPromises = require('dns').promises; 13 | const util = require('util'); 14 | const { webssh2debug, auditLog, logError } = require('./logging'); 15 | 16 | /** 17 | * parse conn errors 18 | * @param {object} socket Socket object 19 | * @param {object} err Error object 20 | */ 21 | function connError(socket, err) { 22 | let msg = util.inspect(err); 23 | const { session } = socket.request; 24 | if (err?.level === 'client-authentication') { 25 | msg = `Authentication failure user=${session.username} from=${socket.handshake.address}`; 26 | socket.emit('allowreauth', session.ssh.allowreauth); 27 | socket.emit('reauth'); 28 | } 29 | if (err?.code === 'ENOTFOUND') { 30 | msg = `Host not found: ${err.hostname}`; 31 | } 32 | if (err?.level === 'client-timeout') { 33 | msg = `Connection Timeout: ${session.ssh.host}`; 34 | } 35 | logError(socket, 'CONN ERROR', msg); 36 | } 37 | 38 | /** 39 | * check ssh host is in allowed subnet 40 | * @param {object} socket Socket information 41 | */ 42 | async function checkSubnet(socket) { 43 | let ipaddress = socket.request.session.ssh.host; 44 | if (!validator.isIP(`${ipaddress}`)) { 45 | try { 46 | const result = await dnsPromises.lookup(socket.request.session.ssh.host); 47 | ipaddress = result.address; 48 | } catch (err) { 49 | logError( 50 | socket, 51 | 'CHECK SUBNET', 52 | `${err.code}: ${err.hostname} user=${socket.request.session.username} from=${socket.handshake.address}` 53 | ); 54 | socket.emit('ssherror', '404 HOST IP NOT FOUND'); 55 | socket.disconnect(true); 56 | return; 57 | } 58 | } 59 | 60 | const matcher = new CIDRMatcher(socket.request.session.ssh.allowedSubnets); 61 | if (!matcher.contains(ipaddress)) { 62 | logError( 63 | socket, 64 | 'CHECK SUBNET', 65 | `Requested host ${ipaddress} outside configured subnets / REJECTED user=${socket.request.session.username} from=${socket.handshake.address}` 66 | ); 67 | socket.emit('ssherror', '401 UNAUTHORIZED'); 68 | socket.disconnect(true); 69 | } 70 | } 71 | 72 | // public 73 | module.exports = function appSocket(socket) { 74 | let login = false; 75 | 76 | socket.once('disconnecting', (reason) => { 77 | webssh2debug(socket, `SOCKET DISCONNECTING: ${reason}`); 78 | if (login === true) { 79 | auditLog( 80 | socket, 81 | `LOGOUT user=${socket.request.session.username} from=${socket.handshake.address} host=${socket.request.session.ssh.host}:${socket.request.session.ssh.port}` 82 | ); 83 | login = false; 84 | } 85 | }); 86 | 87 | async function setupConnection() { 88 | // if websocket connection arrives without an express session, kill it 89 | if (!socket.request.session) { 90 | socket.emit('401 UNAUTHORIZED'); 91 | webssh2debug(socket, 'SOCKET: No Express Session / REJECTED'); 92 | socket.disconnect(true); 93 | return; 94 | } 95 | 96 | // If configured, check that requsted host is in a permitted subnet 97 | if (socket.request.session?.ssh?.allowedSubnets?.length > 0) { 98 | checkSubnet(socket); 99 | } 100 | 101 | const conn = new SSH(); 102 | 103 | conn.on('banner', (data) => { 104 | // need to convert to cr/lf for proper formatting 105 | socket.emit('data', data.replace(/\r?\n/g, '\r\n').toString('utf-8')); 106 | }); 107 | 108 | conn.on('handshake', () => { 109 | socket.emit('setTerminalOpts', socket.request.session.ssh.terminal); 110 | socket.emit('menu'); 111 | socket.emit('allowreauth', socket.request.session.ssh.allowreauth); 112 | socket.emit('title', `ssh://${socket.request.session.ssh.host}`); 113 | if (socket.request.session.ssh.header.background) 114 | socket.emit('headerBackground', socket.request.session.ssh.header.background); 115 | if (socket.request.session.ssh.header.name) 116 | socket.emit('header', socket.request.session.ssh.header.name); 117 | socket.emit( 118 | 'footer', 119 | `ssh://${socket.request.session.username}@${socket.request.session.ssh.host}:${socket.request.session.ssh.port}` 120 | ); 121 | }); 122 | 123 | conn.on('ready', () => { 124 | webssh2debug( 125 | socket, 126 | `CONN READY: LOGIN: user=${socket.request.session.username} from=${socket.handshake.address} host=${socket.request.session.ssh.host} port=${socket.request.session.ssh.port} allowreplay=${socket.request.session.ssh.allowreplay} term=${socket.request.session.ssh.term}` 127 | ); 128 | auditLog( 129 | socket, 130 | `LOGIN user=${socket.request.session.username} from=${socket.handshake.address} host=${socket.request.session.ssh.host}:${socket.request.session.ssh.port}` 131 | ); 132 | login = true; 133 | socket.emit('status', 'SSH CONNECTION ESTABLISHED'); 134 | socket.emit('statusBackground', 'green'); 135 | socket.emit('allowreplay', socket.request.session.ssh.allowreplay); 136 | const { term, cols, rows } = socket.request.session.ssh; 137 | conn.shell({ term, cols, rows }, (err, stream) => { 138 | if (err) { 139 | logError(socket, `EXEC ERROR`, err); 140 | conn.end(); 141 | socket.disconnect(true); 142 | return; 143 | } 144 | socket.once('disconnect', (reason) => { 145 | webssh2debug(socket, `CLIENT SOCKET DISCONNECT: ${util.inspect(reason)}`); 146 | conn.end(); 147 | socket.request.session.destroy(); 148 | }); 149 | socket.on('error', (errMsg) => { 150 | webssh2debug(socket, `SOCKET ERROR: ${errMsg}`); 151 | logError(socket, 'SOCKET ERROR', errMsg); 152 | conn.end(); 153 | socket.disconnect(true); 154 | }); 155 | socket.on('control', (controlData) => { 156 | if (controlData === 'replayCredentials' && socket.request.session.ssh.allowreplay) { 157 | stream.write(`${socket.request.session.userpassword}\n`); 158 | } 159 | if (controlData === 'reauth' && socket.request.session.username && login === true) { 160 | auditLog( 161 | socket, 162 | `LOGOUT user=${socket.request.session.username} from=${socket.handshake.address} host=${socket.request.session.ssh.host}:${socket.request.session.ssh.port}` 163 | ); 164 | login = false; 165 | conn.end(); 166 | socket.disconnect(true); 167 | } 168 | webssh2debug(socket, `SOCKET CONTROL: ${controlData}`); 169 | }); 170 | socket.on('resize', (data) => { 171 | stream.setWindow(data.rows, data.cols); 172 | webssh2debug(socket, `SOCKET RESIZE: ${JSON.stringify([data.rows, data.cols])}`); 173 | }); 174 | socket.on('data', (data) => { 175 | stream.write(data); 176 | }); 177 | stream.on('data', (data) => { 178 | socket.emit('data', data.toString('utf-8')); 179 | }); 180 | stream.on('close', (code, signal) => { 181 | webssh2debug(socket, `STREAM CLOSE: ${util.inspect([code, signal])}`); 182 | if (socket.request.session?.username && login === true) { 183 | auditLog( 184 | socket, 185 | `LOGOUT user=${socket.request.session.username} from=${socket.handshake.address} host=${socket.request.session.ssh.host}:${socket.request.session.ssh.port}` 186 | ); 187 | login = false; 188 | } 189 | if (code !== 0 && typeof code !== 'undefined') 190 | logError(socket, 'STREAM CLOSE', util.inspect({ message: [code, signal] })); 191 | socket.disconnect(true); 192 | conn.end(); 193 | }); 194 | stream.stderr.on('data', (data) => { 195 | console.error(`STDERR: ${data}`); 196 | }); 197 | }); 198 | }); 199 | 200 | conn.on('end', (err) => { 201 | if (err) logError(socket, 'CONN END BY HOST', err); 202 | webssh2debug(socket, 'CONN END BY HOST'); 203 | socket.disconnect(true); 204 | }); 205 | conn.on('close', (err) => { 206 | if (err) logError(socket, 'CONN CLOSE', err); 207 | webssh2debug(socket, 'CONN CLOSE'); 208 | socket.disconnect(true); 209 | }); 210 | conn.on('error', (err) => connError(socket, err)); 211 | 212 | conn.on('keyboard-interactive', (_name, _instructions, _instructionsLang, _prompts, finish) => { 213 | webssh2debug(socket, 'CONN keyboard-interactive'); 214 | finish([socket.request.session.userpassword]); 215 | }); 216 | if ( 217 | socket.request.session.username && 218 | (socket.request.session.userpassword || socket.request.session.privatekey) && 219 | socket.request.session.ssh 220 | ) { 221 | // console.log('hostkeys: ' + hostkeys[0].[0]) 222 | const { ssh } = socket.request.session; 223 | ssh.username = socket.request.session.username; 224 | ssh.password = socket.request.session.userpassword; 225 | ssh.tryKeyboard = true; 226 | ssh.debug = debug('ssh2'); 227 | conn.connect(ssh); 228 | } else { 229 | webssh2debug( 230 | socket, 231 | `CONN CONNECT: Attempt to connect without session.username/password or session varialbles defined, potentially previously abandoned client session. disconnecting websocket client.\r\nHandshake information: \r\n ${util.inspect( 232 | socket.handshake 233 | )}` 234 | ); 235 | socket.emit('ssherror', 'WEBSOCKET ERROR - Refresh the browser and try again'); 236 | socket.request.session.destroy(); 237 | socket.disconnect(true); 238 | } 239 | } 240 | setupConnection(); 241 | }; 242 | -------------------------------------------------------------------------------- /app/server/util.js: -------------------------------------------------------------------------------- 1 | /* jshint esversion: 6, asi: true, node: true */ 2 | // util.js 3 | 4 | // private 5 | const debug = require('debug')('WebSSH2'); 6 | const Auth = require('basic-auth'); 7 | 8 | let defaultCredentials = { username: null, password: null, privatekey: null }; 9 | 10 | exports.setDefaultCredentials = function setDefaultCredentials({ 11 | name: username, 12 | password, 13 | privatekey, 14 | overridebasic, 15 | }) { 16 | defaultCredentials = { username, password, privatekey, overridebasic }; 17 | }; 18 | 19 | exports.basicAuth = function basicAuth(req, res, next) { 20 | const myAuth = Auth(req); 21 | // If Authorize: Basic header exists and the password isn't blank 22 | // AND config.user.overridebasic is false, extract basic credentials 23 | // from client] 24 | const { username, password, privatekey, overridebasic } = defaultCredentials; 25 | if (myAuth && myAuth.pass !== '' && !overridebasic) { 26 | req.session.username = myAuth.name; 27 | req.session.userpassword = myAuth.pass; 28 | debug(`myAuth.name: ${myAuth.name} and password ${myAuth.pass ? 'exists' : 'is blank'}`); 29 | } else { 30 | req.session.username = username; 31 | req.session.userpassword = password; 32 | req.session.privatekey = privatekey; 33 | } 34 | if (!req.session.userpassword && !req.session.privatekey) { 35 | res.statusCode = 401; 36 | debug('basicAuth credential request (401)'); 37 | res.setHeader('WWW-Authenticate', 'Basic realm="WebSSH"'); 38 | res.end('Username and password required for web SSH service.'); 39 | return; 40 | } 41 | next(); 42 | }; 43 | 44 | // takes a string, makes it boolean (true if the string is true, false otherwise) 45 | exports.parseBool = function parseBool(str) { 46 | return str.toLowerCase() === 'true'; 47 | }; 48 | -------------------------------------------------------------------------------- /bun.lockb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/billchurch/webssh2/9c0ba04b31e92b7ed20e5c3509b5cbcc5447f565/bun.lockb -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": {}, 3 | "devDependencies": { 4 | "bun-types": "^1.0.1" 5 | } 6 | } -------------------------------------------------------------------------------- /release-please-config.json: -------------------------------------------------------------------------------- 1 | { 2 | "changelogPath": "CHANGELOG.md", 3 | "include-v-in-tags": false, 4 | "prerelease": true, 5 | "packages": { 6 | "app": { 7 | "releaseType": "node", 8 | "draft": false, 9 | "bumpMinorPreMajor": false, 10 | "bumpPatchForMinorPreMajor": false, 11 | "versioning": "default" 12 | } 13 | } 14 | } --------------------------------------------------------------------------------