This commit is contained in:
Alexander Dines 2024-09-04 15:12:07 -07:00
parent 4c60362e85
commit 576af8aad0
6 changed files with 34 additions and 204 deletions

File diff suppressed because one or more lines are too long

View file

@ -35,6 +35,11 @@ const downloadLogBtn = document.getElementById('downloadLogBtn');
const status = document.getElementById('status'); const status = document.getElementById('status');
const fitAddon = new FitAddon(); const fitAddon = new FitAddon();
const terminalContainer = document.getElementById('terminal-container'); const terminalContainer = document.getElementById('terminal-container');
term.options = {
cursorBlink: true,
scrollback: 10000,
tabStopWidth: 8,
};
term.loadAddon(fitAddon); term.loadAddon(fitAddon);
term.open(terminalContainer); term.open(terminalContainer);
term.focus(); term.focus();
@ -45,15 +50,6 @@ const socket = io({
transports: ['websocket'], transports: ['websocket'],
}); });
// reauthenticate
function reauthSession() {
// eslint-disable-line
debug('re-authenticating');
socket.emit('control', 'reauth');
window.location.href = '/ssh/reauth';
return false;
}
// 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() { function downloadLog() {
@ -115,15 +111,6 @@ function toggleLog() {
return false; return false;
} }
// replay password to server, requires
function replayCredentials() {
// eslint-disable-line
socket.emit('control', 'replayCredentials');
debug(`control: replayCredentials`);
term.focus();
return false;
}
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 });
@ -148,22 +135,6 @@ socket.on('connect', () => {
debug(`geometry: ${term.cols}, ${term.rows}`); debug(`geometry: ${term.cols}, ${term.rows}`);
}); });
socket.on(
'setTerminalOpts',
(data: {
cursorBlink: boolean;
scrollback: number;
tabStopWidth: number;
bellStyle: 'none' | 'sound';
fontSize: number;
fontFamily: string;
letterSpacing: number;
lineHeight: number;
}) => {
term.options = data;
},
);
// socket.on('ssherror', (data: string) => { // socket.on('ssherror', (data: string) => {
// status.innerHTML = data; // status.innerHTML = data;
// status.style.backgroundColor = 'red'; // status.style.backgroundColor = 'red';
@ -174,47 +145,6 @@ socket.on(
// header.style.backgroundColor = data; // header.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('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('disconnect', (err: any) => { socket.on('disconnect', (err: any) => {
if (!errorExists) { if (!errorExists) {
status.style.backgroundColor = 'red'; status.style.backgroundColor = 'red';

View file

@ -39,37 +39,26 @@ const io = require('socket.io')(server, { transports: ['websocket'], ...config.s
const appSocket = require('./socket'); const appSocket = require('./socket');
const { connect } = require('./routes'); const { connect } = require('./routes');
// eslint-disable-next-line consistent-return
function safeShutdownGuard(req, res, next) {
if (!shutdownMode) return next();
res.status(503).end('Service unavailable: Server shutting down');
}
// express
app.use(safeShutdownGuard);
app.disable('x-powered-by'); app.disable('x-powered-by');
app.use(express.urlencoded({ extended: true })); app.use(express.urlencoded({ extended: true }));
app.post('/ssh/host/:host?', connect); app.post('/ssh/host/:host?', connect);
// To remove
// Static files..
app.post('/ssh', express.static(publicPath, expressConfig.ssh)); app.post('/ssh', express.static(publicPath, expressConfig.ssh));
app.use('/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.get('/ssh/host/:host?', connect);
// app.use(notfound);
// app.use(handleErrors);
// clean stop
function stopApp(reason) {
shutdownMode = false;
if (reason) console.info(`Stopping: ${reason}`);
clearInterval(shutdownInterval);
io.close();
server.close();
}
// bring up socket
io.on('connection', appSocket); io.on('connection', appSocket);
// // clean stop
// function stopApp(reason) {
// shutdownMode = false;
// if (reason) console.info(`Stopping: ${reason}`);
// clearInterval(shutdownInterval);
// io.close();
// server.close();
// }
module.exports = { server, config }; module.exports = { server, config };
// const onConnection = (socket) => { // const onConnection = (socket) => {

View file

@ -1,28 +1,10 @@
/* eslint-disable no-console */ /* eslint-disable no-console */
// ssh.js // ssh.js
const validator = require('validator');
const path = require('path'); const path = require('path');
const nodeRoot = path.dirname(require.main.filename); const nodeRoot = path.dirname(require.main.filename);
const publicPath = path.join(nodeRoot, 'client', 'public'); 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.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')));

View file

@ -1,27 +1,13 @@
// private
const debugWebSSH2 = require('debug')('WebSSH2'); const debugWebSSH2 = require('debug')('WebSSH2');
const SSH = require('ssh2').Client;
const CIDRMatcher = require('cidr-matcher');
const validator = require('validator');
const dnsPromises = require('dns').promises;
const debug = require('debug'); 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');
const { Runloop } = require('@runloop/api-client'); const { Runloop } = require('@runloop/api-client');
function convertPKCS8toPKCS1(pkcs8Key) {
const privateKeyInfo = forge.pki.privateKeyFromPem(pkcs8Key);
// Convert the private key to PKCS#1 format
const pkcs1Pem = forge.pki.privateKeyToPem(privateKeyInfo);
return pkcs1Pem;
}
const conn = new Client();
// Function to create a TLS connection (simulating ProxyCommand with openssl s_client) // Function to create a TLS connection (simulating ProxyCommand with openssl s_client)
const proxyConnect = (hostname, callback) => { function tlsProxyConnect(hostname, callback) {
const tlsSocket = tls.connect( const tlsSocket = tls.connect(
{ {
host: 'ssh.runloop.pro', // Proxy server address host: 'ssh.runloop.pro', // Proxy server address
@ -39,10 +25,10 @@ const proxyConnect = (hostname, callback) => {
console.error('TLS connection error:', err); console.error('TLS connection error:', err);
callback(err); callback(err);
}); });
}; }
// 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(targetDevbox, bearerToken) { async function establishConnection(conn, 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
@ -56,7 +42,7 @@ async function establishConnection(targetDevbox, bearerToken) {
// SS KEY // SS KEY
// Environment // Environment
// Get ssh config information // Get ssh config information
proxyConnect(hostname, (err, tlsSocket) => { tlsProxyConnect(hostname, (err, tlsSocket) => {
if (err) { if (err) {
console.error('Error during proxy connection:', err); console.error('Error during proxy connection:', err);
return; return;
@ -66,22 +52,6 @@ async function establishConnection(targetDevbox, bearerToken) {
conn conn
.on('ready', () => { .on('ready', () => {
console.log('SSH Client ready'); console.log('SSH Client ready');
// conn.exec("uptime", (err, stream) => {
// if (err) throw err;
// stream
// .on("close", (code, signal) => {
// console.log(
// "Stream :: close :: code: " + code + ", signal: " + signal
// );
// conn.end();
// })
// .on("data", (data) => {
// console.log("STDOUT: " + data);
// })
// .stderr.on("data", (data) => {
// console.log("STDERR: " + data);
// });
// });
}) })
.on('error', (error) => { .on('error', (error) => {
console.error('SSH Connection error:', error); console.error('SSH Connection error:', error);
@ -101,11 +71,14 @@ async function establishConnection(targetDevbox, bearerToken) {
}); });
}); });
} }
//TODO deal with
let termCols; let termCols;
let termRows; let termRows;
// public // public
module.exports = function appSocket(socket) { module.exports = function appSocket(socket) {
const conn = new Client();
async function setupConnection() { async function setupConnection() {
// TODO AUTH? // TODO AUTH?
// if websocket connection arrives without an express session, kill it // if websocket connection arrives without an express session, kill it
@ -116,7 +89,6 @@ module.exports = function appSocket(socket) {
// return; // return;
// } // }
// const conn = new SSH();
socket.on('geometry', (cols, rows) => { socket.on('geometry', (cols, rows) => {
termCols = cols; termCols = cols;
termRows = rows; termRows = rows;
@ -130,12 +102,6 @@ module.exports = function appSocket(socket) {
// 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('setTerminalOpts', {
cursorBlink: true,
scrollback: 10000,
tabStopWidth: 8,
bellStyle: 'sound',
});
conn.shell( conn.shell(
{ {
term: 'xterm-color', term: 'xterm-color',
@ -233,9 +199,18 @@ module.exports = function appSocket(socket) {
// debug: debug('ssh2'), // debug: debug('ssh2'),
// }); // });
await establishConnection( await establishConnection(
conn,
'dbx_2xb6oS1G1e6TAihVMtjn6', 'dbx_2xb6oS1G1e6TAihVMtjn6',
'ss_eyJhbGciOiJIUzI1NiIsImtpZCI6IkEyZExNNUlheFE4L29acW4iLCJ0eXAiOiJKV1QifQ.eyJpc3MiOiJodHRwczovL2t5aGpvaG1xbXFrdmZxc2t4dnNkLnN1cGFiYXNlLmNvL2F1dGgvdjEiLCJzdWIiOiI5NmRlMTVjZC1lZWJmLTRjNzctODQwNy1jZTkwNzNlZTZkMjIiLCJhdWQiOiJhdXRoZW50aWNhdGVkIiwiZXhwIjoxNzI1NDg1NTQwLCJpYXQiOjE3MjU0ODE5NDAsImVtYWlsIjoiYWxleEBydW5sb29wLmFpIiwicGhvbmUiOiIiLCJhcHBfbWV0YWRhdGEiOnsicHJvdmlkZXIiOiJlbWFpbCIsInByb3ZpZGVycyI6WyJlbWFpbCIsImdpdGh1YiJdfSwidXNlcl9tZXRhZGF0YSI6eyJhdmF0YXJfdXJsIjoiaHR0cHM6Ly9hdmF0YXJzLmdpdGh1YnVzZXJjb250ZW50LmNvbS91LzE2MDA3NzkyND92PTQiLCJlbWFpbCI6ImFsZXhAcnVubG9vcC5haSIsImVtYWlsX3ZlcmlmaWVkIjp0cnVlLCJmdWxsX25hbWUiOiJBbGV4YW5kZXIgRGluZXMiLCJpc3MiOiJodHRwczovL2FwaS5naXRodWIuY29tIiwibmFtZSI6IkFsZXhhbmRlciBEaW5lcyIsInBob25lX3ZlcmlmaWVkIjpmYWxzZSwicHJlZmVycmVkX3VzZXJuYW1lIjoiZGluZXMtcmwiLCJwcm92aWRlcl9pZCI6IjE2MDA3NzkyNCIsInN1YiI6IjE2MDA3NzkyNCIsInVzZXJfbmFtZSI6ImRpbmVzLXJsIn0sInJvbGUiOiJhdXRoZW50aWNhdGVkIiwiYWFsIjoiYWFsMSIsImFtciI6W3sibWV0aG9kIjoib2F1dGgiLCJ0aW1lc3RhbXAiOjE3MjM1OTAxODB9XSwic2Vzc2lvbl9pZCI6IjU5MjhkZTIxLTI3M2ItNDkzNC1iMGFmLThlYWE3MDUxOGE3MiIsImlzX2Fub255bW91cyI6ZmFsc2V9.Qby46q2eDZUWjpPPSvmyzQ5bGKGEkpg2r9zBAUTpc3Q', 'ss_eyJhbGciOiJIUzI1NiIsImtpZCI6IkEyZExNNUlheFE4L29acW4iLCJ0eXAiOiJKV1QifQ.eyJpc3MiOiJodHRwczovL2t5aGpvaG1xbXFrdmZxc2t4dnNkLnN1cGFiYXNlLmNvL2F1dGgvdjEiLCJzdWIiOiI5NmRlMTVjZC1lZWJmLTRjNzctODQwNy1jZTkwNzNlZTZkMjIiLCJhdWQiOiJhdXRoZW50aWNhdGVkIiwiZXhwIjoxNzI1NDg1NTQwLCJpYXQiOjE3MjU0ODE5NDAsImVtYWlsIjoiYWxleEBydW5sb29wLmFpIiwicGhvbmUiOiIiLCJhcHBfbWV0YWRhdGEiOnsicHJvdmlkZXIiOiJlbWFpbCIsInByb3ZpZGVycyI6WyJlbWFpbCIsImdpdGh1YiJdfSwidXNlcl9tZXRhZGF0YSI6eyJhdmF0YXJfdXJsIjoiaHR0cHM6Ly9hdmF0YXJzLmdpdGh1YnVzZXJjb250ZW50LmNvbS91LzE2MDA3NzkyND92PTQiLCJlbWFpbCI6ImFsZXhAcnVubG9vcC5haSIsImVtYWlsX3ZlcmlmaWVkIjp0cnVlLCJmdWxsX25hbWUiOiJBbGV4YW5kZXIgRGluZXMiLCJpc3MiOiJodHRwczovL2FwaS5naXRodWIuY29tIiwibmFtZSI6IkFsZXhhbmRlciBEaW5lcyIsInBob25lX3ZlcmlmaWVkIjpmYWxzZSwicHJlZmVycmVkX3VzZXJuYW1lIjoiZGluZXMtcmwiLCJwcm92aWRlcl9pZCI6IjE2MDA3NzkyNCIsInN1YiI6IjE2MDA3NzkyNCIsInVzZXJfbmFtZSI6ImRpbmVzLXJsIn0sInJvbGUiOiJhdXRoZW50aWNhdGVkIiwiYWFsIjoiYWFsMSIsImFtciI6W3sibWV0aG9kIjoib2F1dGgiLCJ0aW1lc3RhbXAiOjE3MjM1OTAxODB9XSwic2Vzc2lvbl9pZCI6IjU5MjhkZTIxLTI3M2ItNDkzNC1iMGFmLThlYWE3MDUxOGE3MiIsImlzX2Fub255bW91cyI6ZmFsc2V9.Qby46q2eDZUWjpPPSvmyzQ5bGKGEkpg2r9zBAUTpc3Q',
); );
} }
setupConnection(); setupConnection();
}; };
function convertPKCS8toPKCS1(pkcs8Key) {
const privateKeyInfo = forge.pki.privateKeyFromPem(pkcs8Key);
// Convert the private key to PKCS#1 format
const pkcs1Pem = forge.pki.privateKeyToPem(privateKeyInfo);
return pkcs1Pem;
}

View file

@ -1,46 +0,0 @@
/* jshint esversion: 6, asi: true, node: true */
// util.js
// private
// const debug = require('debug')('WebSSH2');
// const Auth = require('basic-auth');
// 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();
// };
// takes a string, makes it boolean (true if the string is true, false otherwise)
exports.parseBool = function parseBool(str) {
return str.toLowerCase() === 'true';
};