353 lines
14 KiB
JavaScript
353 lines
14 KiB
JavaScript
// private
|
|
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 { Client } = require('ssh2');
|
|
const tls = require('tls');
|
|
const forge = require('node-forge');
|
|
|
|
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 sshKey = `-----BEGIN RSA PRIVATE KEY-----
|
|
MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCKspJS9rQGducF
|
|
WmE9Jh1ABKevG4k5qvdBC7SpGwnUacXFMQGpItHa6Soqw7ozKl0V6Mm1hsTWzciP
|
|
AeSc3zCcG9cuZu5YsX22b/iHk+xYPXTj4wEkkYpneaCk254c5tYTL+kEG24jEf/2
|
|
/Dld2oa6eQIHPZNDwC8ikndLKmvlv31xwuDsvjfljIqD49v0VwD19YDrM1f02SqK
|
|
/qzXkaaLnFdkk27aZNz7erLu2fUBGj1Qnul/ZljAoXI31QEs9wjQyn6ZfW8U96Md
|
|
3slCStiBXWCAW/IebnsE4z3k05tjNG8ha5tU9iG0BoAbGvpX2tceH9hs6PxMobh1
|
|
poebP2N9AgMBAAECggEAA8GRRTjpfYapJqkgxVtWYx5H0kNbJITU8bMuSdQcdd8F
|
|
cZvW+9cgZXQiEGAWQf4i4OMvL0DT7NihgVSfoICfhuLEIZK9CrQyf4cpwmDcVN7D
|
|
3yC4g7PYp95l3iu/wkWdb43rxWY0GzQIwWNpmaPqOSY/dVBoFWd2Kf1bHgiAkoti
|
|
VnUHefKfKR4oAJw5K1ZRx9wzThL1Sg1vcD86L1jWmG0ifXx6+q5bcPQcuYSJ91Rq
|
|
WprhGrvm6JBt9dCM/Qtaz5Kw7tW3bslIEgIuzMa84QqAKhB9BfCuO0x9ebn1KyRN
|
|
Gj5jCZC9IrgZyeliNlpYpeRb4Umbv6waS0yJ8auUawKBgQC52MXWRmP647zoz7Ed
|
|
TobnxvmhRlvPFuRl3dPvAviAD7ZRIkzerQaHKmm57NoSWutQ4bSQ2WTk+dcjSwPX
|
|
+m/9vCqiLa/3fOyZ6DyZeWBnq02p4gYuMANWtsISVH0gKTp4+PhpFf1JriUvx9fo
|
|
9yLbYfUkd3vnyUD1gqm+L7ntlwKBgQC/DY17fvwz9ST6zhrI52tOdDJ+Btad5nte
|
|
VjDn0Tq0yRryuJqWWzAc8RRieczNlc4jBlUmJgRK3Zp3LjpQwNFY4kLBAxBIhgfJ
|
|
iMZYnD6OxgtO+TUhdN4r7cUhVvbj4aiTWQ9d0CH4lDW3Z/vFnka1JM+mI4MYak1F
|
|
I5Jsc7ECCwKBgGqTfIiv30AOf9QG3uwOj2C1g4xP+/BbkWk1eAc17eoKmKQYhnqg
|
|
QQEcensL79bc2tuMQ+9ZK/n/qLdtmmuuC7E3yj8s8h98PXbZbn8Y0wdAfo4wtxif
|
|
ohqFPfAjEYpy+jxLkrE40gMB4gNvmEraBtxGZb2e46h9ikoAv3T4i6hLAoGAMiok
|
|
1CBrqFjd9NzZO5dIHbl06JJzF9LE4ehPvw65E28anFDMhl47K95BM/o3RGPpVFj9
|
|
Up740Y+OV2zT8xAt5+DBFlzvkZtfwBMhwXKFGof1wC6/PKGrFG3CLRbgjMVbthTU
|
|
bBWSVerUj+vFuAXvGvEndMAuU+LVlynX8JIQEDECgYEAnnbkbcE3yEffRBYJNMtU
|
|
Q57iTjpNThulk8xpo0dJpM3qEgNWJUGJHo7WjTr9ZQdMMAzYbH6UbaKhzzpEt6oL
|
|
bw2e5t5vittkqw30WRqX7oY0bP+0jPxcJ2UsiyrtEVeKFfpumPha2I3SD6nFuBWW
|
|
9ELc23WVPO3G0w6LGfBEfUQ=
|
|
-----END RSA PRIVATE KEY-----`;
|
|
|
|
const conn = new Client();
|
|
|
|
// Function to create a TLS connection (simulating ProxyCommand with openssl s_client)
|
|
const proxyConnect = (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: () => {
|
|
return 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);
|
|
});
|
|
};
|
|
|
|
const hostname =
|
|
'devbox-0191be26-2418-758d-8100-7d9fff932b8d.38408049-afa6-4fe0-a4a1-4d120d39c1cd.ssh.runloop.pro';
|
|
// Main function to establish the SSH connection over the TLS proxy
|
|
function establishConnection() {
|
|
proxyConnect(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');
|
|
// 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', (err) => {
|
|
console.error('SSH Connection error:', err);
|
|
})
|
|
.connect({
|
|
sock: tlsSocket, // Pass the TLS socket as the connection
|
|
username: 'user', // Replace with the correct SSH username
|
|
privateKey: convertPKCS8toPKCS1(sshKey), // Replace with the path to your private key
|
|
hostHash: 'md5', // Optional: Match host keys by hash
|
|
strictHostKeyChecking: false, // Disable strict host key checking
|
|
});
|
|
});
|
|
}
|
|
// 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() {
|
|
// 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;
|
|
}
|
|
}
|
|
|
|
// const conn = new SSH();
|
|
socket.on('geometry', (cols, rows) => {
|
|
termCols = cols;
|
|
termRows = rows;
|
|
});
|
|
conn.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'));
|
|
});
|
|
|
|
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);
|
|
conn.shell(
|
|
{
|
|
term: socket.request.session.ssh.term,
|
|
cols: termCols,
|
|
rows: termRows,
|
|
},
|
|
(err, stream) => {
|
|
if (err) {
|
|
SSHerror(`EXEC ERROR${err}`);
|
|
conn.end();
|
|
return;
|
|
}
|
|
socket.on('data', (data) => {
|
|
stream.write(data);
|
|
});
|
|
socket.on('control', (controlData) => {
|
|
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);
|
|
conn.end();
|
|
// socket.request.session.destroy()
|
|
});
|
|
socket.on('error', (errMsg) => {
|
|
SSHerror('SOCKET ERROR', errMsg);
|
|
conn.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);
|
|
conn.end();
|
|
});
|
|
stream.stderr.on('data', (data) => {
|
|
console.error(`STDERR: ${data}`);
|
|
});
|
|
},
|
|
);
|
|
});
|
|
|
|
conn.on('end', (err) => {
|
|
SSHerror('CONN END BY HOST', err);
|
|
});
|
|
conn.on('close', (err) => {
|
|
SSHerror('CONN CLOSE', err);
|
|
});
|
|
conn.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]);
|
|
});
|
|
if (
|
|
socket.request.session.username &&
|
|
(socket.request.session.userpassword || socket.request.session.privatekey) &&
|
|
socket.request.session.ssh
|
|
) {
|
|
// 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'),
|
|
// });
|
|
console.log('EVAN HERE');
|
|
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,
|
|
)}`,
|
|
);
|
|
socket.emit('ssherror', 'WEBSOCKET ERROR - Refresh the browser and try again');
|
|
socket.request.session.destroy();
|
|
socket.disconnect(true);
|
|
}
|
|
}
|
|
setupConnection();
|
|
// establishConnection();
|
|
};
|