chore: initial bifurcation of client and server code

This commit is contained in:
Bill Church 2024-07-11 11:23:23 +00:00
parent ea12cc8b7e
commit 3ecda672ba
No known key found for this signature in database
49 changed files with 394 additions and 1785 deletions

View file

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

View file

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

View file

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

4
.snyk
View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,7 +0,0 @@
FROM node:8.6
WORKDIR /usr/src
COPY app/ /usr/src/
RUN npm install --production
EXPOSE 2222
CMD npm run start

View file

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

15
Jenkinsfile vendored
View file

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

90
app/app.js Normal file
View file

@ -0,0 +1,90 @@
// app/app.js
"use strict";
/* jshint esversion: 6, asi: true, node: true */
const path = require("path");
const express = require("express");
const compression = require("compression");
const session = require("express-session");
const logger = require("morgan");
const socketIo = require("socket.io");
const myutil = require("./util");
const config = require("./config");
const socketHandler = require("./socket");
const app = express();
const server = require("http").Server(app);
// Session middleware
const sessionMiddleware = session({
secret: config.session.secret,
name: config.session.name,
resave: false,
saveUninitialized: true,
cookie: {
secure: process.env.NODE_ENV === "production",
maxAge: 24 * 60 * 60 * 1000,
},
});
// Express middleware
app.use(compression({ level: 9 }));
app.use(sessionMiddleware);
if (config.accesslog) app.use(logger("common"));
app.disable("x-powered-by");
// Socket.IO setup
const io = socketIo(server, {
path: "/ssh/socket.io",
cors: {
origin: "http://localhost:8080",
methods: ["GET", "POST"],
credentials: true,
},
});
// Socket.io middleware
io.use((socket, next) => {
sessionMiddleware(socket.request, socket.request.res || {}, next);
});
// WebSocket handling
io.on("connection", (socket) => {
console.log(
"New connection:",
socket.id,
"Transport:",
socket.conn.transport.name
);
// Call the imported socket handler function only once per connection
if (!socket.handled) {
socketHandler(io, socket);
socket.handled = true;
}
socket.on("disconnect", (reason) => {
console.log("Client disconnected:", socket.id, reason);
});
});
// Error handling
app.use((req, res, next) => {
res.status(404).send("Sorry can't find that!");
});
app.use((err, req, res, next) => {
console.error(err.stack);
res.status(500).send("Something broke!");
});
// Graceful shutdown
process.on("SIGINT", () => {
console.log("SIGINT signal received: closing HTTP server");
server.close(() => {
console.log("HTTP server closed");
process.exit(0);
});
});
module.exports = { server, config, io };

View file

@ -1,32 +0,0 @@
<!-- Version Version 0.2.12 - 2024-07-10T13:49:24.751Z - 533f719 -->
<!DOCTYPE html>
<html>
<head>
<title>WebSSH2</title>
<style>
html,
body {
background-color: #000;
height: 100%;
margin: 0;
}
.dropup-content {
display: none;
}
</style>
<link href="/ssh/webssh2.css" rel="stylesheet"></head>
<body>
<div class="box">
<div id="header"></div>
<div id="terminal-container" class="terminal"></div>
<div id="bottomdiv">
<div class="dropup" id="menu">
<i class="fas fa-bars fa-fw"></i> Menu
<div id="dropupContent" class="dropup-content"></div>
</div>
<div id="footer"></div>
<div id="status"></div>
</div>
</div>
<script type="text/javascript" src="/ssh/webssh2.bundle.js"></script></body>
</html>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

File diff suppressed because one or more lines are too long

View file

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

View file

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

View file

@ -1,32 +0,0 @@
<!-- <%= htmlWebpackPlugin.options.version %> -->
<!DOCTYPE html>
<html>
<head>
<title>WebSSH2</title>
<style>
html,
body {
background-color: #000;
height: 100%;
margin: 0;
}
.dropup-content {
display: none;
}
</style>
</head>
<body>
<div class="box">
<div id="header"></div>
<div id="terminal-container" class="terminal"></div>
<div id="bottomdiv">
<div class="dropup" id="menu">
<i class="fas fa-bars fa-fw"></i> Menu
<div id="dropupContent" class="dropup-content"></div>
</div>
<div id="footer"></div>
<div id="status"></div>
</div>
</div>
</body>
</html>

