8 Commits

4 changed files with 106 additions and 53 deletions
Split View
  1. +1
    -1
      lib/account.js
  2. +60
    -38
      lib/ssh.js
  3. +44
    -13
      lib/tui/index.js
  4. +1
    -1
      test/account.js

+ 1
- 1
lib/account.js View File

@ -65,7 +65,7 @@ const Account = function(username, password){
throw new Error('INVALID_ADDRESS');
if(typeof host.port !== 'number' || !Number.isInteger(host.port))
throw new Error('INVALID_PORT');
if(typeof host.user !== 'string')
if(typeof host.username !== 'string')
throw new Error('INVALID_USER');
if(typeof host.password !== 'string')
throw new Error('INVALID_PASSWORD');


+ 60
- 38
lib/ssh.js View File

@ -1,43 +1,65 @@
const { Client } = require('ssh2');
module.exports = {
test: ({ address, port, user, password }) => new Promise((resolve, reject) => {
const client = new Client();
client.on('error', ({ level: error }) => {
if(error === 'client-authentication')
reject('WRONG_CREDENTIALS');
else if(['client-socket', 'client-timeout'].includes(error))
reject('WRONG_DESTINATION');
else
reject();
});
client.on('ready', () => {
client.end();
resolve();
});
client.connect({
host: address,
port,
username: user,
password
});
}),
session: ({ address, port, user, password }, onReady, onClose, onError) => {
const client = new Client();
client.on('ready', () => client.shell((error, clientStream) => {
if(error) return onError(error);
clientStream.on('unpipe', () => {
onClose(clientStream);
client.end();
});
onReady(clientStream);
}));
client.on('error', onError);
client.connect({
host: address,
port,
username: user,
password
});
chainedSession: (hostsChain, onReady, onClose, onError, isTest) => {
const clients = [];
(async () => {
for(let i = 0; i < hostsChain.length; i++){
const
prevClient = clients[i-1],
{ address, port, username, password } = hostsChain[i],
nextHost = hostsChain[i+1],
client = clients[i] = new Client();
client.on('error', onError);
await new Promise(resolve => {
client.on('ready', () => {
if(nextHost) resolve();
else {
if(isTest){
client.end();
onReady();
}
else client.shell((error, clientStream) => {
if(error) return onError(error);
clientStream.on('unpipe', () => {
onClose(clientStream);
client.end();
});
onReady(clientStream);
});
}
});
if(prevClient){
client.on('end', () => prevClient.end());
prevClient.forwardOut('127.0.0.1', 12345 + i, address, port, (error, prevClientStream) => {
if(error) onError(error);
try {
client.connect({
sock: prevClientStream,
username,
password
});
}
catch(error){
onError(error);
}
});
}
else {
try {
client.connect({
host: address,
port,
username,
password
});
}
catch(error){
onError(error);
}
}
});
}
})();
}
};

+ 44
- 13
lib/tui/index.js View File

@ -77,7 +77,7 @@ const getViews = screen => {
},
pwnedPassword: quit => {
text(
{ content: '[!] Your password has been breached.\nSee https://haveibeenpwned.com/Passwords for more details.\nPlease quit and try again.' },
{ content: '[!] Your password has been breached.\nSee https://haveibeenpwned.com/passwords for more details.\nPlease quit and try again.' },
{ color: 'red', borders: true }
);
const quitButton = button({ top: 5, content: 'Quit' });
@ -193,7 +193,7 @@ const getViews = screen => {
height: tableHeight,
columnsWidth: [ 50, 50 ],
headers: [ 'Host', 'URI' ],
data: hosts.map(host => [ host.name, `${host.user}@${host.address}:${host.port}` ])
data: hosts.map(host => [ host.name, `${host.username}@${host.address}:${host.port}` ])
});
selectButton.on('press', () => {
hostsTable.focus();
@ -229,7 +229,7 @@ const getViews = screen => {
});
button({ content: 'Edit' }, undefined, selectedHostActions)
.on('press', () => callback({ action: 'edit', host: selectedHost }));
const removeButton = button({ content: 'Remove' }, { color: 'red' }, selectedHostActions);
const removeButton = button({ content: 'Remove' }, { color: 'yellow' }, selectedHostActions);
removeButton.on('press', () => {
if(removeButton.content === 'Remove'){
removeButton.setContent('Confirm');
@ -244,7 +244,7 @@ const getViews = screen => {
else
addButton.focus();
},
setHost: ({ name, address, port, user, password } = {}, callback) => {
setHost: ({ name, address, port, username, password, chain } = {}, callback) => {
let action;
const hostForm = form();
text({ content: 'Add host' }, { borderBottom: true }, hostForm);
@ -256,9 +256,11 @@ const getViews = screen => {
text({ top: n += 4, content: 'Port :' }, hostForm);
textbox({ top: n += 2, name: 'port', content: port }, hostForm);
text({ top: n += 4, content: 'User :' }, hostForm);
const userInput = textbox({ top: n += 2, name: 'user', content: user }, hostForm);
const userInput = textbox({ top: n += 2, name: 'username', content: username }, hostForm);
text({ top: n += 4, content: 'Password :' }, hostForm);
textbox({ top: n += 2, name: 'password', censor: true, content: password }, hostForm);
text({ top: n += 4, content: 'Chain (optional, comma-separated) :' }, hostForm);
textbox({ top: n += 2, name: 'chain', content: chain }, hostForm);
const actions = flexContainer({ top: n += 4, width: 100 }, hostForm);
button({ content: 'Cancel' }, undefined, actions)
.on('press', () => callback({ action: 'cancel' }));
@ -281,16 +283,18 @@ const getViews = screen => {
{ color: 'red' },
hostForm
);
hostForm.on('submit', ({ name, address, port, user, password }) => {
hostForm.on('submit', ({ name, address, port, username, password, chain }) => {
port = parseInt(port);
if(action === 'test'){
successText.hide();
errorText.hide();
testButton.setContent('Testing');
screen.render();
}
// noinspection JSUnusedGlobalSymbols
callback({
action,
host: { name, address, port, user, password },
host: { name, address, port, username, password, chain },
res: res => {
if(action === 'test'){
testButton.setContent('Test');
@ -301,8 +305,9 @@ const getViews = screen => {
else {
successText.hide();
const error = ({
WRONG_CREDENTIALS: 'wrong user or password',
WRONG_DESTINATION: 'wrong address or port'
WRONG_CREDENTIALS: 'wrong username or password',
WRONG_DESTINATION: 'wrong address or port',
INVALID_CHAIN: 'invalid hosts chain'
})[res];
errorText.setContent(`SSH connection failed : ${error || `unknown error (${res})`}`);
errorText.show();
@ -397,7 +402,7 @@ const getViews = screen => {
const actions = flexContainer({ top: n += 2 }, deleteAccountForm);
button({ content: 'Cancel' }, undefined, actions)
.on('press', () => callback({ isCancelled: true }));
const submitButton = button({ content: 'Submit' }, { color: 'red' }, actions)
const submitButton = button({ content: 'Submit' }, { color: 'yellow' }, actions)
submitButton.on('press', () => deleteAccountForm.submit());
const errorText = text(
{ top: n + 4 },
@ -511,8 +516,28 @@ const getControllers = (stream, end) => {
switch(action2){
case 'cancel': return _controllers.main(user);
case 'test': {
const { address, port, user, password } = host2;
ssh.test({ address, port, user, password }).then(() => res(true)).catch(res);
const
{ address, port, username, password, chain } = host2,
hosts = user.getHosts(),
hostsChain = [
...(chain ? chain.split(',').map(name => hosts.find(host => host.name === name)) : []),
{ address, port, username, password }
];
if(hostsChain.includes(undefined)) return res('INVALID_CHAIN');
ssh.chainedSession(
hostsChain,
() => res(true),
undefined,
({ level: error }) => {
if(error === 'client-authentication')
res('WRONG_CREDENTIALS');
else if(['client-socket', 'client-timeout'].includes(error))
res('WRONG_DESTINATION');
else
res();
},
true
);
return;
}
case 'submit': {
@ -536,7 +561,13 @@ const getControllers = (stream, end) => {
return _controllers.main(user);
}
case 'connect': {
return ssh.session(host, clientStream => {
const
{ chain } = host,
hosts = user.getHosts();
return ssh.chainedSession([
...(chain ? chain.split(',').map(name => hosts.find(host => host.name === name)) : []),
host
], clientStream => {
screen.destroy();
clientStream.stdout.pipe(stream.stdin);
stream.stdin.pipe(clientStream.stdout);


+ 1
- 1
test/account.js View File

@ -110,7 +110,7 @@ describe('account', () => {
name: 'host',
address: 'host',
port: 22,
user: 'user',
username: 'username',
password: 'password'
};


Loading…
Cancel
Save