223 lines
7.7 KiB
JavaScript
223 lines
7.7 KiB
JavaScript
const debugWebSSH2 = require('debug')('WebSSH2');
|
|
const debug = require('debug');
|
|
const { Client } = require('ssh2');
|
|
const tls = require('tls');
|
|
const { Runloop } = require('@runloop/api-client');
|
|
const { convertPKCS8toPKCS1 } = require('./util');
|
|
|
|
// Function to create a TLS connection (simulating ProxyCommand with openssl s_client)
|
|
function tlsProxyConnect(hostname, callback) {
|
|
const tlsSocket = tls.connect(
|
|
{
|
|
host: 'ssh.runloop.pro', // Proxy server address
|
|
port: 443, // Proxy port (HTTPS over TLS)
|
|
servername: hostname, // Target hostname, acts like -servername in openssl
|
|
checkServerIdentity: () => undefined, // Disable hostname validation
|
|
},
|
|
() => {
|
|
console.log('TLS connection established');
|
|
callback(null, tlsSocket); // Return the established socket
|
|
},
|
|
);
|
|
|
|
tlsSocket.on('error', (err) => {
|
|
console.error('TLS connection error:', err);
|
|
callback(err);
|
|
});
|
|
}
|
|
|
|
// Main function to establish the SSH connection over the TLS proxy
|
|
async function establishConnection(conn, socket, 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,
|
|
});
|
|
try {
|
|
console.log(`Creating SSH key for devbox ${targetDevbox}`);
|
|
const sshKeyCreateResp = await runloop.devboxes.createSSHKey(targetDevbox);
|
|
|
|
const hostname = sshKeyCreateResp.url;
|
|
|
|
// SS KEY
|
|
// Environment
|
|
// Get ssh config information
|
|
tlsProxyConnect(hostname, (err, tlsSocket) => {
|
|
if (err) {
|
|
console.error('Error during proxy connection:', err);
|
|
return;
|
|
}
|
|
|
|
// Now use ssh2 to connect over the TLS socket
|
|
conn
|
|
.on('ready', () => {
|
|
console.log('SSH Client ready');
|
|
})
|
|
.on('error', (error) => {
|
|
console.error('SSH Connection error:', error);
|
|
})
|
|
.connect({
|
|
sock: tlsSocket, // Pass the TLS socket as the connection
|
|
username: 'user', // Replace with the correct SSH username
|
|
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'),
|
|
});
|
|
});
|
|
} catch (e) {
|
|
console.error(e);
|
|
socket.disconnect(true);
|
|
}
|
|
}
|
|
|
|
//TODO deal with
|
|
let termCols;
|
|
let termRows;
|
|
|
|
// public
|
|
module.exports = function appSocket(socket) {
|
|
const connection = new Client();
|
|
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;
|
|
// }
|
|
|
|
socket.on('geometry', (cols, rows) => {
|
|
termCols = cols;
|
|
termRows = rows;
|
|
});
|
|
connection.on('banner', (data) => {
|
|
// need to convert to cr/lf for proper formatting
|
|
socket.emit('data', data.replace(/\r?\n/g, '\r\n').toString('utf-8'));
|
|
});
|
|
|
|
connection.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}`,
|
|
// );
|
|
connection.shell(
|
|
{
|
|
term: 'xterm-color',
|
|
cols: termCols,
|
|
rows: termRows,
|
|
},
|
|
(err, stream) => {
|
|
if (err) {
|
|
// SSHerror(`EXEC ERROR${err}`);
|
|
socket.disconnect(true);
|
|
connection.end();
|
|
return;
|
|
}
|
|
socket.on('data', (data) => {
|
|
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`);
|
|
// }
|
|
/* falls through */
|
|
default:
|
|
debugWebSSH2(`controlData: ${controlData}`);
|
|
}
|
|
});
|
|
socket.on('resize', (data) => {
|
|
stream.setWindow(data.rows, data.cols);
|
|
});
|
|
socket.on('disconnecting', (reason) => {
|
|
debugWebSSH2(`SOCKET DISCONNECTING: ${reason}`);
|
|
});
|
|
socket.on('disconnect', (reason) => {
|
|
debugWebSSH2(`SOCKET DISCONNECT: ${reason}`);
|
|
// const errMsg = { message: reason };
|
|
// SSHerror('CLIENT SOCKET DISCONNECT', errMsg);
|
|
socket.disconnect(true);
|
|
connection.end();
|
|
// socket.request.session.destroy()
|
|
});
|
|
socket.on('error', (errMsg) => {
|
|
// SSHerror('SOCKET ERROR', errMsg);
|
|
socket.disconnect(true);
|
|
connection.end();
|
|
});
|
|
|
|
stream.on('data', (data) => {
|
|
socket.emit('data', data.toString('utf-8'));
|
|
});
|
|
stream.on('close', (code, signal) => {
|
|
const errMsg = {
|
|
message:
|
|
code || signal
|
|
? (code ? `CODE: ${code}` : '') +
|
|
(code && signal ? ' ' : '') +
|
|
(signal ? `SIGNAL: ${signal}` : '')
|
|
: undefined,
|
|
};
|
|
// SSHerror('STREAM CLOSE', errMsg);
|
|
socket.disconnect(true);
|
|
connection.end();
|
|
});
|
|
stream.stderr.on('data', (data) => {
|
|
console.error(`STDERR: ${data}`);
|
|
});
|
|
},
|
|
);
|
|
});
|
|
|
|
connection.on('end', (err) => {
|
|
//SSHerror('CONN END BY HOST', err);
|
|
});
|
|
connection.on('close', (err) => {
|
|
//SSHerror('CONN CLOSE', err);
|
|
});
|
|
connection.on('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]);
|
|
// });
|
|
// console.log('hostkeys: ' + hostkeys[0].[0])
|
|
// conn.connect({
|
|
// host: socket.request.session.ssh.host,
|
|
// port: socket.request.session.ssh.port,
|
|
// localAddress: socket.request.session.ssh.localAddress,
|
|
// localPort: socket.request.session.ssh.localPort,
|
|
// username: socket.request.session.username,
|
|
// password: socket.request.session.userpassword,
|
|
// privateKey: socket.request.session.privatekey,
|
|
// tryKeyboard: true,
|
|
// algorithms: socket.request.session.ssh.algorithms,
|
|
// readyTimeout: socket.request.session.ssh.readyTimeout,
|
|
// keepaliveInterval: socket.request.session.ssh.keepaliveInterval,
|
|
// keepaliveCountMax: socket.request.session.ssh.keepaliveCountMax,
|
|
// debug: debug('ssh2'),
|
|
// });
|
|
const devboxId = socket.request._query.devboxId;
|
|
if (!devboxId) {
|
|
console.error('No devboxId');
|
|
throw new Error('No devboxId');
|
|
}
|
|
const sessionId = socket.request._query.sessionId;
|
|
if (!sessionId) {
|
|
console.error('No sessionId');
|
|
throw new Error('No sessionId');
|
|
}
|
|
console.log(sessionId);
|
|
await establishConnection(connection, socket, devboxId, sessionId);
|
|
}
|
|
setupConnection();
|
|
};
|