View file

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

View file

@ -1,246 +0,0 @@
'use strict'
import * as io from 'socket.io-client'
import * as Terminal from 'xterm/dist/xterm'
import * as fit from 'xterm/dist/addons/fit/fit'
import { library, dom } from '@fortawesome/fontawesome-svg-core'
import { faBars, faClipboard, faDownload, faKey, faCog } from '@fortawesome/free-solid-svg-icons'
library.add(faBars, faClipboard, faDownload, faKey, faCog)
dom.watch()
require('xterm/dist/xterm.css')
require('../css/style.css')
Terminal.applyAddon(fit)
/* global Blob, logBtn, credentialsBtn, reauthBtn, downloadLogBtn */
var sessionLogEnable = false
var loggedData = false
var allowreplay = false
var allowreauth = false
var sessionLog, sessionFooter, logDate, currentDate, myFile, errorExists
var socket, termid // eslint-disable-line
var term = new Terminal()
// DOM properties
var status = document.getElementById('status')
var header = document.getElementById('header')
var dropupContent = document.getElementById('dropupContent')
var footer = document.getElementById('footer')
var terminalContainer = document.getElementById('terminal-container')
term.open(terminalContainer)
term.focus()
term.fit()
window.addEventListener('resize', resizeScreen, false)
function resizeScreen () {
term.fit()
socket.emit('resize', { cols: term.cols, rows: term.rows })
}
socket = io.connect({
path: '/ssh/socket.io'
})
term.on('data', function (data) {
socket.emit('data', data)
})
socket.on('data', function (data) {
term.write(data)
if (sessionLogEnable) {
sessionLog = sessionLog + data
}
})
socket.on('connect', function () {
socket.emit('geometry', term.cols, term.rows)
})
socket.on('setTerminalOpts', function (data) {
term.setOption('cursorBlink', data.cursorBlink)
term.setOption('scrollback', data.scrollback)
term.setOption('tabStopWidth', data.tabStopWidth)
term.setOption('bellStyle', data.bellStyle)
})
socket.on('title', function (data) {
document.title = data
})
socket.on('menu', function (data) {
drawMenu(data)
})
socket.on('status', function (data) {
status.innerHTML = data
})
socket.on('ssherror', function (data) {
status.innerHTML = data
status.style.backgroundColor = 'red'
errorExists = true
})
socket.on('headerBackground', function (data) {
header.style.backgroundColor = data
})
socket.on('header', function (data) {
if (data) {
header.innerHTML = data
header.style.display = 'block'
// header is 19px and footer is 19px, recaculate new terminal-container and resize
terminalContainer.style.height = 'calc(100% - 38px)'
resizeScreen()
}
})
socket.on('footer', function (data) {
sessionFooter = data
footer.innerHTML = data
})
socket.on('statusBackground', function (data) {
status.style.backgroundColor = data
})
socket.on('allowreplay', function (data) {
if (data === true) {
console.log('allowreplay: ' + data)
allowreplay = true
drawMenu(dropupContent.innerHTML + '<a id="credentialsBtn"><i class="fas fa-key fa-fw"></i> Credentials</a>')
} else {
allowreplay = false
console.log('allowreplay: ' + data)
}
})
socket.on('allowreauth', function (data) {
if (data === true) {
console.log('allowreauth: ' + data)
allowreauth = true
drawMenu(dropupContent.innerHTML + '<a id="reauthBtn"><i class="fas fa-key fa-fw"></i> Switch User</a>')
} else {
allowreauth = false
console.log('allowreauth: ' + data)
}
})
socket.on('disconnect', function (err) {
if (!errorExists) {
status.style.backgroundColor = 'red'
status.innerHTML =
'WEBSOCKET SERVER DISCONNECTED: ' + err
}
socket.io.reconnection(false)
})
socket.on('error', function (err) {
if (!errorExists) {
status.style.backgroundColor = 'red'
status.innerHTML = 'ERROR: ' + err
}
})
socket.on('reauth', function () {
(allowreauth) && reauthSession()
})
term.on('title', function (title) {
document.title = title
})
// draw/re-draw menu and reattach listeners
// when dom is changed, listeners are abandonded
function drawMenu (data) {
dropupContent.innerHTML = data
logBtn.addEventListener('click', toggleLog)
allowreauth && reauthBtn.addEventListener('click', reauthSession)
allowreplay && credentialsBtn.addEventListener('click', replayCredentials)
loggedData && downloadLogBtn.addEventListener('click', downloadLog)
}
// reauthenticate
function reauthSession () { // eslint-disable-line
console.log('re-authenticating')
window.location.href = '/ssh/reauth'
return false
}
// replay password to server, requires
function replayCredentials () { // eslint-disable-line
socket.emit('control', 'replayCredentials')
console.log('replaying credentials')
term.focus()
return false
}
// Set variable to toggle log data from client/server to a varialble
// for later download
function toggleLog () { // eslint-disable-line
if (sessionLogEnable === true) {
sessionLogEnable = false
loggedData = true
logBtn.innerHTML = '<i class="fas fa-clipboard fa-fw"></i> Start Log'
console.log('stopping log, ' + sessionLogEnable)
currentDate = new Date()
sessionLog = sessionLog + '\r\n\r\nLog End for ' + sessionFooter + ': ' +
currentDate.getFullYear() + '/' + (currentDate.getMonth() + 1) + '/' +
currentDate.getDate() + ' @ ' + currentDate.getHours() + ':' +
currentDate.getMinutes() + ':' + currentDate.getSeconds() + '\r\n'
logDate = currentDate
term.focus()
return false
} else {
sessionLogEnable = true
loggedData = true
logBtn.innerHTML = '<i class="fas fa-cog fa-spin fa-fw"></i> Stop Log'
downloadLogBtn.style.color = '#000'
downloadLogBtn.addEventListener('click', downloadLog)
console.log('starting log, ' + sessionLogEnable)
currentDate = new Date()
sessionLog = 'Log Start for ' + sessionFooter + ': ' +
currentDate.getFullYear() + '/' + (currentDate.getMonth() + 1) + '/' +
currentDate.getDate() + ' @ ' + currentDate.getHours() + ':' +
currentDate.getMinutes() + ':' + currentDate.getSeconds() + '\r\n\r\n'
logDate = currentDate
term.focus()
return false
}
}
// cross browser method to "download" an element to the local system
// used for our client-side logging feature
function downloadLog () { // eslint-disable-line
if (loggedData === true) {
myFile = 'WebSSH2-' + logDate.getFullYear() + (logDate.getMonth() + 1) +
logDate.getDate() + '_' + logDate.getHours() + logDate.getMinutes() +
logDate.getSeconds() + '.log'
// regex should eliminate escape sequences from being logged.
var blob = new Blob([sessionLog.replace(/[\u001b\u009b][[\]()#;?]*(?:[0-9]{1,4}(?:;[0-9]{0,4})*)?[0-9A-ORZcf-nqry=><;]/g, '')], { // eslint-disable-line no-control-regex
type: 'text/plain'
})
if (window.navigator.msSaveOrOpenBlob) {
window.navigator.msSaveBlob(blob, myFile)
} else {
var elem = window.document.createElement('a')
elem.href = window.URL.createObjectURL(blob)
elem.download = myFile
document.body.appendChild(elem)
elem.click()
document.body.removeChild(elem)
}
}
term.focus()
}
// Add an event listener for capturing Ctrl + Shift + 6
document.addEventListener('keydown', function(event) {
if (event.ctrlKey && event.shiftKey && event.code === 'Digit6') {
// Prevent the default action
event.preventDefault();
// Emit the desired key sequence to the server
socket.emit('data', '\x1E'); // 0x1E is the RS control character
}
});

95
app/config.js Normal file
View file

@ -0,0 +1,95 @@
// config.js
const path = require("path");
const fs = require("fs");
const nodeRoot = path.dirname(require.main.filename);
const configPath = path.join(nodeRoot, "config.json");
// Default configuration
let config = {
listen: {
ip: "0.0.0.0",
port: 2222,
},
http: {
origins: ["*:*"],
},
user: {
name: null,
password: null,
},
ssh: {
host: null,
port: 22,
term: "xterm-color",
readyTimeout: 20000,
keepaliveInterval: 120000,
keepaliveCountMax: 10,
},
terminal: {
cursorBlink: true,
scrollback: 10000,
tabStopWidth: 8,
bellStyle: "sound",
},
header: {
text: null,
background: "green",
},
session: {
name: "WebSSH2",
secret: "mysecret",
},
options: {
challengeButton: true,
allowreauth: true,
},
algorithms: {
kex: [
"ecdh-sha2-nistp256",
"ecdh-sha2-nistp384",
"ecdh-sha2-nistp521",
"diffie-hellman-group-exchange-sha256",
"diffie-hellman-group14-sha1",
],
cipher: [
"aes128-ctr",
"aes192-ctr",
"aes256-ctr",
"aes128-gcm",
"aes128-gcm@openssh.com",
"aes256-gcm",
"aes256-gcm@openssh.com",
"aes256-cbc",
],
hmac: ["hmac-sha2-256", "hmac-sha2-512", "hmac-sha1"],
compress: ["none", "zlib@openssh.com", "zlib"],
},
serverlog: {
client: false,
server: false,
},
accesslog: false,
verify: false,
};
try {
if (fs.existsSync(configPath)) {
console.log("WebSSH2 service reading config from: " + configPath);
config = require("read-config-ng")(configPath);
} else {
console.error(
"\n\nERROR: Missing config.json for webssh. Current config: " +
JSON.stringify(config)
);
console.error("\n See config.json.sample for details\n\n");
}
} catch (err) {
console.error(
"\n\nERROR: Missing config.json for webssh. Current config: " +
JSON.stringify(config)
);
console.error("\n See config.json.sample for details\n\n");
console.error("ERROR:\n\n " + err);
}
module.exports = config;

12
app/expressOptions.js Normal file
View file

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

View file

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

View file

@ -1,61 +0,0 @@
const webpack = require("webpack");
const { BannerPlugin } = require("webpack");
const HtmlWebpackPlugin = require("html-webpack-plugin");
const path = require("path");
const CopyWebpackPlugin = require("copy-webpack-plugin");
const CleanWebpackPlugin = require("clean-webpack-plugin");
const ExtractTextPlugin = require("extract-text-webpack-plugin");
const packageJson = require("../package.json"); // Load package.json
const commitHash = require("child_process")
.execSync("git rev-parse --short HEAD")
.toString()
.trim();
module.exports = {
context: path.resolve(__dirname, "../"),
entry: {
webssh2: "./client/src/js/index.js",
},
plugins: [
new BannerPlugin({
banner: `Version ${
packageJson.version
} - ${new Date().toISOString()} - ${commitHash}`,
include: /\.(js|css|html|htm)$/,
}),
new CleanWebpackPlugin(["client/public"], {
root: path.resolve("__dirname", "../"),
verbose: true,
}),
new HtmlWebpackPlugin({
template: "./client/src/client.htm", // Path to your source template
filename: "client.htm", // Optional: output file name, defaults to index.html
minify: false,
scriptLoading: "defer",
version: `Version ${
packageJson.version
} - ${new Date().toISOString()} - ${commitHash}`,
publicPath: "/ssh/", // Prepend /ssh/ to the script tags
}),
new CopyWebpackPlugin([
{ from: "./client/src/favicon.ico", to: "favicon.ico" },
]),
new ExtractTextPlugin("[name].css"),
],
output: {
filename: "[name].bundle.js",
path: path.resolve(__dirname, "../client/public"),
publicPath: "/ssh/", // Prepend /ssh/ to the script tags
},
module: {
rules: [
{
test: /\.css$/,
use: ExtractTextPlugin.extract({
fallback: "style-loader",
use: [{ loader: "css-loader" }],
}),
},
],
},
};

View file

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

View file

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

View file

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

View file

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

View file

@ -1,173 +0,0 @@
'use strict'
/* jshint esversion: 6, asi: true, node: true */
// socket.js
// private
var debug = require('debug')
var debugWebSSH2 = require('debug')('WebSSH2')
var SSH = require('ssh2').Client
// var fs = require('fs')
// var hostkeys = JSON.parse(fs.readFileSync('./hostkeyhashes.json', 'utf8'))
var termCols, termRows
var menuData = '<a id="logBtn"><i class="fas fa-clipboard fa-fw"></i> Start Log</a>' +
'<a id="downloadLogBtn"><i class="fas fa-download fa-fw"></i> Download Log</a>'
// public
module.exports = function socket (socket) {
// if websocket connection arrives without an express session, kill it
if (!socket.request.session) {
socket.emit('401 UNAUTHORIZED')
debugWebSSH2('SOCKET: No Express Session / REJECTED')
socket.disconnect(true)
return
}
var conn = new SSH()
socket.on('geometry', function socketOnGeometry (cols, rows) {
termCols = cols
termRows = rows
})
conn.on('banner', function connOnBanner (data) {
// need to convert to cr/lf for proper formatting
data = data.replace(/\r?\n/g, '\r\n')
socket.emit('data', data.toString('utf-8'))
})
conn.on('ready', function connOnReady () {
console.log('WebSSH2 Login: user=' + socket.request.session.username + ' from=' + socket.handshake.address + ' host=' + socket.request.session.ssh.host + ' port=' + socket.request.session.ssh.port + ' sessionID=' + socket.request.sessionID + '/' + socket.id + ' mrhsession=' + socket.request.session.ssh.mrhsession + ' allowreplay=' + socket.request.session.ssh.allowreplay + ' term=' + socket.request.session.ssh.term)
socket.emit('menu', menuData)
socket.emit('allowreauth', socket.request.session.ssh.allowreauth)
socket.emit('setTerminalOpts', socket.request.session.ssh.terminal)
socket.emit('title', 'ssh://' + socket.request.session.ssh.host)
if (socket.request.session.ssh.header.background) socket.emit('headerBackground', socket.request.session.ssh.header.background)
if (socket.request.session.ssh.header.name) socket.emit('header', socket.request.session.ssh.header.name)
socket.emit('footer', 'ssh://' + socket.request.session.username + '@' + socket.request.session.ssh.host + ':' + socket.request.session.ssh.port)
socket.emit('status', 'SSH CONNECTION ESTABLISHED')
socket.emit('statusBackground', 'green')
socket.emit('allowreplay', socket.request.session.ssh.allowreplay)
conn.shell({
term: socket.request.session.ssh.term,
cols: termCols,
rows: termRows
}, function connShell (err, stream) {
if (err) {
SSHerror('EXEC ERROR' + err)
conn.end()
return
}
// poc to log commands from client
if (socket.request.session.ssh.serverlog.client) var dataBuffer
socket.on('data', function socketOnData (data) {
stream.write(data)
// poc to log commands from client
if (socket.request.session.ssh.serverlog.client) {
if (data === '\r') {
console.log('serverlog.client: ' + socket.request.session.id + '/' + socket.id + ' host: ' + socket.request.session.ssh.host + ' command: ' + dataBuffer)
dataBuffer = undefined
} else {
dataBuffer = (dataBuffer) ? dataBuffer + data : data
}
}
})
socket.on('control', function socketOnControl (controlData) {
switch (controlData) {
case 'replayCredentials':
if (socket.request.session.ssh.allowreplay) {
stream.write(socket.request.session.userpassword + '\n')
}
/* falls through */
default:
console.log('controlData: ' + controlData)
}
})
socket.on('resize', function socketOnResize (data) {
stream.setWindow(data.rows, data.cols)
})
socket.on('disconnecting', function socketOnDisconnecting (reason) { debugWebSSH2('SOCKET DISCONNECTING: ' + reason) })
socket.on('disconnect', function socketOnDisconnect (reason) {
debugWebSSH2('SOCKET DISCONNECT: ' + reason)
err = { message: reason }
SSHerror('CLIENT SOCKET DISCONNECT', err)
conn.end()
// socket.request.session.destroy()
})
socket.on('error', function socketOnError (err) {
SSHerror('SOCKET ERROR', err)
conn.end()
})
stream.on('data', function streamOnData (data) { socket.emit('data', data.toString('utf-8')) })
stream.on('close', function streamOnClose (code, signal) {
err = { message: ((code || signal) ? (((code) ? 'CODE: ' + code : '') + ((code && signal) ? ' ' : '') + ((signal) ? 'SIGNAL: ' + signal : '')) : undefined) }
SSHerror('STREAM CLOSE', err)
conn.end()
})
stream.stderr.on('data', function streamStderrOnData (data) {
console.log('STDERR: ' + data)
})
})
})
conn.on('end', function connOnEnd (err) { SSHerror('CONN END BY HOST', err) })
conn.on('close', function connOnClose (err) { SSHerror('CONN CLOSE', err) })
conn.on('error', function connOnError (err) { SSHerror('CONN ERROR', err) })
conn.on('keyboard-interactive', function connOnKeyboardInteractive (name, instructions, instructionsLang, prompts, finish) {
debugWebSSH2('conn.on(\'keyboard-interactive\')')
finish([socket.request.session.userpassword])
})
if (socket.request.session.username && socket.request.session.userpassword && socket.request.session.ssh) {
// console.log('hostkeys: ' + hostkeys[0].[0])
conn.connect({
host: socket.request.session.ssh.host,
port: socket.request.session.ssh.port,
username: socket.request.session.username,
password: socket.request.session.userpassword,
tryKeyboard: true,
algorithms: socket.request.session.ssh.algorithms,
readyTimeout: socket.request.session.ssh.readyTimeout,
keepaliveInterval: socket.request.session.ssh.keepaliveInterval,
keepaliveCountMax: socket.request.session.ssh.keepaliveCountMax,
debug: debug('ssh2')
})
} else {
debugWebSSH2('Attempt to connect without session.username/password or session varialbles defined, potentially previously abandoned client session. disconnecting websocket client.\r\nHandshake information: \r\n ' + JSON.stringify(socket.handshake))
socket.emit('ssherror', 'WEBSOCKET ERROR - Refresh the browser and try again')
socket.request.session.destroy()
socket.disconnect(true)
}
/**
* Error handling for various events. Outputs error to client, logs to
* server, destroys session and disconnects socket.
* @param {string} myFunc Function calling this function
* @param {object} err error object or error message
*/
function SSHerror (myFunc, err) {
var theError
if (socket.request.session) {
// we just want the first error of the session to pass to the client
socket.request.session.error = (socket.request.session.error) || ((err) ? err.message : undefined)
theError = (socket.request.session.error) ? ': ' + socket.request.session.error : ''
// log unsuccessful login attempt
if (err && (err.level === 'client-authentication')) {
console.log('WebSSH2 ' + 'error: Authentication failure'.red.bold +
' user=' + socket.request.session.username.yellow.bold.underline +
' from=' + socket.handshake.address.yellow.bold.underline)
socket.emit('allowreauth', socket.request.session.ssh.allowreauth)
socket.emit('reauth')
} else {
console.log('WebSSH2 Logout: user=' + socket.request.session.username + ' from=' + socket.handshake.address + ' host=' + socket.request.session.ssh.host + ' port=' + socket.request.session.ssh.port + ' sessionID=' + socket.request.sessionID + '/' + socket.id + ' allowreplay=' + socket.request.session.ssh.allowreplay + ' term=' + socket.request.session.ssh.term)
if (err) {
theError = (err) ? ': ' + err.message : ''
console.log('WebSSH2 error' + theError)
}
}
socket.emit('ssherror', 'SSH ' + myFunc + theError)
socket.request.session.destroy()
socket.disconnect(true)
} else {
theError = (err) ? ': ' + err.message : ''
socket.disconnect(true)
}
debugWebSSH2('SSHerror ' + myFunc + theError)
}
}

View file

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

150
app/socket.js Normal file
View file

@ -0,0 +1,150 @@
// app/socket.js
"use strict";
const debug = require("debug");
const debugWebSSH2 = require("debug")("WebSSH2");
const SSH = require("ssh2").Client;
const menuData = `
<a id="logBtn"><i class="fas fa-clipboard fa-fw"></i> Start Log</a>
<a id="downloadLogBtn"><i class="fas fa-download fa-fw"></i> Download Log</a>
`;
module.exports = function (io) {
io.on("connection", (socket) => {
let conn = null;
let stream = null;
console.log(`SOCKET CONNECT: ${socket.id}`);
// Remove existing listeners to prevent duplicates
socket.removeAllListeners("authenticate");
socket.removeAllListeners("data");
socket.removeAllListeners("resize");
socket.removeAllListeners("disconnect");
// Authenticate user
socket.on("authenticate", (credentials) => {
console.log(`SOCKET AUTHENTICATE: ${socket.id}`);
if (isValidCredentials(credentials)) {
console.log(`SOCKET AUTHENTICATE SUCCESS: ${socket.id}`);
initializeConnection(socket, credentials);
} else {
console.log(`SOCKET AUTHENTICATE FAILED: ${socket.id}`);
socket.emit("auth_result", {
success: false,
message: "Invalid credentials",
});
}
});
socket.on("disconnect", (reason) => {
debugWebSSH2(`SOCKET DISCONNECT: ${socket.id}, Reason: ${reason}`);
if (conn) {
conn.end();
}
// Clean up listeners
socket.removeAllListeners();
});
socket.on("data", (data) => {
if (stream) {
stream.write(data);
}
});
socket.on("resize", (data) => {
if (stream) {
stream.setWindow(data.rows, data.cols);
}
});
function initializeConnection(socket, credentials) {
if (conn) {
// If there's an existing connection, end it before creating a new one
conn.end();
}
conn = new SSH();
conn.on("ready", () => {
console.log(
`WebSSH2 Login: user=${credentials.username} from=${socket.handshake.address} host=${credentials.host} port=${credentials.port} sessionID=${socket.id}`
);
socket.emit("auth_result", { success: true });
socket.emit("menu", menuData);
socket.emit("title", `ssh://${credentials.host}`);
socket.emit("status", "SSH CONNECTION ESTABLISHED");
socket.emit("statusBackground", "green");
conn.shell(
{
term: credentials.term,
cols: credentials.cols,
rows: credentials.rows,
},
(err, str) => {
if (err) {
return SSHerror("EXEC ERROR", err);
}
stream = str;
stream.on("data", (data) => {
socket.emit("data", data.toString("utf-8"));
});
stream.on("close", (code, signal) => {
SSHerror("STREAM CLOSE", {
message:
code || signal
? `CODE: ${code} SIGNAL: ${signal}`
: undefined,
});
});
stream.stderr.on("data", (data) => {
console.log("STDERR: " + data);
});
}
);
});
conn.on("banner", (data) => {
socket.emit("data", data.replace(/\r?\n/g, "\r\n"));
});
conn.on("end", () => SSHerror("CONN END BY HOST"));
conn.on("close", () => SSHerror("CONN CLOSE"));
conn.on("error", (err) => SSHerror("CONN ERROR", err));
conn.connect({
host: credentials.host,
port: credentials.port,
username: credentials.username,
password: credentials.password,
tryKeyboard: true,
algorithms: credentials.algorithms,
readyTimeout: credentials.readyTimeout,
keepaliveInterval: credentials.keepaliveInterval,
keepaliveCountMax: credentials.keepaliveCountMax,
debug: debug("ssh2"),
});
}
function SSHerror(myFunc, err) {
const errorMessage = err ? `: ${err.message}` : "";
console.log(`WebSSH2 error: ${myFunc}${errorMessage}`);
socket.emit("ssherror", `SSH ${myFunc}${errorMessage}`);
if (conn) {
conn.end();
}
// Don't disconnect the socket here, let the client handle reconnection if necessary
// socket.disconnect(true);
}
function isValidCredentials(credentials) {
// Implement your credential validation logic here
return credentials && credentials.username && credentials.password;
}
});
};

13
app/util.js Normal file
View file

@ -0,0 +1,13 @@
"use strict";
// app/util.js
/* jshint esversion: 6, asi: true, node: true */
// private
require("colors"); // allow for color property extensions in log messages
var debug = require("debug")("WebSSH2");
var Auth = require("basic-auth");
// takes a string, makes it boolean (true if the string is true, false otherwise)
exports.parseBool = function parseBool(str) {
return str.toLowerCase() === "true";
};

30
index.js Normal file
View file

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

View file

@ -1,6 +1,6 @@
{ {
"name": "webssh2", "name": "webssh2-server",
"version": "0.2.12", "version": "0.2.13",
"ignore": [ "ignore": [
".gitignore" ".gitignore"
], ],
@ -47,46 +47,19 @@
}, },
"scripts": { "scripts": {
"start": "node index.js", "start": "node index.js",
"build": "webpack --progress --colors --config scripts/webpack.prod.js",
"builddev": "webpack --progress --colors --config scripts/webpack.dev.js",
"analyze": "webpack --json --config scripts/webpack.prod.js | webpack-bundle-size-analyzer",
"test": "snyk test",
"watch": "nodemon index.js", "watch": "nodemon index.js",
"standard": "standard --verbose --fix | snazzy", "standard": "standard --verbose --fix | snazzy",
"cleanmac": "find . -name '.DS_Store' -type f -delete" "cleanmac": "find . -name '.DS_Store' -type f -delete"
}, },
"standard": { "standard": {
"ignore": [ "ignore": [
"client/public/webssh2.bundle.js",
"bigip/*",
"screenshots/*",
"bin/*", "bin/*",
"build/*", "build/*"
"workspace/*"
] ]
}, },
"devDependencies": { "devDependencies": {
"@fortawesome/fontawesome-svg-core": "^1.2.12",
"@fortawesome/free-solid-svg-icons": "^5.6.3",
"clean-webpack-plugin": "^1.0.0",
"copy-webpack-plugin": "^4.6.0",
"cross-env": "^5.2.0",
"css-loader": "^2.1.0",
"extract-text-webpack-plugin": "^4.0.0-beta.0",
"file-loader": "^3.0.1",
"html-webpack-plugin": "^3.2.0",
"nodaemon": "0.0.5", "nodaemon": "0.0.5",
"postcss-discard-comments": "^4.0.1",
"snazzy": "^8.0.0", "snazzy": "^8.0.0",
"standard": "^12.0.1", "standard": "^12.0.1"
"style-loader": "^0.23.1",
"uglifyjs-webpack-plugin": "^2.1.1",
"url-loader": "^1.1.2",
"webpack": "^4.28.4",
"webpack-cli": "^3.2.1",
"webpack-merge": "^4.2.1",
"webpack-stream": "^5.2.1",
"xterm": "^3.10.1",
"zip-webpack-plugin": "^4.0.1"
} }
} }

Binary file not shown.

Before

Width:  |  Height:  |  Size: 129 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 131 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 186 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 187 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 130 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.5 MiB

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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