This commit is contained in:
Alexander Dines 2024-09-04 14:57:10 -07:00
parent b332d35bb1
commit 5d62a0bc1d
11 changed files with 319 additions and 597 deletions

View file

@ -10,21 +10,9 @@
</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">
<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="countdown"></div>
</div>
</div>
<script src="/ssh/webssh2.bundle.js" defer></script>

File diff suppressed because one or more lines are too long

View file

@ -10,21 +10,9 @@
</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">
<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="countdown"></div>
</div>
</div>
<script src="/ssh/webssh2.bundle.js" defer></script>

View file

@ -15,8 +15,6 @@ require('../css/style.css');
/* global Blob, logBtn, credentialsBtn, reauthBtn, downloadLogBtn */ // eslint-disable-line
let sessionLogEnable = false;
let loggedData = false;
let allowreplay = false;
let allowreauth = false;
let sessionLog: string;
let sessionFooter: any;
let logDate: {
@ -33,13 +31,8 @@ let errorExists: boolean;
const term = new Terminal();
// DOM properties
const logBtn = document.getElementById('logBtn');
const credentialsBtn = document.getElementById('credentialsBtn');
const reauthBtn = document.getElementById('reauthBtn');
const downloadLogBtn = document.getElementById('downloadLogBtn');
const status = document.getElementById('status');
const header = document.getElementById('header');
const footer = document.getElementById('footer');
const countdown = document.getElementById('countdown');
const fitAddon = new FitAddon();
const terminalContainer = document.getElementById('terminal-container');
term.loadAddon(fitAddon);
@ -49,10 +42,12 @@ fitAddon.fit();
const socket = io({
path: '/ssh/socket.io',
transports: ['websocket'],
});
// reauthenticate
function reauthSession () { // eslint-disable-line
function reauthSession() {
// eslint-disable-line
debug('re-authenticating');
socket.emit('control', '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
// used for our client-side logging feature
function downloadLog () { // eslint-disable-line
function downloadLog() {
// eslint-disable-line
if (loggedData === true) {
myFile = `WebSSH2-${logDate.getFullYear()}${
logDate.getMonth() + 1
@ -72,13 +68,13 @@ function downloadLog () { // eslint-disable-line
sessionLog.replace(
// eslint-disable-next-line no-control-regex
/[\u001b\u009b][[\]()#;?]*(?:\d{1,4}(?:;\d{0,4})*)?[0-9A-ORZcf-nqry=><;]/g,
''
'',
),
],
{
// eslint-disable-line no-control-regex
type: 'text/plain',
}
},
);
const elem = window.document.createElement('a');
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
// for later download
function toggleLog () { // eslint-disable-line
function toggleLog() {
// eslint-disable-line
if (sessionLogEnable === true) {
sessionLogEnable = false;
loggedData = true;
@ -119,31 +116,14 @@ function toggleLog () { // eslint-disable-line
}
// replay password to server, requires
function replayCredentials () { // eslint-disable-line
function replayCredentials() {
// eslint-disable-line
socket.emit('control', 'replayCredentials');
debug(`control: replayCredentials`);
term.focus();
return false;
}
// draw/re-draw menu and reattach listeners
// when dom is changed, listeners are abandonded
function drawMenu() {
logBtn.addEventListener('click', toggleLog);
if (allowreauth) {
reauthBtn.addEventListener('click', reauthSession);
reauthBtn.style.display = 'block';
}
if (allowreplay) {
credentialsBtn.addEventListener('click', replayCredentials);
credentialsBtn.style.display = 'block';
}
if (loggedData) {
downloadLogBtn.addEventListener('click', downloadLog);
downloadLogBtn.style.display = 'block';
}
}
function resizeScreen() {
fitAddon.fit();
socket.emit('resize', { cols: term.cols, rows: term.rows });
@ -181,71 +161,59 @@ socket.on(
lineHeight: number;
}) => {
term.options = data;
}
},
);
socket.on('title', (data: string) => {
document.title = data;
});
// socket.on('ssherror', (data: string) => {
// status.innerHTML = data;
// status.style.backgroundColor = 'red';
// errorExists = true;
// });
socket.on('menu', () => {
drawMenu();
});
// socket.on('headerBackground', (data: string) => {
// header.style.backgroundColor = data;
// });
socket.on('status', (data: string) => {
status.innerHTML = data;
});
// socket.on('header', (data: string) => {
// if (data) {
// header.innerHTML = data;
// header.style.display = 'block';
// // header is 19px and footer is 19px, recaculate new terminal-container and resize
// terminalContainer.style.height = 'calc(100% - 38px)';
// resizeScreen();
// }
// });
socket.on('ssherror', (data: string) => {
status.innerHTML = data;
status.style.backgroundColor = 'red';
errorExists = true;
});
// socket.on('footer', (data: string) => {
// sessionFooter = data;
// footer.innerHTML = data;
// });
socket.on('headerBackground', (data: string) => {
header.style.backgroundColor = data;
});
// socket.on('statusBackground', (data: string) => {
// status.style.backgroundColor = data;
// });
socket.on('header', (data: string) => {
if (data) {
header.innerHTML = data;
header.style.display = 'block';
// header is 19px and footer is 19px, recaculate new terminal-container and resize
terminalContainer.style.height = 'calc(100% - 38px)';
resizeScreen();
}
});
// socket.on('allowreplay', (data: boolean) => {
// if (data === true) {
// debug(`allowreplay: ${data}`);
// allowreplay = true;
// drawMenu();
// } else {
// allowreplay = false;
// debug(`allowreplay: ${data}`);
// }
// });
socket.on('footer', (data: string) => {
sessionFooter = data;
footer.innerHTML = data;
});
socket.on('statusBackground', (data: string) => {
status.style.backgroundColor = data;
});
socket.on('allowreplay', (data: boolean) => {
if (data === true) {
debug(`allowreplay: ${data}`);
allowreplay = true;
drawMenu();
} else {
allowreplay = false;
debug(`allowreplay: ${data}`);
}
});
socket.on('allowreauth', (data: boolean) => {
if (data === true) {
debug(`allowreauth: ${data}`);
allowreauth = true;
drawMenu();
} else {
allowreauth = false;
debug(`allowreauth: ${data}`);
}
});
// socket.on('allowreauth', (data: boolean) => {
// if (data === true) {
// debug(`allowreauth: ${data}`);
// allowreauth = true;
// drawMenu();
// } else {
// allowreauth = false;
// debug(`allowreauth: ${data}`);
// }
// });
socket.on('disconnect', (err: any) => {
if (!errorExists) {
@ -253,33 +221,31 @@ socket.on('disconnect', (err: any) => {
status.innerHTML = `WEBSOCKET SERVER DISCONNECTED: ${err}`;
}
socket.io.reconnection(false);
countdown.classList.remove('active');
});
socket.on('error', (err: any) => {
if (!errorExists) {
status.style.backgroundColor = 'red';
status.innerHTML = `ERROR: ${err}`;
}
});
// socket.on('error', (err: any) => {
// if (!errorExists) {
// status.style.backgroundColor = 'red';
// status.innerHTML = `ERROR: ${err}`;
// }
// });
socket.on('reauth', () => {
if (allowreauth) {
reauthSession();
}
});
// socket.on('reauth', () => {
// if (allowreauth) {
// reauthSession();
// }
// });
// safe shutdown
let hasCountdownStarted = false;
socket.on('shutdownCountdownUpdate', (remainingSeconds: any) => {
if (!hasCountdownStarted) {
countdown.classList.add('active');
hasCountdownStarted = true;
}
countdown.innerText = `Shutting down in ${remainingSeconds}s`;
});
// socket.on('shutdownCountdownUpdate', (remainingSeconds: any) => {
// if (!hasCountdownStarted) {
// countdown.classList.add('active');
// hasCountdownStarted = true;
// }
// countdown.innerText = `Shutting down in ${remainingSeconds}s`;
// });
term.onTitleChange((title) => {
document.title = title;
});
// term.onTitleChange((title) => {
// document.title = title;
// });

View file

@ -6,7 +6,7 @@
"socketio": {
"serveClient": false,
"path": "/ssh/socket.io",
"origins": ["localhost:2222"]
"origins": ["localhost:2224"]
},
"user": {
"name": null,
@ -62,16 +62,8 @@
"aes256-gcm@openssh.com",
"aes256-cbc"
],
"hmac": [
"hmac-sha2-256",
"hmac-sha2-512",
"hmac-sha1"
],
"compress": [
"none",
"zlib@openssh.com",
"zlib"
]
"hmac": ["hmac-sha2-256", "hmac-sha2-512", "hmac-sha1"],
"compress": ["none", "zlib@openssh.com", "zlib"]
},
"serverlog": {
"client": false,

View file

@ -1,36 +1,15 @@
{
"name": "webssh2",
"name": "myssh",
"version": "0.6.0-pre-1",
"ignore": [
".gitignore"
],
"bin": "./index.js",
"description": "A Websocket to SSH2 gateway using term.js, socket.io, ssh2, and express",
"homepage": "https://github.com/billchurch/WebSSH2",
"keywords": [
"ssh",
"webssh",
"terminal",
"webterminal"
],
"license": "SEE LICENSE IN FILE - LICENSE",
"private": false,
"repository": {
"type": "git",
"url": "git+https://github.com/billchurch/WebSSH2.git"
},
"contributors": [
{
"name": "Bill Church",
"email": "wmchurch@gmail.com"
}
],
"engines": {
"node": ">= 14"
},
"bugs": {
"url": "https://github.com/billchurch/WebSSH2/issues"
},
"dependencies": {
"@runloop/api-client": "^0.1.0-alpha.23",
"basic-auth": "~2.0.1",
@ -42,7 +21,7 @@
"node-forge": "^1.3.1",
"read-config-ng": "^3.0.7",
"serve-favicon": "^2.5.0",
"socket.io": "^4.7.5",
"socket.io": "4.7.5",
"ssh2": "^1.15.0",
"validator": "^13.11.0",
"winston": "^3.13.0"

View file

@ -11,22 +11,41 @@ const nodeRoot = path.dirname(require.main.filename);
const publicPath = path.join(nodeRoot, 'client', 'public');
const express = require('express');
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 server = require('http').createServer(app);
const favicon = require('serve-favicon');
const io = require('socket.io')(server, config.socketio);
const session = require('express-session')(config.express);
const io = require('socket.io')(server, { transports: ['websocket'], ...config.socketio });
const session = require('express-session')(expressConfig);
const appSocket = require('./socket');
const { setDefaultCredentials, basicAuth } = require('./util');
// const { setDefaultCredentials } = require('./util');
const { webssh2debug } = require('./logging');
const { reauth, connect, notfound, handleErrors } = require('./routes');
const { connect } = require('./routes');
setDefaultCredentials(config.user);
// setDefaultCredentials(config.user);
// safe shutdown
let remainingSeconds = config.safeShutdownDuration;
let shutdownMode = false;
let shutdownInterval;
let connectionCount = 0;
@ -37,19 +56,18 @@ function safeShutdownGuard(req, res, next) {
}
// express
app.use(safeShutdownGuard);
app.use(session);
// app.use(session);
if (config.accesslog) app.use(logger('common'));
app.disable('x-powered-by');
app.use(favicon(path.join(publicPath, 'favicon.ico')));
app.use(express.urlencoded({ extended: true }));
app.post('/ssh/host/:host?', connect);
app.post('/ssh', express.static(publicPath, config.express.ssh));
app.use('/ssh', express.static(publicPath, config.express.ssh));
app.use(basicAuth);
app.get('/ssh/reauth', reauth);
app.post('/ssh', express.static(publicPath, expressConfig.ssh));
app.use('/ssh', express.static(publicPath, expressConfig.ssh));
//app.use(basicAuth);
//app.get('/ssh/reauth', reauth);
app.get('/ssh/host/:host?', connect);
app.use(notfound);
app.use(handleErrors);
// app.use(notfound);
// app.use(handleErrors);
// clean stop
function stopApp(reason) {
@ -63,36 +81,10 @@ function stopApp(reason) {
// bring up socket
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 };
const onConnection = (socket) => {
console.log('connected');
connectionCount += 1;
socket.on('disconnect', () => {
connectionCount -= 1;
@ -102,9 +94,9 @@ const onConnection = (socket) => {
});
socket.on('geometry', (cols, rows) => {
// 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.rows = rows;
webssh2debug(socket, `SOCKET GEOMETRY: termCols = ${cols}, termRows = ${rows}`);
//socket.request.session.ssh.cols = cols; //TODO make this part of the terminal config on connect
//socket.request.session.ssh.rows = rows;
//webssh2debug(socket, `SOCKET GEOMETRY: termCols = ${cols}, termRows = ${rows}`);
});
};

View file

@ -20,7 +20,7 @@ const configDefault = {
socketio: {
serveClient: false,
path: '/ssh/socket.io',
origins: ['localhost:2222'],
origins: ['localhost:*'],
},
express: {
secret: crypto.randomBytes(20).toString('hex'),
@ -103,7 +103,7 @@ const configDefault = {
try {
if (!fs.existsSync(configPath)) {
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');
}
@ -115,7 +115,7 @@ try {
} catch (err) {
myConfig = configDefault;
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(`ERROR:\n\n ${err}`);

View file

@ -9,195 +9,110 @@ const publicPath = path.join(nodeRoot, 'client', 'public');
const { parseBool } = require('./util');
const config = require('./config');
exports.reauth = function reauth(req, res) {
let { referer } = req.headers;
if (!validator.isURL(referer, { host_whitelist: ['localhost'] })) {
console.error(
`WebSSH2 (${req.sessionID}) ERROR: Referrer '${referer}' for '/reauth' invalid. Setting to '/' which will probably fail.`
);
referer = '/';
}
res
.status(401)
.send(
`<!DOCTYPE html><html><head><meta http-equiv="refresh" content="0; url=${referer}"></head><body bgcolor="#000"></body></html>`
);
};
// exports.reauth = function reauth(req, res) {
// let { referer } = req.headers;
// if (!validator.isURL(referer, { host_whitelist: ['localhost'] })) {
// console.error(
// `WebSSH2 (${req.sessionID}) ERROR: Referrer '${referer}' for '/reauth' invalid. Setting to '/' which will probably fail.`,
// );
// referer = '/';
// }
// res
// .status(401)
// .send(
// `<!DOCTYPE html><html><head><meta http-equiv="refresh" content="0; url=${referer}"></head><body bgcolor="#000"></body></html>`,
// );
// };
exports.connect = function connect(req, res) {
res.sendFile(path.join(path.join(publicPath, 'client.htm')));
let { host, port } = config.ssh;
let { text: header, background: headerBackground } = config.header;
let { term: sshterm, readyTimeout } = config.ssh;
let {
cursorBlink,
scrollback,
tabStopWidth,
bellStyle,
fontSize,
fontFamily,
letterSpacing,
lineHeight,
} = config.terminal;
// let { host, port } = config.ssh;
// let { text: header, background: headerBackground } = config.header;
// let { term: sshterm, readyTimeout } = config.ssh;
// let {
// cursorBlink,
// scrollback,
// tabStopWidth,
// bellStyle,
// fontSize,
// fontFamily,
// letterSpacing,
// lineHeight,
// } = config.terminal;
// capture, assign, and validate variables
if (req.params?.host) {
if (
validator.isIP(`${req.params.host}`) ||
validator.isFQDN(req.params.host) ||
/^(([a-z]|[A-Z]|\d|[!^(){}\-_~])+)?\w$/.test(req.params.host)
) {
host = req.params.host;
}
}
// if (req.params?.host) {
// if (
// validator.isIP(`${req.params.host}`) ||
// validator.isFQDN(req.params.host) ||
// /^(([a-z]|[A-Z]|\d|[!^(){}\-_~])+)?\w$/.test(req.params.host)
// ) {
// host = req.params.host;
// }
// }
if (req.method === 'POST' && req.body.username && req.body.userpassword) {
req.session.username = req.body.username;
req.session.userpassword = req.body.userpassword;
// //// ADding exta
// if (req.params?.devboxID) {
// 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 }))
port = req.body.port;
// 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);
// };
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) {
headerBackground = req.body.headerBackground;
console.log(`background: ${req.body.headerBackground}`);
}
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!');
// exports.handleErrors = function handleErrors(err, _req, res) {
// console.error(err.stack);
// res.status(500).send('Something broke!');
// };
};

View file

@ -4,6 +4,7 @@ const SSH = require('ssh2').Client;
const CIDRMatcher = require('cidr-matcher');
const validator = require('validator');
const dnsPromises = require('dns').promises;
const debug = require('debug');
const { Client } = require('ssh2');
const tls = require('tls');
const forge = require('node-forge');
@ -14,7 +15,6 @@ function convertPKCS8toPKCS1(pkcs8Key) {
// Convert the private key to PKCS#1 format
const pkcs1Pem = forge.pki.privateKeyToPem(privateKeyInfo);
return pkcs1Pem;
}
@ -27,9 +27,7 @@ const proxyConnect = (hostname, callback) => {
host: 'ssh.runloop.pro', // Proxy server address
port: 443, // Proxy port (HTTPS over TLS)
servername: hostname, // Target hostname, acts like -servername in openssl
checkServerIdentity: () => {
return undefined;
}, // Disable hostname validation
checkServerIdentity: () => undefined, // Disable hostname validation
},
() => {
console.log('TLS connection established');
@ -44,18 +42,16 @@ const proxyConnect = (hostname, callback) => {
};
// Main function to establish the SSH connection over the TLS proxy
async function establishConnection() {
async function establishConnection(targetDevbox, bearerToken) {
const runloop = new Runloop({
baseURL: 'https://api.runloop.pro',
// This is gotten by just inspecting the browser cookies on platform.runloop.pro
bearerToken:
'ss_eyJhbGciOiJIUzI1NiIsImtpZCI6IkEyZExNNUlheFE4L29acW4iLCJ0eXAiOiJKV1QifQ.eyJpc3MiOiJodHRwczovL2t5aGpvaG1xbXFrdmZxc2t4dnNkLnN1cGFiYXNlLmNvL2F1dGgvdjEiLCJzdWIiOiJmZjRjOWRjOS1kNzQ1LTQ2MmItYTFiNS1lZmIxMDgwMjU0ZTkiLCJhdWQiOiJhdXRoZW50aWNhdGVkIiwiZXhwIjoxNzI1NDc5ODU4LCJpYXQiOjE3MjU0NzYyNTgsImVtYWlsIjoiZXZhbkBydW5sb29wLmFpIiwicGhvbmUiOiIiLCJhcHBfbWV0YWRhdGEiOnsicHJvdmlkZXIiOiJlbWFpbCIsInByb3ZpZGVycyI6WyJlbWFpbCIsImdpdGh1YiJdfSwidXNlcl9tZXRhZGF0YSI6eyJhdmF0YXJfdXJsIjoiaHR0cHM6Ly9hdmF0YXJzLmdpdGh1YnVzZXJjb250ZW50LmNvbS91LzE1NTQ3NTU1Nz92PTQiLCJlbWFpbCI6ImV2YW5AcnVubG9vcC5haSIsImVtYWlsX3ZlcmlmaWVkIjp0cnVlLCJpc3MiOiJodHRwczovL2FwaS5naXRodWIuY29tIiwicGhvbmVfdmVyaWZpZWQiOmZhbHNlLCJwcmVmZXJyZWRfdXNlcm5hbWUiOiJldmFuLXJ1bmxvb3BhaSIsInByb3ZpZGVyX2lkIjoiMTU1NDc1NTU3Iiwic3ViIjoiMTU1NDc1NTU3IiwidXNlcl9uYW1lIjoiZXZhbi1ydW5sb29wYWkifSwicm9sZSI6ImF1dGhlbnRpY2F0ZWQiLCJhYWwiOiJhYWwxIiwiYW1yIjpbeyJtZXRob2QiOiJvYXV0aCIsInRpbWVzdGFtcCI6MTcyNDQ1MjY4MX1dLCJzZXNzaW9uX2lkIjoiNzc4ODUzMjQtNmYwYy00ZGRhLWFjMDMtZWJiMDAxZWFkYTc4IiwiaXNfYW5vbnltb3VzIjpmYWxzZX0.ghcTeoYcppsNj6xIg1AhG-lL5RyNWKMSWdH--iZZ2co',
bearerToken,
});
const targetDevbox = 'dbx_2xb2Swl1rMFqrvGVX0Q4N';
const sshKeyCreateResp = await runloop.devboxes.createSSHKey(targetDevbox);
const hostname = sshKeyCreateResp.url;
console.log("EVAN SSH KEY RESP", sshKeyCreateResp)
console.log('EVAN SSH KEY RESP', sshKeyCreateResp);
// SS KEY
// Environment
@ -87,8 +83,8 @@ async function establishConnection() {
// });
// });
})
.on('error', (err) => {
console.error('SSH Connection error:', err);
.on('error', (error) => {
console.error('SSH Connection error:', error);
})
.connect({
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
hostHash: 'md5', // Optional: Match host keys by hash
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 termRows;
// public
module.exports = function appSocket(socket) {
async function setupConnection() {
// TODO AUTH?
// 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;
}
/**
* 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;
}
}
// if (!socket.request.session) {
// socket.emit('401 UNAUTHORIZED');
// debugWebSSH2('SOCKET: No Express Session / REJECTED');
// socket.disconnect(true);
// return;
// }
// const conn = new SSH();
socket.on('geometry', (cols, rows) => {
@ -202,33 +127,24 @@ module.exports = function appSocket(socket) {
});
conn.on('ready', () => {
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}`,
);
socket.emit('menu');
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);
// 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}`,
// );
socket.emit('setTerminalOpts', {
cursorBlink: true,
scrollback: 10000,
tabStopWidth: 8,
bellStyle: 'sound',
});
conn.shell(
{
term: socket.request.session.ssh.term,
term: 'xterm-color',
cols: termCols,
rows: termRows,
},
(err, stream) => {
if (err) {
SSHerror(`EXEC ERROR${err}`);
// SSHerror(`EXEC ERROR${err}`);
conn.end();
return;
}
@ -236,11 +152,12 @@ module.exports = function appSocket(socket) {
stream.write(data);
});
socket.on('control', (controlData) => {
// Todo probably remove
switch (controlData) {
case 'replayCredentials':
if (socket.request.session.ssh.allowreplay) {
stream.write(`${socket.request.session.userpassword}\n`);
}
// case 'replayCredentials':
// if (socket.request.session.ssh.allowreplay) {
// stream.write(`${socket.request.session.userpassword}\n`);
// }
/* falls through */
default:
debugWebSSH2(`controlData: ${controlData}`);
@ -254,13 +171,13 @@ module.exports = function appSocket(socket) {
});
socket.on('disconnect', (reason) => {
debugWebSSH2(`SOCKET DISCONNECT: ${reason}`);
const errMsg = { message: reason };
SSHerror('CLIENT SOCKET DISCONNECT', errMsg);
//const errMsg = { message: reason };
// SSHerror('CLIENT SOCKET DISCONNECT', errMsg);
conn.end();
// socket.request.session.destroy()
});
socket.on('error', (errMsg) => {
SSHerror('SOCKET ERROR', errMsg);
// SSHerror('SOCKET ERROR', errMsg);
conn.end();
});
@ -276,7 +193,7 @@ module.exports = function appSocket(socket) {
(signal ? `SIGNAL: ${signal}` : '')
: undefined,
};
SSHerror('STREAM CLOSE', errMsg);
//SSHerror('STREAM CLOSE', errMsg);
conn.end();
});
stream.stderr.on('data', (data) => {
@ -287,23 +204,18 @@ module.exports = function appSocket(socket) {
});
conn.on('end', (err) => {
SSHerror('CONN END BY HOST', err);
//SSHerror('CONN END BY HOST', err);
});
conn.on('close', (err) => {
SSHerror('CONN CLOSE', err);
//SSHerror('CONN CLOSE', err);
});
conn.on('error', (err) => {
SSHerror('CONN ERROR', err);
//SSHerror('CONN ERROR', err);
});
conn.on('keyboard-interactive', (name, instructions, instructionsLang, prompts, finish) => {
debugWebSSH2("conn.on('keyboard-interactive')");
finish([socket.request.session.userpassword]);
});
if (
socket.request.session.username &&
(socket.request.session.userpassword || socket.request.session.privatekey) &&
socket.request.session.ssh
) {
// conn.on('keyboard-interactive', (name, instructions, instructionsLang, prompts, finish) => {
// debugWebSSH2("conn.on('keyboard-interactive')");
// finish([socket.request.session.userpassword]);
// });
// console.log('hostkeys: ' + hostkeys[0].[0])
// conn.connect({
// host: socket.request.session.ssh.host,
@ -320,19 +232,10 @@ module.exports = function appSocket(socket) {
// keepaliveCountMax: socket.request.session.ssh.keepaliveCountMax,
// debug: debug('ssh2'),
// });
console.log('EVAN HERE');
await establishConnection();
} 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,
)}`,
await establishConnection(
'dbx_2xb6oS1G1e6TAihVMtjn6',
'ss_eyJhbGciOiJIUzI1NiIsImtpZCI6IkEyZExNNUlheFE4L29acW4iLCJ0eXAiOiJKV1QifQ.eyJpc3MiOiJodHRwczovL2t5aGpvaG1xbXFrdmZxc2t4dnNkLnN1cGFiYXNlLmNvL2F1dGgvdjEiLCJzdWIiOiI5NmRlMTVjZC1lZWJmLTRjNzctODQwNy1jZTkwNzNlZTZkMjIiLCJhdWQiOiJhdXRoZW50aWNhdGVkIiwiZXhwIjoxNzI1NDg1NTQwLCJpYXQiOjE3MjU0ODE5NDAsImVtYWlsIjoiYWxleEBydW5sb29wLmFpIiwicGhvbmUiOiIiLCJhcHBfbWV0YWRhdGEiOnsicHJvdmlkZXIiOiJlbWFpbCIsInByb3ZpZGVycyI6WyJlbWFpbCIsImdpdGh1YiJdfSwidXNlcl9tZXRhZGF0YSI6eyJhdmF0YXJfdXJsIjoiaHR0cHM6Ly9hdmF0YXJzLmdpdGh1YnVzZXJjb250ZW50LmNvbS91LzE2MDA3NzkyND92PTQiLCJlbWFpbCI6ImFsZXhAcnVubG9vcC5haSIsImVtYWlsX3ZlcmlmaWVkIjp0cnVlLCJmdWxsX25hbWUiOiJBbGV4YW5kZXIgRGluZXMiLCJpc3MiOiJodHRwczovL2FwaS5naXRodWIuY29tIiwibmFtZSI6IkFsZXhhbmRlciBEaW5lcyIsInBob25lX3ZlcmlmaWVkIjpmYWxzZSwicHJlZmVycmVkX3VzZXJuYW1lIjoiZGluZXMtcmwiLCJwcm92aWRlcl9pZCI6IjE2MDA3NzkyNCIsInN1YiI6IjE2MDA3NzkyNCIsInVzZXJfbmFtZSI6ImRpbmVzLXJsIn0sInJvbGUiOiJhdXRoZW50aWNhdGVkIiwiYWFsIjoiYWFsMSIsImFtciI6W3sibWV0aG9kIjoib2F1dGgiLCJ0aW1lc3RhbXAiOjE3MjM1OTAxODB9XSwic2Vzc2lvbl9pZCI6IjU5MjhkZTIxLTI3M2ItNDkzNC1iMGFmLThlYWE3MDUxOGE3MiIsImlzX2Fub255bW91cyI6ZmFsc2V9.Qby46q2eDZUWjpPPSvmyzQ5bGKGEkpg2r9zBAUTpc3Q',
);
socket.emit('ssherror', 'WEBSOCKET ERROR - Refresh the browser and try again');
socket.request.session.destroy();
socket.disconnect(true);
}
}
setupConnection();
// establishConnection();
};

View file

@ -2,44 +2,43 @@
// util.js
// private
const debug = require('debug')('WebSSH2');
const Auth = require('basic-auth');
// const debug = require('debug')('WebSSH2');
// const Auth = require('basic-auth');
let defaultCredentials = { username: null, password: null, privatekey: null };
exports.setDefaultCredentials = function setDefaultCredentials({
name: username,
password,
privatekey,
overridebasic,
}) {
defaultCredentials = { username, password, privatekey, overridebasic };
};
// exports.setDefaultCredentials = function setDefaultCredentials({
// name: username,
// password,
// privatekey,
// overridebasic,
// }) {
// defaultCredentials = { username, password, privatekey, overridebasic };
// };
exports.basicAuth = function basicAuth(req, res, next) {
const myAuth = Auth(req);
// If Authorize: Basic header exists and the password isn't blank
// AND config.user.overridebasic is false, extract basic credentials
// from client]
const { username, password, privatekey, overridebasic } = defaultCredentials;
if (myAuth && myAuth.pass !== '' && !overridebasic) {
req.session.username = myAuth.name;
req.session.userpassword = myAuth.pass;
debug(`myAuth.name: ${myAuth.name} and password ${myAuth.pass ? 'exists' : 'is blank'}`);
} else {
req.session.username = username;
req.session.userpassword = password;
req.session.privatekey = privatekey;
}
if (!req.session.userpassword && !req.session.privatekey) {
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.');
return;
}
next();
};
// exports.basicAuth = function basicAuth(req, res, next) {
// const myAuth = Auth(req);
// // If Authorize: Basic header exists and the password isn't blank
// // AND config.user.overridebasic is false, extract basic credentials
// // from client]
// const { username, password, privatekey, overridebasic } = defaultCredentials;
// if (myAuth && myAuth.pass !== '' && !overridebasic) {
// req.session.username = myAuth.name;
// req.session.userpassword = myAuth.pass;
// debug(`myAuth.name: ${myAuth.name} and password ${myAuth.pass ? 'exists' : 'is blank'}`);
// } else {
// req.session.username = username;
// req.session.userpassword = password;
// req.session.privatekey = privatekey;
// }
// if (!req.session.userpassword && !req.session.privatekey) {
// 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.');
// return;
// }
// next();
// };
// takes a string, makes it boolean (true if the string is true, false otherwise)
exports.parseBool = function parseBool(str) {