cp dines
This commit is contained in:
parent
b332d35bb1
commit
5d62a0bc1d
11 changed files with 319 additions and 597 deletions
|
@ -10,21 +10,9 @@
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div class="box">
|
<div class="box">
|
||||||
<div id="header"></div>
|
|
||||||
<div id="terminal-container" class="terminal"></div>
|
<div id="terminal-container" class="terminal"></div>
|
||||||
<div id="bottomdiv">
|
<div id="bottomdiv">
|
||||||
<div class="dropup" id="menu">
|
|
||||||
<i class="fas fa-bars fa-fw"></i> Menu
|
|
||||||
<div id="dropupContent" class="dropup-content">
|
|
||||||
<a id="logBtn"><i class="fas fa-clipboard fa-fw"></i> Start Log</a>
|
|
||||||
<a id="downloadLogBtn"><i class="fas fa-download fa-fw"></i> Download Log</a>
|
|
||||||
<a id="reauthBtn" style="display: none;"><i class="fas fa-key fa-fw"></i> Switch User</a>
|
|
||||||
<a id="credentialsBtn" style="display: none;"><i class="fas fa-key fa-fw"></i> Credentials</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div id="footer"></div>
|
|
||||||
<div id="status"></div>
|
<div id="status"></div>
|
||||||
<div id="countdown"></div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<script src="/ssh/webssh2.bundle.js" defer></script>
|
<script src="/ssh/webssh2.bundle.js" defer></script>
|
||||||
|
|
File diff suppressed because one or more lines are too long
|
@ -10,21 +10,9 @@
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div class="box">
|
<div class="box">
|
||||||
<div id="header"></div>
|
|
||||||
<div id="terminal-container" class="terminal"></div>
|
<div id="terminal-container" class="terminal"></div>
|
||||||
<div id="bottomdiv">
|
<div id="bottomdiv">
|
||||||
<div class="dropup" id="menu">
|
|
||||||
<i class="fas fa-bars fa-fw"></i> Menu
|
|
||||||
<div id="dropupContent" class="dropup-content">
|
|
||||||
<a id="logBtn"><i class="fas fa-clipboard fa-fw"></i> Start Log</a>
|
|
||||||
<a id="downloadLogBtn"><i class="fas fa-download fa-fw"></i> Download Log</a>
|
|
||||||
<a id="reauthBtn" style="display: none;"><i class="fas fa-key fa-fw"></i> Switch User</a>
|
|
||||||
<a id="credentialsBtn" style="display: none;"><i class="fas fa-key fa-fw"></i> Credentials</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div id="footer"></div>
|
|
||||||
<div id="status"></div>
|
<div id="status"></div>
|
||||||
<div id="countdown"></div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<script src="/ssh/webssh2.bundle.js" defer></script>
|
<script src="/ssh/webssh2.bundle.js" defer></script>
|
||||||
|
|
|
@ -15,8 +15,6 @@ require('../css/style.css');
|
||||||
/* global Blob, logBtn, credentialsBtn, reauthBtn, downloadLogBtn */ // eslint-disable-line
|
/* global Blob, logBtn, credentialsBtn, reauthBtn, downloadLogBtn */ // eslint-disable-line
|
||||||
let sessionLogEnable = false;
|
let sessionLogEnable = false;
|
||||||
let loggedData = false;
|
let loggedData = false;
|
||||||
let allowreplay = false;
|
|
||||||
let allowreauth = false;
|
|
||||||
let sessionLog: string;
|
let sessionLog: string;
|
||||||
let sessionFooter: any;
|
let sessionFooter: any;
|
||||||
let logDate: {
|
let logDate: {
|
||||||
|
@ -33,13 +31,8 @@ let errorExists: boolean;
|
||||||
const term = new Terminal();
|
const term = new Terminal();
|
||||||
// DOM properties
|
// DOM properties
|
||||||
const logBtn = document.getElementById('logBtn');
|
const logBtn = document.getElementById('logBtn');
|
||||||
const credentialsBtn = document.getElementById('credentialsBtn');
|
|
||||||
const reauthBtn = document.getElementById('reauthBtn');
|
|
||||||
const downloadLogBtn = document.getElementById('downloadLogBtn');
|
const downloadLogBtn = document.getElementById('downloadLogBtn');
|
||||||
const status = document.getElementById('status');
|
const status = document.getElementById('status');
|
||||||
const header = document.getElementById('header');
|
|
||||||
const footer = document.getElementById('footer');
|
|
||||||
const countdown = document.getElementById('countdown');
|
|
||||||
const fitAddon = new FitAddon();
|
const fitAddon = new FitAddon();
|
||||||
const terminalContainer = document.getElementById('terminal-container');
|
const terminalContainer = document.getElementById('terminal-container');
|
||||||
term.loadAddon(fitAddon);
|
term.loadAddon(fitAddon);
|
||||||
|
@ -49,10 +42,12 @@ fitAddon.fit();
|
||||||
|
|
||||||
const socket = io({
|
const socket = io({
|
||||||
path: '/ssh/socket.io',
|
path: '/ssh/socket.io',
|
||||||
|
transports: ['websocket'],
|
||||||
});
|
});
|
||||||
|
|
||||||
// reauthenticate
|
// reauthenticate
|
||||||
function reauthSession () { // eslint-disable-line
|
function reauthSession() {
|
||||||
|
// eslint-disable-line
|
||||||
debug('re-authenticating');
|
debug('re-authenticating');
|
||||||
socket.emit('control', 'reauth');
|
socket.emit('control', 'reauth');
|
||||||
window.location.href = '/ssh/reauth';
|
window.location.href = '/ssh/reauth';
|
||||||
|
@ -61,7 +56,8 @@ function reauthSession () { // eslint-disable-line
|
||||||
|
|
||||||
// cross browser method to "download" an element to the local system
|
// cross browser method to "download" an element to the local system
|
||||||
// used for our client-side logging feature
|
// used for our client-side logging feature
|
||||||
function downloadLog () { // eslint-disable-line
|
function downloadLog() {
|
||||||
|
// eslint-disable-line
|
||||||
if (loggedData === true) {
|
if (loggedData === true) {
|
||||||
myFile = `WebSSH2-${logDate.getFullYear()}${
|
myFile = `WebSSH2-${logDate.getFullYear()}${
|
||||||
logDate.getMonth() + 1
|
logDate.getMonth() + 1
|
||||||
|
@ -72,13 +68,13 @@ function downloadLog () { // eslint-disable-line
|
||||||
sessionLog.replace(
|
sessionLog.replace(
|
||||||
// eslint-disable-next-line no-control-regex
|
// eslint-disable-next-line no-control-regex
|
||||||
/[\u001b\u009b][[\]()#;?]*(?:\d{1,4}(?:;\d{0,4})*)?[0-9A-ORZcf-nqry=><;]/g,
|
/[\u001b\u009b][[\]()#;?]*(?:\d{1,4}(?:;\d{0,4})*)?[0-9A-ORZcf-nqry=><;]/g,
|
||||||
''
|
'',
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
{
|
{
|
||||||
// eslint-disable-line no-control-regex
|
// eslint-disable-line no-control-regex
|
||||||
type: 'text/plain',
|
type: 'text/plain',
|
||||||
}
|
},
|
||||||
);
|
);
|
||||||
const elem = window.document.createElement('a');
|
const elem = window.document.createElement('a');
|
||||||
elem.href = window.URL.createObjectURL(blob);
|
elem.href = window.URL.createObjectURL(blob);
|
||||||
|
@ -91,7 +87,8 @@ function downloadLog () { // eslint-disable-line
|
||||||
}
|
}
|
||||||
// Set variable to toggle log data from client/server to a varialble
|
// Set variable to toggle log data from client/server to a varialble
|
||||||
// for later download
|
// for later download
|
||||||
function toggleLog () { // eslint-disable-line
|
function toggleLog() {
|
||||||
|
// eslint-disable-line
|
||||||
if (sessionLogEnable === true) {
|
if (sessionLogEnable === true) {
|
||||||
sessionLogEnable = false;
|
sessionLogEnable = false;
|
||||||
loggedData = true;
|
loggedData = true;
|
||||||
|
@ -119,31 +116,14 @@ function toggleLog () { // eslint-disable-line
|
||||||
}
|
}
|
||||||
|
|
||||||
// replay password to server, requires
|
// replay password to server, requires
|
||||||
function replayCredentials () { // eslint-disable-line
|
function replayCredentials() {
|
||||||
|
// eslint-disable-line
|
||||||
socket.emit('control', 'replayCredentials');
|
socket.emit('control', 'replayCredentials');
|
||||||
debug(`control: replayCredentials`);
|
debug(`control: replayCredentials`);
|
||||||
term.focus();
|
term.focus();
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// draw/re-draw menu and reattach listeners
|
|
||||||
// when dom is changed, listeners are abandonded
|
|
||||||
function drawMenu() {
|
|
||||||
logBtn.addEventListener('click', toggleLog);
|
|
||||||
if (allowreauth) {
|
|
||||||
reauthBtn.addEventListener('click', reauthSession);
|
|
||||||
reauthBtn.style.display = 'block';
|
|
||||||
}
|
|
||||||
if (allowreplay) {
|
|
||||||
credentialsBtn.addEventListener('click', replayCredentials);
|
|
||||||
credentialsBtn.style.display = 'block';
|
|
||||||
}
|
|
||||||
if (loggedData) {
|
|
||||||
downloadLogBtn.addEventListener('click', downloadLog);
|
|
||||||
downloadLogBtn.style.display = 'block';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function resizeScreen() {
|
function resizeScreen() {
|
||||||
fitAddon.fit();
|
fitAddon.fit();
|
||||||
socket.emit('resize', { cols: term.cols, rows: term.rows });
|
socket.emit('resize', { cols: term.cols, rows: term.rows });
|
||||||
|
@ -181,71 +161,59 @@ socket.on(
|
||||||
lineHeight: number;
|
lineHeight: number;
|
||||||
}) => {
|
}) => {
|
||||||
term.options = data;
|
term.options = data;
|
||||||
}
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
socket.on('title', (data: string) => {
|
// socket.on('ssherror', (data: string) => {
|
||||||
document.title = data;
|
// status.innerHTML = data;
|
||||||
});
|
// status.style.backgroundColor = 'red';
|
||||||
|
// errorExists = true;
|
||||||
|
// });
|
||||||
|
|
||||||
socket.on('menu', () => {
|
// socket.on('headerBackground', (data: string) => {
|
||||||
drawMenu();
|
// header.style.backgroundColor = data;
|
||||||
});
|
// });
|
||||||
|
|
||||||
socket.on('status', (data: string) => {
|
// socket.on('header', (data: string) => {
|
||||||
status.innerHTML = 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('ssherror', (data: string) => {
|
// socket.on('footer', (data: string) => {
|
||||||
status.innerHTML = data;
|
// sessionFooter = data;
|
||||||
status.style.backgroundColor = 'red';
|
// footer.innerHTML = data;
|
||||||
errorExists = true;
|
// });
|
||||||
});
|
|
||||||
|
|
||||||
socket.on('headerBackground', (data: string) => {
|
// socket.on('statusBackground', (data: string) => {
|
||||||
header.style.backgroundColor = data;
|
// status.style.backgroundColor = data;
|
||||||
});
|
// });
|
||||||
|
|
||||||
socket.on('header', (data: string) => {
|
// socket.on('allowreplay', (data: boolean) => {
|
||||||
if (data) {
|
// if (data === true) {
|
||||||
header.innerHTML = data;
|
// debug(`allowreplay: ${data}`);
|
||||||
header.style.display = 'block';
|
// allowreplay = true;
|
||||||
// header is 19px and footer is 19px, recaculate new terminal-container and resize
|
// drawMenu();
|
||||||
terminalContainer.style.height = 'calc(100% - 38px)';
|
// } else {
|
||||||
resizeScreen();
|
// allowreplay = false;
|
||||||
}
|
// debug(`allowreplay: ${data}`);
|
||||||
});
|
// }
|
||||||
|
// });
|
||||||
|
|
||||||
socket.on('footer', (data: string) => {
|
// socket.on('allowreauth', (data: boolean) => {
|
||||||
sessionFooter = data;
|
// if (data === true) {
|
||||||
footer.innerHTML = data;
|
// debug(`allowreauth: ${data}`);
|
||||||
});
|
// allowreauth = true;
|
||||||
|
// drawMenu();
|
||||||
socket.on('statusBackground', (data: string) => {
|
// } else {
|
||||||
status.style.backgroundColor = data;
|
// allowreauth = false;
|
||||||
});
|
// debug(`allowreauth: ${data}`);
|
||||||
|
// }
|
||||||
socket.on('allowreplay', (data: boolean) => {
|
// });
|
||||||
if (data === true) {
|
|
||||||
debug(`allowreplay: ${data}`);
|
|
||||||
allowreplay = true;
|
|
||||||
drawMenu();
|
|
||||||
} else {
|
|
||||||
allowreplay = false;
|
|
||||||
debug(`allowreplay: ${data}`);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
socket.on('allowreauth', (data: boolean) => {
|
|
||||||
if (data === true) {
|
|
||||||
debug(`allowreauth: ${data}`);
|
|
||||||
allowreauth = true;
|
|
||||||
drawMenu();
|
|
||||||
} else {
|
|
||||||
allowreauth = false;
|
|
||||||
debug(`allowreauth: ${data}`);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
socket.on('disconnect', (err: any) => {
|
socket.on('disconnect', (err: any) => {
|
||||||
if (!errorExists) {
|
if (!errorExists) {
|
||||||
|
@ -253,33 +221,31 @@ socket.on('disconnect', (err: any) => {
|
||||||
status.innerHTML = `WEBSOCKET SERVER DISCONNECTED: ${err}`;
|
status.innerHTML = `WEBSOCKET SERVER DISCONNECTED: ${err}`;
|
||||||
}
|
}
|
||||||
socket.io.reconnection(false);
|
socket.io.reconnection(false);
|
||||||
countdown.classList.remove('active');
|
|
||||||
});
|
});
|
||||||
|
|
||||||
socket.on('error', (err: any) => {
|
// socket.on('error', (err: any) => {
|
||||||
if (!errorExists) {
|
// if (!errorExists) {
|
||||||
status.style.backgroundColor = 'red';
|
// status.style.backgroundColor = 'red';
|
||||||
status.innerHTML = `ERROR: ${err}`;
|
// status.innerHTML = `ERROR: ${err}`;
|
||||||
}
|
// }
|
||||||
});
|
// });
|
||||||
|
|
||||||
socket.on('reauth', () => {
|
// socket.on('reauth', () => {
|
||||||
if (allowreauth) {
|
// if (allowreauth) {
|
||||||
reauthSession();
|
// reauthSession();
|
||||||
}
|
// }
|
||||||
});
|
// });
|
||||||
|
|
||||||
// safe shutdown
|
// safe shutdown
|
||||||
let hasCountdownStarted = false;
|
|
||||||
|
|
||||||
socket.on('shutdownCountdownUpdate', (remainingSeconds: any) => {
|
// socket.on('shutdownCountdownUpdate', (remainingSeconds: any) => {
|
||||||
if (!hasCountdownStarted) {
|
// if (!hasCountdownStarted) {
|
||||||
countdown.classList.add('active');
|
// countdown.classList.add('active');
|
||||||
hasCountdownStarted = true;
|
// hasCountdownStarted = true;
|
||||||
}
|
// }
|
||||||
countdown.innerText = `Shutting down in ${remainingSeconds}s`;
|
// countdown.innerText = `Shutting down in ${remainingSeconds}s`;
|
||||||
});
|
// });
|
||||||
|
|
||||||
term.onTitleChange((title) => {
|
// term.onTitleChange((title) => {
|
||||||
document.title = title;
|
// document.title = title;
|
||||||
});
|
// });
|
||||||
|
|
|
@ -6,7 +6,7 @@
|
||||||
"socketio": {
|
"socketio": {
|
||||||
"serveClient": false,
|
"serveClient": false,
|
||||||
"path": "/ssh/socket.io",
|
"path": "/ssh/socket.io",
|
||||||
"origins": ["localhost:2222"]
|
"origins": ["localhost:2224"]
|
||||||
},
|
},
|
||||||
"user": {
|
"user": {
|
||||||
"name": null,
|
"name": null,
|
||||||
|
@ -62,16 +62,8 @@
|
||||||
"aes256-gcm@openssh.com",
|
"aes256-gcm@openssh.com",
|
||||||
"aes256-cbc"
|
"aes256-cbc"
|
||||||
],
|
],
|
||||||
"hmac": [
|
"hmac": ["hmac-sha2-256", "hmac-sha2-512", "hmac-sha1"],
|
||||||
"hmac-sha2-256",
|
"compress": ["none", "zlib@openssh.com", "zlib"]
|
||||||
"hmac-sha2-512",
|
|
||||||
"hmac-sha1"
|
|
||||||
],
|
|
||||||
"compress": [
|
|
||||||
"none",
|
|
||||||
"zlib@openssh.com",
|
|
||||||
"zlib"
|
|
||||||
]
|
|
||||||
},
|
},
|
||||||
"serverlog": {
|
"serverlog": {
|
||||||
"client": false,
|
"client": false,
|
||||||
|
|
|
@ -1,36 +1,15 @@
|
||||||
{
|
{
|
||||||
"name": "webssh2",
|
"name": "myssh",
|
||||||
"version": "0.6.0-pre-1",
|
"version": "0.6.0-pre-1",
|
||||||
"ignore": [
|
"ignore": [
|
||||||
".gitignore"
|
".gitignore"
|
||||||
],
|
],
|
||||||
"bin": "./index.js",
|
"bin": "./index.js",
|
||||||
"description": "A Websocket to SSH2 gateway using term.js, socket.io, ssh2, and express",
|
|
||||||
"homepage": "https://github.com/billchurch/WebSSH2",
|
|
||||||
"keywords": [
|
|
||||||
"ssh",
|
|
||||||
"webssh",
|
|
||||||
"terminal",
|
|
||||||
"webterminal"
|
|
||||||
],
|
|
||||||
"license": "SEE LICENSE IN FILE - LICENSE",
|
"license": "SEE LICENSE IN FILE - LICENSE",
|
||||||
"private": false,
|
"private": false,
|
||||||
"repository": {
|
|
||||||
"type": "git",
|
|
||||||
"url": "git+https://github.com/billchurch/WebSSH2.git"
|
|
||||||
},
|
|
||||||
"contributors": [
|
|
||||||
{
|
|
||||||
"name": "Bill Church",
|
|
||||||
"email": "wmchurch@gmail.com"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">= 14"
|
"node": ">= 14"
|
||||||
},
|
},
|
||||||
"bugs": {
|
|
||||||
"url": "https://github.com/billchurch/WebSSH2/issues"
|
|
||||||
},
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@runloop/api-client": "^0.1.0-alpha.23",
|
"@runloop/api-client": "^0.1.0-alpha.23",
|
||||||
"basic-auth": "~2.0.1",
|
"basic-auth": "~2.0.1",
|
||||||
|
@ -42,7 +21,7 @@
|
||||||
"node-forge": "^1.3.1",
|
"node-forge": "^1.3.1",
|
||||||
"read-config-ng": "^3.0.7",
|
"read-config-ng": "^3.0.7",
|
||||||
"serve-favicon": "^2.5.0",
|
"serve-favicon": "^2.5.0",
|
||||||
"socket.io": "^4.7.5",
|
"socket.io": "4.7.5",
|
||||||
"ssh2": "^1.15.0",
|
"ssh2": "^1.15.0",
|
||||||
"validator": "^13.11.0",
|
"validator": "^13.11.0",
|
||||||
"winston": "^3.13.0"
|
"winston": "^3.13.0"
|
||||||
|
|
|
@ -11,22 +11,41 @@ const nodeRoot = path.dirname(require.main.filename);
|
||||||
const publicPath = path.join(nodeRoot, 'client', 'public');
|
const publicPath = path.join(nodeRoot, 'client', 'public');
|
||||||
const express = require('express');
|
const express = require('express');
|
||||||
const logger = require('morgan');
|
const logger = require('morgan');
|
||||||
|
const crypto = require('crypto');
|
||||||
|
|
||||||
|
const expressConfig = {
|
||||||
|
secret: crypto.randomBytes(20).toString('hex'),
|
||||||
|
name: 'WebSSH2',
|
||||||
|
resave: true,
|
||||||
|
saveUninitialized: false,
|
||||||
|
unset: 'destroy',
|
||||||
|
ssh: {
|
||||||
|
dotfiles: 'ignore',
|
||||||
|
etag: false,
|
||||||
|
extensions: ['htm', 'html'],
|
||||||
|
index: false,
|
||||||
|
maxAge: '1s',
|
||||||
|
redirect: false,
|
||||||
|
setHeaders(res) {
|
||||||
|
res.set('x-timestamp', Date.now());
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
const app = express();
|
const app = express();
|
||||||
const server = require('http').createServer(app);
|
const server = require('http').createServer(app);
|
||||||
const favicon = require('serve-favicon');
|
const io = require('socket.io')(server, { transports: ['websocket'], ...config.socketio });
|
||||||
const io = require('socket.io')(server, config.socketio);
|
const session = require('express-session')(expressConfig);
|
||||||
const session = require('express-session')(config.express);
|
|
||||||
|
|
||||||
const appSocket = require('./socket');
|
const appSocket = require('./socket');
|
||||||
const { setDefaultCredentials, basicAuth } = require('./util');
|
// const { setDefaultCredentials } = require('./util');
|
||||||
const { webssh2debug } = require('./logging');
|
const { webssh2debug } = require('./logging');
|
||||||
const { reauth, connect, notfound, handleErrors } = require('./routes');
|
const { connect } = require('./routes');
|
||||||
|
|
||||||
setDefaultCredentials(config.user);
|
// setDefaultCredentials(config.user);
|
||||||
|
|
||||||
// safe shutdown
|
// safe shutdown
|
||||||
let remainingSeconds = config.safeShutdownDuration;
|
|
||||||
let shutdownMode = false;
|
let shutdownMode = false;
|
||||||
let shutdownInterval;
|
let shutdownInterval;
|
||||||
let connectionCount = 0;
|
let connectionCount = 0;
|
||||||
|
@ -37,19 +56,18 @@ function safeShutdownGuard(req, res, next) {
|
||||||
}
|
}
|
||||||
// express
|
// express
|
||||||
app.use(safeShutdownGuard);
|
app.use(safeShutdownGuard);
|
||||||
app.use(session);
|
// app.use(session);
|
||||||
if (config.accesslog) app.use(logger('common'));
|
if (config.accesslog) app.use(logger('common'));
|
||||||
app.disable('x-powered-by');
|
app.disable('x-powered-by');
|
||||||
app.use(favicon(path.join(publicPath, 'favicon.ico')));
|
|
||||||
app.use(express.urlencoded({ extended: true }));
|
app.use(express.urlencoded({ extended: true }));
|
||||||
app.post('/ssh/host/:host?', connect);
|
app.post('/ssh/host/:host?', connect);
|
||||||
app.post('/ssh', express.static(publicPath, config.express.ssh));
|
app.post('/ssh', express.static(publicPath, expressConfig.ssh));
|
||||||
app.use('/ssh', express.static(publicPath, config.express.ssh));
|
app.use('/ssh', express.static(publicPath, expressConfig.ssh));
|
||||||
app.use(basicAuth);
|
//app.use(basicAuth);
|
||||||
app.get('/ssh/reauth', reauth);
|
//app.get('/ssh/reauth', reauth);
|
||||||
app.get('/ssh/host/:host?', connect);
|
app.get('/ssh/host/:host?', connect);
|
||||||
app.use(notfound);
|
// app.use(notfound);
|
||||||
app.use(handleErrors);
|
// app.use(handleErrors);
|
||||||
|
|
||||||
// clean stop
|
// clean stop
|
||||||
function stopApp(reason) {
|
function stopApp(reason) {
|
||||||
|
@ -63,36 +81,10 @@ function stopApp(reason) {
|
||||||
// bring up socket
|
// bring up socket
|
||||||
io.on('connection', appSocket);
|
io.on('connection', appSocket);
|
||||||
|
|
||||||
// socket.io
|
|
||||||
// expose express session with socket.request.session
|
|
||||||
io.use((socket, next) => {
|
|
||||||
socket.request.res ? session(socket.request, socket.request.res, next) : next(next); // eslint disable-line
|
|
||||||
});
|
|
||||||
|
|
||||||
function countdownTimer() {
|
|
||||||
if (!shutdownMode) clearInterval(shutdownInterval);
|
|
||||||
remainingSeconds -= 1;
|
|
||||||
if (remainingSeconds <= 0) {
|
|
||||||
stopApp('Countdown is over');
|
|
||||||
} else io.emit('shutdownCountdownUpdate', remainingSeconds);
|
|
||||||
}
|
|
||||||
|
|
||||||
const signals = ['SIGTERM', 'SIGINT'];
|
|
||||||
signals.forEach((signal) =>
|
|
||||||
process.on(signal, () => {
|
|
||||||
if (shutdownMode) stopApp('Safe shutdown aborted, force quitting');
|
|
||||||
if (!connectionCount > 0) stopApp('All connections ended');
|
|
||||||
shutdownMode = true;
|
|
||||||
console.error(
|
|
||||||
`\r\n${connectionCount} client(s) are still connected.\r\nStarting a ${remainingSeconds} seconds countdown.\r\nPress Ctrl+C again to force quit`
|
|
||||||
);
|
|
||||||
if (!shutdownInterval) shutdownInterval = setInterval(countdownTimer, 1000);
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
module.exports = { server, config };
|
module.exports = { server, config };
|
||||||
|
|
||||||
const onConnection = (socket) => {
|
const onConnection = (socket) => {
|
||||||
|
console.log('connected');
|
||||||
connectionCount += 1;
|
connectionCount += 1;
|
||||||
socket.on('disconnect', () => {
|
socket.on('disconnect', () => {
|
||||||
connectionCount -= 1;
|
connectionCount -= 1;
|
||||||
|
@ -102,9 +94,9 @@ const onConnection = (socket) => {
|
||||||
});
|
});
|
||||||
socket.on('geometry', (cols, rows) => {
|
socket.on('geometry', (cols, rows) => {
|
||||||
// TODO need to rework how we pass settings to ssh2, this is less than ideal
|
// TODO need to rework how we pass settings to ssh2, this is less than ideal
|
||||||
socket.request.session.ssh.cols = cols;
|
//socket.request.session.ssh.cols = cols; //TODO make this part of the terminal config on connect
|
||||||
socket.request.session.ssh.rows = rows;
|
//socket.request.session.ssh.rows = rows;
|
||||||
webssh2debug(socket, `SOCKET GEOMETRY: termCols = ${cols}, termRows = ${rows}`);
|
//webssh2debug(socket, `SOCKET GEOMETRY: termCols = ${cols}, termRows = ${rows}`);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -20,7 +20,7 @@ const configDefault = {
|
||||||
socketio: {
|
socketio: {
|
||||||
serveClient: false,
|
serveClient: false,
|
||||||
path: '/ssh/socket.io',
|
path: '/ssh/socket.io',
|
||||||
origins: ['localhost:2222'],
|
origins: ['localhost:*'],
|
||||||
},
|
},
|
||||||
express: {
|
express: {
|
||||||
secret: crypto.randomBytes(20).toString('hex'),
|
secret: crypto.randomBytes(20).toString('hex'),
|
||||||
|
@ -103,7 +103,7 @@ const configDefault = {
|
||||||
try {
|
try {
|
||||||
if (!fs.existsSync(configPath)) {
|
if (!fs.existsSync(configPath)) {
|
||||||
console.error(
|
console.error(
|
||||||
`\n\nERROR: Missing config.json for WebSSH2. Current config: ${util.inspect(myConfig)}`
|
`\n\nERROR: Missing config.json for WebSSH2. Current config: ${util.inspect(myConfig)}`,
|
||||||
);
|
);
|
||||||
console.error('\n See config.json.sample for details\n\n');
|
console.error('\n See config.json.sample for details\n\n');
|
||||||
}
|
}
|
||||||
|
@ -115,7 +115,7 @@ try {
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
myConfig = configDefault;
|
myConfig = configDefault;
|
||||||
console.error(
|
console.error(
|
||||||
`\n\nERROR: Missing config.json for WebSSH2. Current config: ${util.inspect(myConfig)}`
|
`\n\nERROR: Missing config.json for WebSSH2. Current config: ${util.inspect(myConfig)}`,
|
||||||
);
|
);
|
||||||
console.error('\n See config.json.sample for details\n\n');
|
console.error('\n See config.json.sample for details\n\n');
|
||||||
console.error(`ERROR:\n\n ${err}`);
|
console.error(`ERROR:\n\n ${err}`);
|
||||||
|
|
|
@ -9,195 +9,110 @@ const publicPath = path.join(nodeRoot, 'client', 'public');
|
||||||
const { parseBool } = require('./util');
|
const { parseBool } = require('./util');
|
||||||
const config = require('./config');
|
const config = require('./config');
|
||||||
|
|
||||||
exports.reauth = function reauth(req, res) {
|
// exports.reauth = function reauth(req, res) {
|
||||||
let { referer } = req.headers;
|
// let { referer } = req.headers;
|
||||||
if (!validator.isURL(referer, { host_whitelist: ['localhost'] })) {
|
// if (!validator.isURL(referer, { host_whitelist: ['localhost'] })) {
|
||||||
console.error(
|
// console.error(
|
||||||
`WebSSH2 (${req.sessionID}) ERROR: Referrer '${referer}' for '/reauth' invalid. Setting to '/' which will probably fail.`
|
// `WebSSH2 (${req.sessionID}) ERROR: Referrer '${referer}' for '/reauth' invalid. Setting to '/' which will probably fail.`,
|
||||||
);
|
// );
|
||||||
referer = '/';
|
// referer = '/';
|
||||||
}
|
// }
|
||||||
res
|
// res
|
||||||
.status(401)
|
// .status(401)
|
||||||
.send(
|
// .send(
|
||||||
`<!DOCTYPE html><html><head><meta http-equiv="refresh" content="0; url=${referer}"></head><body bgcolor="#000"></body></html>`
|
// `<!DOCTYPE html><html><head><meta http-equiv="refresh" content="0; url=${referer}"></head><body bgcolor="#000"></body></html>`,
|
||||||
);
|
// );
|
||||||
};
|
// };
|
||||||
|
|
||||||
exports.connect = function connect(req, res) {
|
exports.connect = function connect(req, res) {
|
||||||
res.sendFile(path.join(path.join(publicPath, 'client.htm')));
|
res.sendFile(path.join(path.join(publicPath, 'client.htm')));
|
||||||
|
|
||||||
let { host, port } = config.ssh;
|
// let { host, port } = config.ssh;
|
||||||
let { text: header, background: headerBackground } = config.header;
|
// let { text: header, background: headerBackground } = config.header;
|
||||||
let { term: sshterm, readyTimeout } = config.ssh;
|
// let { term: sshterm, readyTimeout } = config.ssh;
|
||||||
let {
|
// let {
|
||||||
cursorBlink,
|
// cursorBlink,
|
||||||
scrollback,
|
// scrollback,
|
||||||
tabStopWidth,
|
// tabStopWidth,
|
||||||
bellStyle,
|
// bellStyle,
|
||||||
fontSize,
|
// fontSize,
|
||||||
fontFamily,
|
// fontFamily,
|
||||||
letterSpacing,
|
// letterSpacing,
|
||||||
lineHeight,
|
// lineHeight,
|
||||||
} = config.terminal;
|
// } = config.terminal;
|
||||||
|
|
||||||
// capture, assign, and validate variables
|
// capture, assign, and validate variables
|
||||||
|
|
||||||
if (req.params?.host) {
|
// if (req.params?.host) {
|
||||||
if (
|
// if (
|
||||||
validator.isIP(`${req.params.host}`) ||
|
// validator.isIP(`${req.params.host}`) ||
|
||||||
validator.isFQDN(req.params.host) ||
|
// validator.isFQDN(req.params.host) ||
|
||||||
/^(([a-z]|[A-Z]|\d|[!^(){}\-_~])+)?\w$/.test(req.params.host)
|
// /^(([a-z]|[A-Z]|\d|[!^(){}\-_~])+)?\w$/.test(req.params.host)
|
||||||
) {
|
// ) {
|
||||||
host = req.params.host;
|
// host = req.params.host;
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
|
|
||||||
if (req.method === 'POST' && req.body.username && req.body.userpassword) {
|
// //// ADding exta
|
||||||
req.session.username = req.body.username;
|
// if (req.params?.devboxID) {
|
||||||
req.session.userpassword = req.body.userpassword;
|
// devboxID = req.params.devboxID;
|
||||||
|
// }
|
||||||
|
// if (req.params?.supabaseAuth) {
|
||||||
|
// supaBaseAuth = req.params.supaBaseAuth;
|
||||||
|
// }
|
||||||
|
|
||||||
if (req.body.port && validator.isInt(`${req.body.port}`, { min: 1, max: 65535 }))
|
// req.session.ssh = {
|
||||||
port = req.body.port;
|
// host,
|
||||||
|
// port,
|
||||||
|
// localAddress: config.ssh.localAddress,
|
||||||
|
// localPort: config.ssh.localPort,
|
||||||
|
// header: {
|
||||||
|
// name: header,
|
||||||
|
// background: headerBackground,
|
||||||
|
// },
|
||||||
|
// algorithms: config.algorithms,
|
||||||
|
// keepaliveInterval: config.ssh.keepaliveInterval,
|
||||||
|
// keepaliveCountMax: config.ssh.keepaliveCountMax,
|
||||||
|
// allowedSubnets: config.ssh.allowedSubnets,
|
||||||
|
// term: sshterm,
|
||||||
|
// terminal: {
|
||||||
|
// cursorBlink,
|
||||||
|
// scrollback,
|
||||||
|
// tabStopWidth,
|
||||||
|
// bellStyle,
|
||||||
|
// fontSize,
|
||||||
|
// fontFamily,
|
||||||
|
// letterSpacing,
|
||||||
|
// lineHeight,
|
||||||
|
// },
|
||||||
|
// cols: null,
|
||||||
|
// rows: null,
|
||||||
|
// allowreplay:
|
||||||
|
// config.options.challengeButton ||
|
||||||
|
// (validator.isBoolean(`${req.headers.allowreplay}`)
|
||||||
|
// ? 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,
|
||||||
|
// };
|
||||||
|
// 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);
|
||||||
|
// };
|
||||||
|
|
||||||
if (req.body.header) header = req.body.header;
|
// exports.notfound = function notfound(_req, res) {
|
||||||
|
// res.status(404).send("Sorry, can't find that!");
|
||||||
|
// };
|
||||||
|
|
||||||
if (req.body.headerBackground) {
|
// exports.handleErrors = function handleErrors(err, _req, res) {
|
||||||
headerBackground = req.body.headerBackground;
|
// console.error(err.stack);
|
||||||
console.log(`background: ${req.body.headerBackground}`);
|
// res.status(500).send('Something broke!');
|
||||||
}
|
// };
|
||||||
|
|
||||||
if (req.body.sshterm && /^(([a-z]|[A-Z]|\d|[!^(){}\-_~])+)?\w$/.test(req.body.sshterm))
|
|
||||||
sshterm = req.body.sshterm;
|
|
||||||
|
|
||||||
if (req.body.cursorBlink && validator.isBoolean(`${req.body.cursorBlink}`))
|
|
||||||
cursorBlink = parseBool(req.body.cursorBlink);
|
|
||||||
|
|
||||||
if (req.body.scrollback && validator.isInt(`${req.body.scrollback}`, { min: 1, max: 200000 }))
|
|
||||||
scrollback = req.body.scrollback;
|
|
||||||
|
|
||||||
if (req.body.tabStopWidth && validator.isInt(`${req.body.tabStopWidth}`, { min: 1, max: 100 }))
|
|
||||||
tabStopWidth = req.body.tabStopWidth;
|
|
||||||
|
|
||||||
if (req.body.bellStyle && ['sound', 'none'].indexOf(req.body.bellStyle) > -1)
|
|
||||||
bellStyle = req.body.bellStyle;
|
|
||||||
|
|
||||||
if (
|
|
||||||
req.body.readyTimeout &&
|
|
||||||
validator.isInt(`${req.body.readyTimeout}`, { min: 1, max: 300000 })
|
|
||||||
)
|
|
||||||
readyTimeout = req.body.readyTimeout;
|
|
||||||
|
|
||||||
if (req.body.fontSize && validator.isNumeric(`${req.body.fontSize}`))
|
|
||||||
fontSize = req.body.fontSize;
|
|
||||||
|
|
||||||
if (req.body.fontFamily) fontFamily = req.body.fontFamily;
|
|
||||||
|
|
||||||
if (req.body.letterSpacing && validator.isNumeric(`${req.body.letterSpacing}`))
|
|
||||||
letterSpacing = req.body.letterSpacing;
|
|
||||||
|
|
||||||
if (req.body.lineHeight && validator.isNumeric(`${req.body.lineHeight}`))
|
|
||||||
lineHeight = req.body.lineHeight;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (req.method === 'GET') {
|
|
||||||
if (req.query?.port && validator.isInt(`${req.query.port}`, { min: 1, max: 65535 }))
|
|
||||||
port = req.query.port;
|
|
||||||
|
|
||||||
if (req.query?.header) header = req.query.header;
|
|
||||||
|
|
||||||
if (req.query?.headerBackground) headerBackground = req.query.headerBackground;
|
|
||||||
|
|
||||||
if (req.query?.sshterm && /^(([a-z]|[A-Z]|\d|[!^(){}\-_~])+)?\w$/.test(req.query.sshterm))
|
|
||||||
sshterm = req.query.sshterm;
|
|
||||||
|
|
||||||
if (req.query?.cursorBlink && validator.isBoolean(`${req.query.cursorBlink}`))
|
|
||||||
cursorBlink = parseBool(req.query.cursorBlink);
|
|
||||||
|
|
||||||
if (
|
|
||||||
req.query?.scrollback &&
|
|
||||||
validator.isInt(`${req.query.scrollback}`, { min: 1, max: 200000 })
|
|
||||||
)
|
|
||||||
scrollback = req.query.scrollback;
|
|
||||||
|
|
||||||
if (
|
|
||||||
req.query?.tabStopWidth &&
|
|
||||||
validator.isInt(`${req.query.tabStopWidth}`, { min: 1, max: 100 })
|
|
||||||
)
|
|
||||||
tabStopWidth = req.query.tabStopWidth;
|
|
||||||
|
|
||||||
if (req.query?.bellStyle && ['sound', 'none'].indexOf(req.query.bellStyle) > -1)
|
|
||||||
bellStyle = req.query.bellStyle;
|
|
||||||
|
|
||||||
if (
|
|
||||||
req.query?.readyTimeout &&
|
|
||||||
validator.isInt(`${req.query.readyTimeout}`, { min: 1, max: 300000 })
|
|
||||||
)
|
|
||||||
readyTimeout = req.query.readyTimeout;
|
|
||||||
|
|
||||||
if (req.query?.fontSize && validator.isNumeric(`${req.query.fontSize}`))
|
|
||||||
fontSize = req.query.fontSize;
|
|
||||||
|
|
||||||
if (req.query?.fontFamily) fontFamily = req.query.fontFamily;
|
|
||||||
|
|
||||||
if (req.query?.lineHeight && validator.isNumeric(`${req.query.lineHeight}`))
|
|
||||||
lineHeight = req.query.lineHeight;
|
|
||||||
|
|
||||||
if (req.query?.letterSpacing && validator.isNumeric(`${req.query.letterSpacing}`))
|
|
||||||
letterSpacing = req.query.letterSpacing;
|
|
||||||
}
|
|
||||||
|
|
||||||
req.session.ssh = {
|
|
||||||
host,
|
|
||||||
port,
|
|
||||||
localAddress: config.ssh.localAddress,
|
|
||||||
localPort: config.ssh.localPort,
|
|
||||||
header: {
|
|
||||||
name: header,
|
|
||||||
background: headerBackground,
|
|
||||||
},
|
|
||||||
algorithms: config.algorithms,
|
|
||||||
keepaliveInterval: config.ssh.keepaliveInterval,
|
|
||||||
keepaliveCountMax: config.ssh.keepaliveCountMax,
|
|
||||||
allowedSubnets: config.ssh.allowedSubnets,
|
|
||||||
term: sshterm,
|
|
||||||
terminal: {
|
|
||||||
cursorBlink,
|
|
||||||
scrollback,
|
|
||||||
tabStopWidth,
|
|
||||||
bellStyle,
|
|
||||||
fontSize,
|
|
||||||
fontFamily,
|
|
||||||
letterSpacing,
|
|
||||||
lineHeight,
|
|
||||||
},
|
|
||||||
cols: null,
|
|
||||||
rows: null,
|
|
||||||
allowreplay:
|
|
||||||
config.options.challengeButton ||
|
|
||||||
(validator.isBoolean(`${req.headers.allowreplay}`)
|
|
||||||
? 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,
|
|
||||||
};
|
|
||||||
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);
|
|
||||||
};
|
|
||||||
|
|
||||||
exports.notfound = function notfound(_req, res) {
|
|
||||||
res.status(404).send("Sorry, can't find that!");
|
|
||||||
};
|
|
||||||
|
|
||||||
exports.handleErrors = function handleErrors(err, _req, res) {
|
|
||||||
console.error(err.stack);
|
|
||||||
res.status(500).send('Something broke!');
|
|
||||||
};
|
};
|
||||||
|
|
|
@ -4,6 +4,7 @@ const SSH = require('ssh2').Client;
|
||||||
const CIDRMatcher = require('cidr-matcher');
|
const CIDRMatcher = require('cidr-matcher');
|
||||||
const validator = require('validator');
|
const validator = require('validator');
|
||||||
const dnsPromises = require('dns').promises;
|
const dnsPromises = require('dns').promises;
|
||||||
|
const debug = require('debug');
|
||||||
const { Client } = require('ssh2');
|
const { Client } = require('ssh2');
|
||||||
const tls = require('tls');
|
const tls = require('tls');
|
||||||
const forge = require('node-forge');
|
const forge = require('node-forge');
|
||||||
|
@ -14,7 +15,6 @@ function convertPKCS8toPKCS1(pkcs8Key) {
|
||||||
|
|
||||||
// Convert the private key to PKCS#1 format
|
// Convert the private key to PKCS#1 format
|
||||||
const pkcs1Pem = forge.pki.privateKeyToPem(privateKeyInfo);
|
const pkcs1Pem = forge.pki.privateKeyToPem(privateKeyInfo);
|
||||||
|
|
||||||
return pkcs1Pem;
|
return pkcs1Pem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -27,9 +27,7 @@ const proxyConnect = (hostname, callback) => {
|
||||||
host: 'ssh.runloop.pro', // Proxy server address
|
host: 'ssh.runloop.pro', // Proxy server address
|
||||||
port: 443, // Proxy port (HTTPS over TLS)
|
port: 443, // Proxy port (HTTPS over TLS)
|
||||||
servername: hostname, // Target hostname, acts like -servername in openssl
|
servername: hostname, // Target hostname, acts like -servername in openssl
|
||||||
checkServerIdentity: () => {
|
checkServerIdentity: () => undefined, // Disable hostname validation
|
||||||
return undefined;
|
|
||||||
}, // Disable hostname validation
|
|
||||||
},
|
},
|
||||||
() => {
|
() => {
|
||||||
console.log('TLS connection established');
|
console.log('TLS connection established');
|
||||||
|
@ -44,18 +42,16 @@ const proxyConnect = (hostname, callback) => {
|
||||||
};
|
};
|
||||||
|
|
||||||
// Main function to establish the SSH connection over the TLS proxy
|
// Main function to establish the SSH connection over the TLS proxy
|
||||||
async function establishConnection() {
|
async function establishConnection(targetDevbox, bearerToken) {
|
||||||
const runloop = new Runloop({
|
const runloop = new Runloop({
|
||||||
baseURL: 'https://api.runloop.pro',
|
baseURL: 'https://api.runloop.pro',
|
||||||
// This is gotten by just inspecting the browser cookies on platform.runloop.pro
|
// This is gotten by just inspecting the browser cookies on platform.runloop.pro
|
||||||
bearerToken:
|
bearerToken,
|
||||||
'ss_eyJhbGciOiJIUzI1NiIsImtpZCI6IkEyZExNNUlheFE4L29acW4iLCJ0eXAiOiJKV1QifQ.eyJpc3MiOiJodHRwczovL2t5aGpvaG1xbXFrdmZxc2t4dnNkLnN1cGFiYXNlLmNvL2F1dGgvdjEiLCJzdWIiOiJmZjRjOWRjOS1kNzQ1LTQ2MmItYTFiNS1lZmIxMDgwMjU0ZTkiLCJhdWQiOiJhdXRoZW50aWNhdGVkIiwiZXhwIjoxNzI1NDc5ODU4LCJpYXQiOjE3MjU0NzYyNTgsImVtYWlsIjoiZXZhbkBydW5sb29wLmFpIiwicGhvbmUiOiIiLCJhcHBfbWV0YWRhdGEiOnsicHJvdmlkZXIiOiJlbWFpbCIsInByb3ZpZGVycyI6WyJlbWFpbCIsImdpdGh1YiJdfSwidXNlcl9tZXRhZGF0YSI6eyJhdmF0YXJfdXJsIjoiaHR0cHM6Ly9hdmF0YXJzLmdpdGh1YnVzZXJjb250ZW50LmNvbS91LzE1NTQ3NTU1Nz92PTQiLCJlbWFpbCI6ImV2YW5AcnVubG9vcC5haSIsImVtYWlsX3ZlcmlmaWVkIjp0cnVlLCJpc3MiOiJodHRwczovL2FwaS5naXRodWIuY29tIiwicGhvbmVfdmVyaWZpZWQiOmZhbHNlLCJwcmVmZXJyZWRfdXNlcm5hbWUiOiJldmFuLXJ1bmxvb3BhaSIsInByb3ZpZGVyX2lkIjoiMTU1NDc1NTU3Iiwic3ViIjoiMTU1NDc1NTU3IiwidXNlcl9uYW1lIjoiZXZhbi1ydW5sb29wYWkifSwicm9sZSI6ImF1dGhlbnRpY2F0ZWQiLCJhYWwiOiJhYWwxIiwiYW1yIjpbeyJtZXRob2QiOiJvYXV0aCIsInRpbWVzdGFtcCI6MTcyNDQ1MjY4MX1dLCJzZXNzaW9uX2lkIjoiNzc4ODUzMjQtNmYwYy00ZGRhLWFjMDMtZWJiMDAxZWFkYTc4IiwiaXNfYW5vbnltb3VzIjpmYWxzZX0.ghcTeoYcppsNj6xIg1AhG-lL5RyNWKMSWdH--iZZ2co',
|
|
||||||
});
|
});
|
||||||
const targetDevbox = 'dbx_2xb2Swl1rMFqrvGVX0Q4N';
|
|
||||||
|
|
||||||
const sshKeyCreateResp = await runloop.devboxes.createSSHKey(targetDevbox);
|
const sshKeyCreateResp = await runloop.devboxes.createSSHKey(targetDevbox);
|
||||||
const hostname = sshKeyCreateResp.url;
|
const hostname = sshKeyCreateResp.url;
|
||||||
console.log("EVAN SSH KEY RESP", sshKeyCreateResp)
|
console.log('EVAN SSH KEY RESP', sshKeyCreateResp);
|
||||||
|
|
||||||
// SS KEY
|
// SS KEY
|
||||||
// Environment
|
// Environment
|
||||||
|
@ -87,8 +83,8 @@ async function establishConnection() {
|
||||||
// });
|
// });
|
||||||
// });
|
// });
|
||||||
})
|
})
|
||||||
.on('error', (err) => {
|
.on('error', (error) => {
|
||||||
console.error('SSH Connection error:', err);
|
console.error('SSH Connection error:', error);
|
||||||
})
|
})
|
||||||
.connect({
|
.connect({
|
||||||
sock: tlsSocket, // Pass the TLS socket as the connection
|
sock: tlsSocket, // Pass the TLS socket as the connection
|
||||||
|
@ -96,100 +92,29 @@ async function establishConnection() {
|
||||||
privateKey: convertPKCS8toPKCS1(sshKeyCreateResp.ssh_private_key), // Replace with the path to your private key
|
privateKey: convertPKCS8toPKCS1(sshKeyCreateResp.ssh_private_key), // Replace with the path to your private key
|
||||||
hostHash: 'md5', // Optional: Match host keys by hash
|
hostHash: 'md5', // Optional: Match host keys by hash
|
||||||
strictHostKeyChecking: false, // Disable strict host key checking
|
strictHostKeyChecking: false, // Disable strict host key checking
|
||||||
|
|
||||||
|
// algorithms: socket.request.session.ssh.algorithms,
|
||||||
|
readyTimeout: 10000,
|
||||||
|
keepaliveInterval: 120000,
|
||||||
|
keepaliveCountMax: 10,
|
||||||
|
debug: debug('ssh2'),
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
// var fs = require('fs')
|
|
||||||
// var hostkeys = JSON.parse(fs.readFileSync('./hostkeyhashes.json', 'utf8'))
|
|
||||||
let termCols;
|
let termCols;
|
||||||
let termRows;
|
let termRows;
|
||||||
|
|
||||||
// public
|
// public
|
||||||
module.exports = function appSocket(socket) {
|
module.exports = function appSocket(socket) {
|
||||||
async function setupConnection() {
|
async function setupConnection() {
|
||||||
|
// TODO AUTH?
|
||||||
// if websocket connection arrives without an express session, kill it
|
// if websocket connection arrives without an express session, kill it
|
||||||
if (!socket.request.session) {
|
// if (!socket.request.session) {
|
||||||
socket.emit('401 UNAUTHORIZED');
|
// socket.emit('401 UNAUTHORIZED');
|
||||||
debugWebSSH2('SOCKET: No Express Session / REJECTED');
|
// debugWebSSH2('SOCKET: No Express Session / REJECTED');
|
||||||
socket.disconnect(true);
|
// socket.disconnect(true);
|
||||||
return;
|
// return;
|
||||||
}
|
// }
|
||||||
|
|
||||||
/**
|
|
||||||
* Error handling for various events. Outputs error to client, logs to
|
|
||||||
* server, destroys session and disconnects socket.
|
|
||||||
* @param {string} myFunc Function calling this function
|
|
||||||
* @param {object} err error object or error message
|
|
||||||
*/
|
|
||||||
// eslint-disable-next-line complexity
|
|
||||||
function SSHerror(myFunc, err) {
|
|
||||||
let theError;
|
|
||||||
if (socket.request.session) {
|
|
||||||
// we just want the first error of the session to pass to the client
|
|
||||||
const firstError = socket.request.session.error || (err ? err.message : undefined);
|
|
||||||
theError = firstError ? `: ${firstError}` : '';
|
|
||||||
// log unsuccessful login attempt
|
|
||||||
if (err && err.level === 'client-authentication') {
|
|
||||||
console.error(
|
|
||||||
`WebSSH2 ${'error: Authentication failure'.red.bold} user=${socket.request.session.username.yellow.bold.underline} from=${socket.handshake.address.yellow.bold.underline}`,
|
|
||||||
);
|
|
||||||
socket.emit('allowreauth', socket.request.session.ssh.allowreauth);
|
|
||||||
socket.emit('reauth');
|
|
||||||
} else {
|
|
||||||
// eslint-disable-next-line no-console
|
|
||||||
console.log(
|
|
||||||
`WebSSH2 Logout: user=${socket.request.session.username} from=${socket.handshake.address} host=${socket.request.session.ssh.host} port=${socket.request.session.ssh.port} sessionID=${socket.request.sessionID}/${socket.id} allowreplay=${socket.request.session.ssh.allowreplay} term=${socket.request.session.ssh.term}`,
|
|
||||||
);
|
|
||||||
if (err) {
|
|
||||||
theError = err ? `: ${err.message}` : '';
|
|
||||||
console.error(`WebSSH2 error${theError}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
socket.emit('ssherror', `SSH ${myFunc}${theError}`);
|
|
||||||
socket.request.session.destroy();
|
|
||||||
socket.disconnect(true);
|
|
||||||
} else {
|
|
||||||
theError = err ? `: ${err.message}` : '';
|
|
||||||
socket.disconnect(true);
|
|
||||||
}
|
|
||||||
debugWebSSH2(`SSHerror ${myFunc}${theError}`);
|
|
||||||
}
|
|
||||||
// If configured, check that requsted host is in a permitted subnet
|
|
||||||
if (
|
|
||||||
(((socket.request.session || {}).ssh || {}).allowedSubnets || {}).length &&
|
|
||||||
socket.request.session.ssh.allowedSubnets.length > 0
|
|
||||||
) {
|
|
||||||
let ipaddress = socket.request.session.ssh.host;
|
|
||||||
if (!validator.isIP(`${ipaddress}`)) {
|
|
||||||
try {
|
|
||||||
const result = await dnsPromises.lookup(socket.request.session.ssh.host);
|
|
||||||
ipaddress = result.address;
|
|
||||||
} catch (err) {
|
|
||||||
console.error(
|
|
||||||
`WebSSH2 ${`error: ${err.code} ${err.hostname}`.red.bold} user=${
|
|
||||||
socket.request.session.username.yellow.bold.underline
|
|
||||||
} from=${socket.handshake.address.yellow.bold.underline}`,
|
|
||||||
);
|
|
||||||
socket.emit('ssherror', '404 HOST IP NOT FOUND');
|
|
||||||
socket.disconnect(true);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const matcher = new CIDRMatcher(socket.request.session.ssh.allowedSubnets);
|
|
||||||
if (!matcher.contains(ipaddress)) {
|
|
||||||
console.error(
|
|
||||||
`WebSSH2 ${
|
|
||||||
`error: Requested host ${ipaddress} outside configured subnets / REJECTED`.red.bold
|
|
||||||
} user=${socket.request.session.username.yellow.bold.underline} from=${
|
|
||||||
socket.handshake.address.yellow.bold.underline
|
|
||||||
}`,
|
|
||||||
);
|
|
||||||
socket.emit('ssherror', '401 UNAUTHORIZED');
|
|
||||||
socket.disconnect(true);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// const conn = new SSH();
|
// const conn = new SSH();
|
||||||
socket.on('geometry', (cols, rows) => {
|
socket.on('geometry', (cols, rows) => {
|
||||||
|
@ -202,33 +127,24 @@ module.exports = function appSocket(socket) {
|
||||||
});
|
});
|
||||||
|
|
||||||
conn.on('ready', () => {
|
conn.on('ready', () => {
|
||||||
debugWebSSH2(
|
// debugWebSSH2(
|
||||||
`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}`,
|
// `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');
|
socket.emit('setTerminalOpts', {
|
||||||
socket.emit('allowreauth', socket.request.session.ssh.allowreauth);
|
cursorBlink: true,
|
||||||
socket.emit('setTerminalOpts', socket.request.session.ssh.terminal);
|
scrollback: 10000,
|
||||||
socket.emit('title', `ssh://${socket.request.session.ssh.host}`);
|
tabStopWidth: 8,
|
||||||
if (socket.request.session.ssh.header.background)
|
bellStyle: 'sound',
|
||||||
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(
|
conn.shell(
|
||||||
{
|
{
|
||||||
term: socket.request.session.ssh.term,
|
term: 'xterm-color',
|
||||||
cols: termCols,
|
cols: termCols,
|
||||||
rows: termRows,
|
rows: termRows,
|
||||||
},
|
},
|
||||||
(err, stream) => {
|
(err, stream) => {
|
||||||
if (err) {
|
if (err) {
|
||||||
SSHerror(`EXEC ERROR${err}`);
|
// SSHerror(`EXEC ERROR${err}`);
|
||||||
conn.end();
|
conn.end();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -236,11 +152,12 @@ module.exports = function appSocket(socket) {
|
||||||
stream.write(data);
|
stream.write(data);
|
||||||
});
|
});
|
||||||
socket.on('control', (controlData) => {
|
socket.on('control', (controlData) => {
|
||||||
|
// Todo probably remove
|
||||||
switch (controlData) {
|
switch (controlData) {
|
||||||
case 'replayCredentials':
|
// case 'replayCredentials':
|
||||||
if (socket.request.session.ssh.allowreplay) {
|
// if (socket.request.session.ssh.allowreplay) {
|
||||||
stream.write(`${socket.request.session.userpassword}\n`);
|
// stream.write(`${socket.request.session.userpassword}\n`);
|
||||||
}
|
// }
|
||||||
/* falls through */
|
/* falls through */
|
||||||
default:
|
default:
|
||||||
debugWebSSH2(`controlData: ${controlData}`);
|
debugWebSSH2(`controlData: ${controlData}`);
|
||||||
|
@ -254,13 +171,13 @@ module.exports = function appSocket(socket) {
|
||||||
});
|
});
|
||||||
socket.on('disconnect', (reason) => {
|
socket.on('disconnect', (reason) => {
|
||||||
debugWebSSH2(`SOCKET DISCONNECT: ${reason}`);
|
debugWebSSH2(`SOCKET DISCONNECT: ${reason}`);
|
||||||
const errMsg = { message: reason };
|
//const errMsg = { message: reason };
|
||||||
SSHerror('CLIENT SOCKET DISCONNECT', errMsg);
|
// SSHerror('CLIENT SOCKET DISCONNECT', errMsg);
|
||||||
conn.end();
|
conn.end();
|
||||||
// socket.request.session.destroy()
|
// socket.request.session.destroy()
|
||||||
});
|
});
|
||||||
socket.on('error', (errMsg) => {
|
socket.on('error', (errMsg) => {
|
||||||
SSHerror('SOCKET ERROR', errMsg);
|
// SSHerror('SOCKET ERROR', errMsg);
|
||||||
conn.end();
|
conn.end();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -276,7 +193,7 @@ module.exports = function appSocket(socket) {
|
||||||
(signal ? `SIGNAL: ${signal}` : '')
|
(signal ? `SIGNAL: ${signal}` : '')
|
||||||
: undefined,
|
: undefined,
|
||||||
};
|
};
|
||||||
SSHerror('STREAM CLOSE', errMsg);
|
//SSHerror('STREAM CLOSE', errMsg);
|
||||||
conn.end();
|
conn.end();
|
||||||
});
|
});
|
||||||
stream.stderr.on('data', (data) => {
|
stream.stderr.on('data', (data) => {
|
||||||
|
@ -287,23 +204,18 @@ module.exports = function appSocket(socket) {
|
||||||
});
|
});
|
||||||
|
|
||||||
conn.on('end', (err) => {
|
conn.on('end', (err) => {
|
||||||
SSHerror('CONN END BY HOST', err);
|
//SSHerror('CONN END BY HOST', err);
|
||||||
});
|
});
|
||||||
conn.on('close', (err) => {
|
conn.on('close', (err) => {
|
||||||
SSHerror('CONN CLOSE', err);
|
//SSHerror('CONN CLOSE', err);
|
||||||
});
|
});
|
||||||
conn.on('error', (err) => {
|
conn.on('error', (err) => {
|
||||||
SSHerror('CONN ERROR', err);
|
//SSHerror('CONN ERROR', err);
|
||||||
});
|
});
|
||||||
conn.on('keyboard-interactive', (name, instructions, instructionsLang, prompts, finish) => {
|
// conn.on('keyboard-interactive', (name, instructions, instructionsLang, prompts, finish) => {
|
||||||
debugWebSSH2("conn.on('keyboard-interactive')");
|
// debugWebSSH2("conn.on('keyboard-interactive')");
|
||||||
finish([socket.request.session.userpassword]);
|
// finish([socket.request.session.userpassword]);
|
||||||
});
|
// });
|
||||||
if (
|
|
||||||
socket.request.session.username &&
|
|
||||||
(socket.request.session.userpassword || socket.request.session.privatekey) &&
|
|
||||||
socket.request.session.ssh
|
|
||||||
) {
|
|
||||||
// console.log('hostkeys: ' + hostkeys[0].[0])
|
// console.log('hostkeys: ' + hostkeys[0].[0])
|
||||||
// conn.connect({
|
// conn.connect({
|
||||||
// host: socket.request.session.ssh.host,
|
// host: socket.request.session.ssh.host,
|
||||||
|
@ -320,19 +232,10 @@ module.exports = function appSocket(socket) {
|
||||||
// keepaliveCountMax: socket.request.session.ssh.keepaliveCountMax,
|
// keepaliveCountMax: socket.request.session.ssh.keepaliveCountMax,
|
||||||
// debug: debug('ssh2'),
|
// debug: debug('ssh2'),
|
||||||
// });
|
// });
|
||||||
console.log('EVAN HERE');
|
await establishConnection(
|
||||||
await establishConnection();
|
'dbx_2xb6oS1G1e6TAihVMtjn6',
|
||||||
} else {
|
'ss_eyJhbGciOiJIUzI1NiIsImtpZCI6IkEyZExNNUlheFE4L29acW4iLCJ0eXAiOiJKV1QifQ.eyJpc3MiOiJodHRwczovL2t5aGpvaG1xbXFrdmZxc2t4dnNkLnN1cGFiYXNlLmNvL2F1dGgvdjEiLCJzdWIiOiI5NmRlMTVjZC1lZWJmLTRjNzctODQwNy1jZTkwNzNlZTZkMjIiLCJhdWQiOiJhdXRoZW50aWNhdGVkIiwiZXhwIjoxNzI1NDg1NTQwLCJpYXQiOjE3MjU0ODE5NDAsImVtYWlsIjoiYWxleEBydW5sb29wLmFpIiwicGhvbmUiOiIiLCJhcHBfbWV0YWRhdGEiOnsicHJvdmlkZXIiOiJlbWFpbCIsInByb3ZpZGVycyI6WyJlbWFpbCIsImdpdGh1YiJdfSwidXNlcl9tZXRhZGF0YSI6eyJhdmF0YXJfdXJsIjoiaHR0cHM6Ly9hdmF0YXJzLmdpdGh1YnVzZXJjb250ZW50LmNvbS91LzE2MDA3NzkyND92PTQiLCJlbWFpbCI6ImFsZXhAcnVubG9vcC5haSIsImVtYWlsX3ZlcmlmaWVkIjp0cnVlLCJmdWxsX25hbWUiOiJBbGV4YW5kZXIgRGluZXMiLCJpc3MiOiJodHRwczovL2FwaS5naXRodWIuY29tIiwibmFtZSI6IkFsZXhhbmRlciBEaW5lcyIsInBob25lX3ZlcmlmaWVkIjpmYWxzZSwicHJlZmVycmVkX3VzZXJuYW1lIjoiZGluZXMtcmwiLCJwcm92aWRlcl9pZCI6IjE2MDA3NzkyNCIsInN1YiI6IjE2MDA3NzkyNCIsInVzZXJfbmFtZSI6ImRpbmVzLXJsIn0sInJvbGUiOiJhdXRoZW50aWNhdGVkIiwiYWFsIjoiYWFsMSIsImFtciI6W3sibWV0aG9kIjoib2F1dGgiLCJ0aW1lc3RhbXAiOjE3MjM1OTAxODB9XSwic2Vzc2lvbl9pZCI6IjU5MjhkZTIxLTI3M2ItNDkzNC1iMGFmLThlYWE3MDUxOGE3MiIsImlzX2Fub255bW91cyI6ZmFsc2V9.Qby46q2eDZUWjpPPSvmyzQ5bGKGEkpg2r9zBAUTpc3Q',
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
setupConnection();
|
setupConnection();
|
||||||
// establishConnection();
|
|
||||||
};
|
};
|
||||||
|
|
|
@ -2,44 +2,43 @@
|
||||||
// util.js
|
// util.js
|
||||||
|
|
||||||
// private
|
// private
|
||||||
const debug = require('debug')('WebSSH2');
|
// const debug = require('debug')('WebSSH2');
|
||||||
const Auth = require('basic-auth');
|
// const Auth = require('basic-auth');
|
||||||
|
|
||||||
let defaultCredentials = { username: null, password: null, privatekey: null };
|
|
||||||
|
|
||||||
exports.setDefaultCredentials = function setDefaultCredentials({
|
// exports.setDefaultCredentials = function setDefaultCredentials({
|
||||||
name: username,
|
// name: username,
|
||||||
password,
|
// password,
|
||||||
privatekey,
|
// privatekey,
|
||||||
overridebasic,
|
// overridebasic,
|
||||||
}) {
|
// }) {
|
||||||
defaultCredentials = { username, password, privatekey, overridebasic };
|
// defaultCredentials = { username, password, privatekey, overridebasic };
|
||||||
};
|
// };
|
||||||
|
|
||||||
exports.basicAuth = function basicAuth(req, res, next) {
|
// exports.basicAuth = function basicAuth(req, res, next) {
|
||||||
const myAuth = Auth(req);
|
// const myAuth = Auth(req);
|
||||||
// If Authorize: Basic header exists and the password isn't blank
|
// // If Authorize: Basic header exists and the password isn't blank
|
||||||
// AND config.user.overridebasic is false, extract basic credentials
|
// // AND config.user.overridebasic is false, extract basic credentials
|
||||||
// from client]
|
// // from client]
|
||||||
const { username, password, privatekey, overridebasic } = defaultCredentials;
|
// const { username, password, privatekey, overridebasic } = defaultCredentials;
|
||||||
if (myAuth && myAuth.pass !== '' && !overridebasic) {
|
// if (myAuth && myAuth.pass !== '' && !overridebasic) {
|
||||||
req.session.username = myAuth.name;
|
// req.session.username = myAuth.name;
|
||||||
req.session.userpassword = myAuth.pass;
|
// req.session.userpassword = myAuth.pass;
|
||||||
debug(`myAuth.name: ${myAuth.name} and password ${myAuth.pass ? 'exists' : 'is blank'}`);
|
// debug(`myAuth.name: ${myAuth.name} and password ${myAuth.pass ? 'exists' : 'is blank'}`);
|
||||||
} else {
|
// } else {
|
||||||
req.session.username = username;
|
// req.session.username = username;
|
||||||
req.session.userpassword = password;
|
// req.session.userpassword = password;
|
||||||
req.session.privatekey = privatekey;
|
// req.session.privatekey = privatekey;
|
||||||
}
|
// }
|
||||||
if (!req.session.userpassword && !req.session.privatekey) {
|
// if (!req.session.userpassword && !req.session.privatekey) {
|
||||||
res.statusCode = 401;
|
// res.statusCode = 401;
|
||||||
debug('basicAuth credential request (401)');
|
// debug('basicAuth credential request (401)');
|
||||||
res.setHeader('WWW-Authenticate', 'Basic realm="WebSSH"');
|
// res.setHeader('WWW-Authenticate', 'Basic realm="WebSSH"');
|
||||||
res.end('Username and password required for web SSH service.');
|
// res.end('Username and password required for web SSH service.');
|
||||||
return;
|
// return;
|
||||||
}
|
// }
|
||||||
next();
|
// next();
|
||||||
};
|
// };
|
||||||
|
|
||||||
// takes a string, makes it boolean (true if the string is true, false otherwise)
|
// takes a string, makes it boolean (true if the string is true, false otherwise)
|
||||||
exports.parseBool = function parseBool(str) {
|
exports.parseBool = function parseBool(str) {
|
||||||
|
|
Loading…
Reference in a new issue