ssh2
Version:
SSH2 client and server modules written in pure JavaScript for node.js
1,628 lines (1,453 loc) • 64.2 kB
JavaScript
// TODO:
// * add `.connected` or similar property to allow immediate connection
// status checking
// * add/improve debug output during user authentication phase
'use strict';
const {
createHash,
getHashes,
randomFillSync,
} = require('crypto');
const { Socket } = require('net');
const { lookup: dnsLookup } = require('dns');
const EventEmitter = require('events');
const HASHES = getHashes();
const {
COMPAT,
CHANNEL_EXTENDED_DATATYPE: { STDERR },
CHANNEL_OPEN_FAILURE,
DEFAULT_CIPHER,
DEFAULT_COMPRESSION,
DEFAULT_KEX,
DEFAULT_MAC,
DEFAULT_SERVER_HOST_KEY,
DISCONNECT_REASON,
DISCONNECT_REASON_BY_VALUE,
SUPPORTED_CIPHER,
SUPPORTED_COMPRESSION,
SUPPORTED_KEX,
SUPPORTED_MAC,
SUPPORTED_SERVER_HOST_KEY,
} = require('./protocol/constants.js');
const { init: cryptoInit } = require('./protocol/crypto.js');
const Protocol = require('./protocol/Protocol.js');
const { parseKey } = require('./protocol/keyParser.js');
const { SFTP } = require('./protocol/SFTP.js');
const {
bufferCopy,
makeBufferParser,
makeError,
readUInt32BE,
sigSSHToASN1,
writeUInt32BE,
} = require('./protocol/utils.js');
const { AgentContext, createAgent, isAgent } = require('./agent.js');
const {
Channel,
MAX_WINDOW,
PACKET_SIZE,
windowAdjust,
WINDOW_THRESHOLD,
} = require('./Channel.js');
const {
ChannelManager,
generateAlgorithmList,
isWritable,
onChannelOpenFailure,
onCHANNEL_CLOSE,
} = require('./utils.js');
const bufferParser = makeBufferParser();
const sigParser = makeBufferParser();
const RE_OPENSSH = /^OpenSSH_(?:(?![0-4])\d)|(?:\d{2,})/;
const noop = (err) => {};
class Client extends EventEmitter {
constructor() {
super();
this.config = {
host: undefined,
port: undefined,
localAddress: undefined,
localPort: undefined,
forceIPv4: undefined,
forceIPv6: undefined,
keepaliveCountMax: undefined,
keepaliveInterval: undefined,
readyTimeout: undefined,
ident: undefined,
username: undefined,
password: undefined,
privateKey: undefined,
tryKeyboard: undefined,
agent: undefined,
allowAgentFwd: undefined,
authHandler: undefined,
hostHashAlgo: undefined,
hostHashCb: undefined,
strictVendor: undefined,
debug: undefined
};
this._agent = undefined;
this._readyTimeout = undefined;
this._chanMgr = undefined;
this._callbacks = undefined;
this._forwarding = undefined;
this._forwardingUnix = undefined;
this._acceptX11 = undefined;
this._agentFwdEnabled = undefined;
this._remoteVer = undefined;
this._protocol = undefined;
this._sock = undefined;
this._resetKA = undefined;
}
connect(cfg) {
if (this._sock && isWritable(this._sock)) {
this.once('close', () => {
this.connect(cfg);
});
this.end();
return this;
}
this.config.host = cfg.hostname || cfg.host || 'localhost';
this.config.port = cfg.port || 22;
this.config.localAddress = (typeof cfg.localAddress === 'string'
? cfg.localAddress
: undefined);
this.config.localPort = (typeof cfg.localPort === 'string'
|| typeof cfg.localPort === 'number'
? cfg.localPort
: undefined);
this.config.forceIPv4 = cfg.forceIPv4 || false;
this.config.forceIPv6 = cfg.forceIPv6 || false;
this.config.keepaliveCountMax = (typeof cfg.keepaliveCountMax === 'number'
&& cfg.keepaliveCountMax >= 0
? cfg.keepaliveCountMax
: 3);
this.config.keepaliveInterval = (typeof cfg.keepaliveInterval === 'number'
&& cfg.keepaliveInterval > 0
? cfg.keepaliveInterval
: 0);
this.config.readyTimeout = (typeof cfg.readyTimeout === 'number'
&& cfg.readyTimeout >= 0
? cfg.readyTimeout
: 20000);
this.config.ident = (typeof cfg.ident === 'string'
|| Buffer.isBuffer(cfg.ident)
? cfg.ident
: undefined);
const algorithms = {
kex: undefined,
serverHostKey: undefined,
cs: {
cipher: undefined,
mac: undefined,
compress: undefined,
lang: [],
},
sc: undefined,
};
let allOfferDefaults = true;
if (typeof cfg.algorithms === 'object' && cfg.algorithms !== null) {
algorithms.kex = generateAlgorithmList(cfg.algorithms.kex,
DEFAULT_KEX,
SUPPORTED_KEX);
if (algorithms.kex !== DEFAULT_KEX)
allOfferDefaults = false;
algorithms.serverHostKey =
generateAlgorithmList(cfg.algorithms.serverHostKey,
DEFAULT_SERVER_HOST_KEY,
SUPPORTED_SERVER_HOST_KEY);
if (algorithms.serverHostKey !== DEFAULT_SERVER_HOST_KEY)
allOfferDefaults = false;
algorithms.cs.cipher = generateAlgorithmList(cfg.algorithms.cipher,
DEFAULT_CIPHER,
SUPPORTED_CIPHER);
if (algorithms.cs.cipher !== DEFAULT_CIPHER)
allOfferDefaults = false;
algorithms.cs.mac = generateAlgorithmList(cfg.algorithms.hmac,
DEFAULT_MAC,
SUPPORTED_MAC);
if (algorithms.cs.mac !== DEFAULT_MAC)
allOfferDefaults = false;
algorithms.cs.compress = generateAlgorithmList(cfg.algorithms.compress,
DEFAULT_COMPRESSION,
SUPPORTED_COMPRESSION);
if (algorithms.cs.compress !== DEFAULT_COMPRESSION)
allOfferDefaults = false;
if (!allOfferDefaults)
algorithms.sc = algorithms.cs;
}
if (typeof cfg.username === 'string')
this.config.username = cfg.username;
else if (typeof cfg.user === 'string')
this.config.username = cfg.user;
else
throw new Error('Invalid username');
this.config.password = (typeof cfg.password === 'string'
? cfg.password
: undefined);
this.config.privateKey = (typeof cfg.privateKey === 'string'
|| Buffer.isBuffer(cfg.privateKey)
? cfg.privateKey
: undefined);
this.config.localHostname = (typeof cfg.localHostname === 'string'
? cfg.localHostname
: undefined);
this.config.localUsername = (typeof cfg.localUsername === 'string'
? cfg.localUsername
: undefined);
this.config.tryKeyboard = (cfg.tryKeyboard === true);
if (typeof cfg.agent === 'string' && cfg.agent.length)
this.config.agent = createAgent(cfg.agent);
else if (isAgent(cfg.agent))
this.config.agent = cfg.agent;
else
this.config.agent = undefined;
this.config.allowAgentFwd = (cfg.agentForward === true
&& this.config.agent !== undefined);
let authHandler = this.config.authHandler = (
typeof cfg.authHandler === 'function'
|| Array.isArray(cfg.authHandler)
? cfg.authHandler
: undefined
);
this.config.strictVendor = (typeof cfg.strictVendor === 'boolean'
? cfg.strictVendor
: true);
const debug = this.config.debug = (typeof cfg.debug === 'function'
? cfg.debug
: undefined);
if (cfg.agentForward === true && !this.config.allowAgentFwd) {
throw new Error(
'You must set a valid agent path to allow agent forwarding'
);
}
let callbacks = this._callbacks = [];
this._chanMgr = new ChannelManager(this);
this._forwarding = {};
this._forwardingUnix = {};
this._acceptX11 = 0;
this._agentFwdEnabled = false;
this._agent = (this.config.agent ? this.config.agent : undefined);
this._remoteVer = undefined;
let privateKey;
if (this.config.privateKey) {
privateKey = parseKey(this.config.privateKey, cfg.passphrase);
if (privateKey instanceof Error)
throw new Error(`Cannot parse privateKey: ${privateKey.message}`);
if (Array.isArray(privateKey)) {
// OpenSSH's newer format only stores 1 key for now
privateKey = privateKey[0];
}
if (privateKey.getPrivatePEM() === null) {
throw new Error(
'privateKey value does not contain a (valid) private key'
);
}
}
let hostVerifier;
if (typeof cfg.hostVerifier === 'function') {
const hashCb = cfg.hostVerifier;
let hashAlgo;
if (HASHES.indexOf(cfg.hostHash) !== -1) {
// Default to old behavior of hashing on user's behalf
hashAlgo = cfg.hostHash;
}
hostVerifier = (key, verify) => {
if (hashAlgo)
key = createHash(hashAlgo).update(key).digest('hex');
const ret = hashCb(key, verify);
if (ret !== undefined)
verify(ret);
};
}
const sock = this._sock = (cfg.sock || new Socket());
let ready = false;
let sawHeader = false;
if (this._protocol)
this._protocol.cleanup();
const DEBUG_HANDLER = (!debug ? undefined : (p, display, msg) => {
debug(`Debug output from server: ${JSON.stringify(msg)}`);
});
let serverSigAlgs;
const proto = this._protocol = new Protocol({
ident: this.config.ident,
offer: (allOfferDefaults ? undefined : algorithms),
onWrite: (data) => {
if (isWritable(sock))
sock.write(data);
},
onError: (err) => {
if (err.level === 'handshake')
clearTimeout(this._readyTimeout);
if (!proto._destruct)
sock.removeAllListeners('data');
this.emit('error', err);
try {
sock.end();
} catch {}
},
onHeader: (header) => {
sawHeader = true;
this._remoteVer = header.versions.software;
if (header.greeting)
this.emit('greeting', header.greeting);
},
onHandshakeComplete: (negotiated) => {
this.emit('handshake', negotiated);
if (!ready) {
ready = true;
proto.service('ssh-userauth');
}
},
debug,
hostVerifier,
messageHandlers: {
DEBUG: DEBUG_HANDLER,
DISCONNECT: (p, reason, desc) => {
if (reason !== DISCONNECT_REASON.BY_APPLICATION) {
if (!desc) {
desc = DISCONNECT_REASON_BY_VALUE[reason];
if (desc === undefined)
desc = `Unexpected disconnection reason: ${reason}`;
}
const err = new Error(desc);
err.code = reason;
this.emit('error', err);
}
sock.end();
},
SERVICE_ACCEPT: (p, name) => {
if (name === 'ssh-userauth')
tryNextAuth();
},
EXT_INFO: (p, exts) => {
if (serverSigAlgs === undefined) {
for (const ext of exts) {
if (ext.name === 'server-sig-algs') {
serverSigAlgs = ext.algs;
return;
}
}
serverSigAlgs = null;
}
},
USERAUTH_BANNER: (p, msg) => {
this.emit('banner', msg);
},
USERAUTH_SUCCESS: (p) => {
// Start keepalive mechanism
resetKA();
clearTimeout(this._readyTimeout);
this.emit('ready');
},
USERAUTH_FAILURE: (p, authMethods, partialSuccess) => {
// For key-based authentication, check if we should retry the current
// key with a different algorithm first
if (curAuth.keyAlgos) {
const oldKeyAlgo = curAuth.keyAlgos[0][0];
if (debug)
debug(`Client: ${curAuth.type} (${oldKeyAlgo}) auth failed`);
curAuth.keyAlgos.shift();
if (curAuth.keyAlgos.length) {
const [keyAlgo, hashAlgo] = curAuth.keyAlgos[0];
switch (curAuth.type) {
case 'agent':
proto.authPK(
curAuth.username,
curAuth.agentCtx.currentKey(),
keyAlgo
);
return;
case 'publickey':
proto.authPK(curAuth.username, curAuth.key, keyAlgo);
return;
case 'hostbased':
proto.authHostbased(curAuth.username,
curAuth.key,
curAuth.localHostname,
curAuth.localUsername,
keyAlgo,
(buf, cb) => {
const signature = curAuth.key.sign(buf, hashAlgo);
if (signature instanceof Error) {
signature.message =
`Error while signing with key: ${signature.message}`;
signature.level = 'client-authentication';
this.emit('error', signature);
return tryNextAuth();
}
cb(signature);
});
return;
}
} else {
curAuth.keyAlgos = undefined;
}
}
if (curAuth.type === 'agent') {
const pos = curAuth.agentCtx.pos();
debug && debug(`Client: Agent key #${pos + 1} failed`);
return tryNextAgentKey();
}
debug && debug(`Client: ${curAuth.type} auth failed`);
curPartial = partialSuccess;
curAuthsLeft = authMethods;
tryNextAuth();
},
USERAUTH_PASSWD_CHANGEREQ: (p, prompt) => {
if (curAuth.type === 'password') {
// TODO: support a `changePrompt()` on `curAuth` that defaults to
// emitting 'change password' as before
this.emit('change password', prompt, (newPassword) => {
proto.authPassword(
this.config.username,
this.config.password,
newPassword
);
});
}
},
USERAUTH_PK_OK: (p) => {
let keyAlgo;
let hashAlgo;
if (curAuth.keyAlgos)
[keyAlgo, hashAlgo] = curAuth.keyAlgos[0];
if (curAuth.type === 'agent') {
const key = curAuth.agentCtx.currentKey();
proto.authPK(curAuth.username, key, keyAlgo, (buf, cb) => {
const opts = { hash: hashAlgo };
curAuth.agentCtx.sign(key, buf, opts, (err, signed) => {
if (err) {
err.level = 'agent';
this.emit('error', err);
} else {
return cb(signed);
}
tryNextAgentKey();
});
});
} else if (curAuth.type === 'publickey') {
proto.authPK(curAuth.username, curAuth.key, keyAlgo, (buf, cb) => {
const signature = curAuth.key.sign(buf, hashAlgo);
if (signature instanceof Error) {
signature.message =
`Error signing data with key: ${signature.message}`;
signature.level = 'client-authentication';
this.emit('error', signature);
return tryNextAuth();
}
cb(signature);
});
}
},
USERAUTH_INFO_REQUEST: (p, name, instructions, prompts) => {
if (curAuth.type === 'keyboard-interactive') {
const nprompts = (Array.isArray(prompts) ? prompts.length : 0);
if (nprompts === 0) {
debug && debug(
'Client: Sending automatic USERAUTH_INFO_RESPONSE'
);
proto.authInfoRes();
return;
}
// We sent a keyboard-interactive user authentication request and
// now the server is sending us the prompts we need to present to
// the user
curAuth.prompt(
name,
instructions,
'',
prompts,
(answers) => {
proto.authInfoRes(answers);
}
);
}
},
REQUEST_SUCCESS: (p, data) => {
if (callbacks.length)
callbacks.shift()(false, data);
},
REQUEST_FAILURE: (p) => {
if (callbacks.length)
callbacks.shift()(true);
},
GLOBAL_REQUEST: (p, name, wantReply, data) => {
switch (name) {
case 'hostkeys-00@openssh.com':
// Automatically verify keys before passing to end user
hostKeysProve(this, data, (err, keys) => {
if (err)
return;
this.emit('hostkeys', keys);
});
if (wantReply)
proto.requestSuccess();
break;
default:
// Auto-reject all other global requests, this can be especially
// useful if the server is sending us dummy keepalive global
// requests
if (wantReply)
proto.requestFailure();
}
},
CHANNEL_OPEN: (p, info) => {
// Handle incoming requests from server, typically a forwarded TCP or
// X11 connection
onCHANNEL_OPEN(this, info);
},
CHANNEL_OPEN_CONFIRMATION: (p, info) => {
const channel = this._chanMgr.get(info.recipient);
if (typeof channel !== 'function')
return;
const isSFTP = (channel.type === 'sftp');
const type = (isSFTP ? 'session' : channel.type);
const chanInfo = {
type,
incoming: {
id: info.recipient,
window: MAX_WINDOW,
packetSize: PACKET_SIZE,
state: 'open'
},
outgoing: {
id: info.sender,
window: info.window,
packetSize: info.packetSize,
state: 'open'
}
};
const instance = (
isSFTP
? new SFTP(this, chanInfo, { debug })
: new Channel(this, chanInfo)
);
this._chanMgr.update(info.recipient, instance);
channel(undefined, instance);
},
CHANNEL_OPEN_FAILURE: (p, recipient, reason, description) => {
const channel = this._chanMgr.get(recipient);
if (typeof channel !== 'function')
return;
const info = { reason, description };
onChannelOpenFailure(this, recipient, info, channel);
},
CHANNEL_DATA: (p, recipient, data) => {
const channel = this._chanMgr.get(recipient);
if (typeof channel !== 'object' || channel === null)
return;
// The remote party should not be sending us data if there is no
// window space available ...
// TODO: raise error on data with not enough window?
if (channel.incoming.window === 0)
return;
channel.incoming.window -= data.length;
if (channel.push(data) === false) {
channel._waitChanDrain = true;
return;
}
if (channel.incoming.window <= WINDOW_THRESHOLD)
windowAdjust(channel);
},
CHANNEL_EXTENDED_DATA: (p, recipient, data, type) => {
if (type !== STDERR)
return;
const channel = this._chanMgr.get(recipient);
if (typeof channel !== 'object' || channel === null)
return;
// The remote party should not be sending us data if there is no
// window space available ...
// TODO: raise error on data with not enough window?
if (channel.incoming.window === 0)
return;
channel.incoming.window -= data.length;
if (!channel.stderr.push(data)) {
channel._waitChanDrain = true;
return;
}
if (channel.incoming.window <= WINDOW_THRESHOLD)
windowAdjust(channel);
},
CHANNEL_WINDOW_ADJUST: (p, recipient, amount) => {
const channel = this._chanMgr.get(recipient);
if (typeof channel !== 'object' || channel === null)
return;
// The other side is allowing us to send `amount` more bytes of data
channel.outgoing.window += amount;
if (channel._waitWindow) {
channel._waitWindow = false;
if (channel._chunk) {
channel._write(channel._chunk, null, channel._chunkcb);
} else if (channel._chunkcb) {
channel._chunkcb();
} else if (channel._chunkErr) {
channel.stderr._write(channel._chunkErr,
null,
channel._chunkcbErr);
} else if (channel._chunkcbErr) {
channel._chunkcbErr();
}
}
},
CHANNEL_SUCCESS: (p, recipient) => {
const channel = this._chanMgr.get(recipient);
if (typeof channel !== 'object' || channel === null)
return;
this._resetKA();
if (channel._callbacks.length)
channel._callbacks.shift()(false);
},
CHANNEL_FAILURE: (p, recipient) => {
const channel = this._chanMgr.get(recipient);
if (typeof channel !== 'object' || channel === null)
return;
this._resetKA();
if (channel._callbacks.length)
channel._callbacks.shift()(true);
},
CHANNEL_REQUEST: (p, recipient, type, wantReply, data) => {
const channel = this._chanMgr.get(recipient);
if (typeof channel !== 'object' || channel === null)
return;
const exit = channel._exit;
if (exit.code !== undefined)
return;
switch (type) {
case 'exit-status':
channel.emit('exit', exit.code = data);
return;
case 'exit-signal':
channel.emit('exit',
exit.code = null,
exit.signal = `SIG${data.signal}`,
exit.dump = data.coreDumped,
exit.desc = data.errorMessage);
return;
}
// Keepalive request? OpenSSH will send one as a channel request if
// there is a channel open
if (wantReply)
p.channelFailure(channel.outgoing.id);
},
CHANNEL_EOF: (p, recipient) => {
const channel = this._chanMgr.get(recipient);
if (typeof channel !== 'object' || channel === null)
return;
if (channel.incoming.state !== 'open')
return;
channel.incoming.state = 'eof';
if (channel.readable)
channel.push(null);
if (channel.stderr.readable)
channel.stderr.push(null);
},
CHANNEL_CLOSE: (p, recipient) => {
onCHANNEL_CLOSE(this, recipient, this._chanMgr.get(recipient));
},
},
});
sock.pause();
// TODO: check keepalive implementation
// Keepalive-related
const kainterval = this.config.keepaliveInterval;
const kacountmax = this.config.keepaliveCountMax;
let kacount = 0;
let katimer;
const sendKA = () => {
if (++kacount > kacountmax) {
clearInterval(katimer);
if (sock.readable) {
const err = new Error('Keepalive timeout');
err.level = 'client-timeout';
this.emit('error', err);
sock.destroy();
}
return;
}
if (isWritable(sock)) {
// Append dummy callback to keep correct callback order
callbacks.push(resetKA);
proto.ping();
} else {
clearInterval(katimer);
}
};
function resetKA() {
if (kainterval > 0) {
kacount = 0;
clearInterval(katimer);
if (isWritable(sock))
katimer = setInterval(sendKA, kainterval);
}
}
this._resetKA = resetKA;
const onDone = (() => {
let called = false;
return () => {
if (called)
return;
called = true;
if (wasConnected && !sawHeader) {
const err =
makeError('Connection lost before handshake', 'protocol', true);
this.emit('error', err);
}
};
})();
const onConnect = (() => {
let called = false;
return () => {
if (called)
return;
called = true;
wasConnected = true;
debug && debug('Socket connected');
this.emit('connect');
cryptoInit.then(() => {
proto.start();
sock.on('data', (data) => {
try {
proto.parse(data, 0, data.length);
} catch (ex) {
this.emit('error', ex);
try {
if (isWritable(sock))
sock.end();
} catch {}
}
});
// Drain stderr if we are connection hopping using an exec stream
if (sock.stderr && typeof sock.stderr.resume === 'function')
sock.stderr.resume();
sock.resume();
}).catch((err) => {
this.emit('error', err);
try {
if (isWritable(sock))
sock.end();
} catch {}
});
};
})();
let wasConnected = false;
sock.on('connect', onConnect)
.on('timeout', () => {
this.emit('timeout');
}).on('error', (err) => {
debug && debug(`Socket error: ${err.message}`);
clearTimeout(this._readyTimeout);
err.level = 'client-socket';
this.emit('error', err);
}).on('end', () => {
debug && debug('Socket ended');
onDone();
proto.cleanup();
clearTimeout(this._readyTimeout);
clearInterval(katimer);
this.emit('end');
}).on('close', () => {
debug && debug('Socket closed');
onDone();
proto.cleanup();
clearTimeout(this._readyTimeout);
clearInterval(katimer);
this.emit('close');
// Notify outstanding channel requests of disconnection ...
const callbacks_ = callbacks;
callbacks = this._callbacks = [];
const err = new Error('No response from server');
for (let i = 0; i < callbacks_.length; ++i)
callbacks_[i](err);
// Simulate error for any channels waiting to be opened
this._chanMgr.cleanup(err);
});
// Begin authentication handling ===========================================
let curAuth;
let curPartial = null;
let curAuthsLeft = null;
const authsAllowed = ['none'];
if (this.config.password !== undefined)
authsAllowed.push('password');
if (privateKey !== undefined)
authsAllowed.push('publickey');
if (this._agent !== undefined)
authsAllowed.push('agent');
if (this.config.tryKeyboard)
authsAllowed.push('keyboard-interactive');
if (privateKey !== undefined
&& this.config.localHostname !== undefined
&& this.config.localUsername !== undefined) {
authsAllowed.push('hostbased');
}
if (Array.isArray(authHandler))
authHandler = makeSimpleAuthHandler(authHandler);
else if (typeof authHandler !== 'function')
authHandler = makeSimpleAuthHandler(authsAllowed);
let hasSentAuth = false;
const doNextAuth = (nextAuth) => {
if (hasSentAuth)
return;
hasSentAuth = true;
if (nextAuth === false) {
const err = new Error('All configured authentication methods failed');
err.level = 'client-authentication';
this.emit('error', err);
this.end();
return;
}
if (typeof nextAuth === 'string') {
// Remain backwards compatible with original `authHandler()` usage,
// which only supported passing names of next method to try using data
// from the `connect()` config object
const type = nextAuth;
if (authsAllowed.indexOf(type) === -1)
return skipAuth(`Authentication method not allowed: ${type}`);
const username = this.config.username;
switch (type) {
case 'password':
nextAuth = { type, username, password: this.config.password };
break;
case 'publickey':
nextAuth = { type, username, key: privateKey };
break;
case 'hostbased':
nextAuth = {
type,
username,
key: privateKey,
localHostname: this.config.localHostname,
localUsername: this.config.localUsername,
};
break;
case 'agent':
nextAuth = {
type,
username,
agentCtx: new AgentContext(this._agent),
};
break;
case 'keyboard-interactive':
nextAuth = {
type,
username,
prompt: (...args) => this.emit('keyboard-interactive', ...args),
};
break;
case 'none':
nextAuth = { type, username };
break;
default:
return skipAuth(
`Skipping unsupported authentication method: ${nextAuth}`
);
}
} else if (typeof nextAuth !== 'object' || nextAuth === null) {
return skipAuth(
`Skipping invalid authentication attempt: ${nextAuth}`
);
} else {
const username = nextAuth.username;
if (typeof username !== 'string') {
return skipAuth(
`Skipping invalid authentication attempt: ${nextAuth}`
);
}
const type = nextAuth.type;
switch (type) {
case 'password': {
const { password } = nextAuth;
if (typeof password !== 'string' && !Buffer.isBuffer(password))
return skipAuth('Skipping invalid password auth attempt');
nextAuth = { type, username, password };
break;
}
case 'publickey': {
const key = parseKey(nextAuth.key, nextAuth.passphrase);
if (key instanceof Error)
return skipAuth('Skipping invalid key auth attempt');
if (!key.isPrivateKey())
return skipAuth('Skipping non-private key');
nextAuth = { type, username, key };
break;
}
case 'hostbased': {
const { localHostname, localUsername } = nextAuth;
const key = parseKey(nextAuth.key, nextAuth.passphrase);
if (key instanceof Error
|| typeof localHostname !== 'string'
|| typeof localUsername !== 'string') {
return skipAuth('Skipping invalid hostbased auth attempt');
}
if (!key.isPrivateKey())
return skipAuth('Skipping non-private key');
nextAuth = { type, username, key, localHostname, localUsername };
break;
}
case 'agent': {
let agent = nextAuth.agent;
if (typeof agent === 'string' && agent.length) {
agent = createAgent(agent);
} else if (!isAgent(agent)) {
return skipAuth(
`Skipping invalid agent: ${nextAuth.agent}`
);
}
nextAuth = { type, username, agentCtx: new AgentContext(agent) };
break;
}
case 'keyboard-interactive': {
const { prompt } = nextAuth;
if (typeof prompt !== 'function') {
return skipAuth(
'Skipping invalid keyboard-interactive auth attempt'
);
}
nextAuth = { type, username, prompt };
break;
}
case 'none':
nextAuth = { type, username };
break;
default:
return skipAuth(
`Skipping unsupported authentication method: ${nextAuth}`
);
}
}
curAuth = nextAuth;
// Begin authentication method's process
try {
const username = curAuth.username;
switch (curAuth.type) {
case 'password':
proto.authPassword(username, curAuth.password);
break;
case 'publickey': {
let keyAlgo;
curAuth.keyAlgos = getKeyAlgos(this, curAuth.key, serverSigAlgs);
if (curAuth.keyAlgos) {
if (curAuth.keyAlgos.length) {
keyAlgo = curAuth.keyAlgos[0][0];
} else {
return skipAuth(
'Skipping key authentication (no mutual hash algorithm)'
);
}
}
proto.authPK(username, curAuth.key, keyAlgo);
break;
}
case 'hostbased': {
let keyAlgo;
let hashAlgo;
curAuth.keyAlgos = getKeyAlgos(this, curAuth.key, serverSigAlgs);
if (curAuth.keyAlgos) {
if (curAuth.keyAlgos.length) {
[keyAlgo, hashAlgo] = curAuth.keyAlgos[0];
} else {
return skipAuth(
'Skipping hostbased authentication (no mutual hash algorithm)'
);
}
}
proto.authHostbased(username,
curAuth.key,
curAuth.localHostname,
curAuth.localUsername,
keyAlgo,
(buf, cb) => {
const signature = curAuth.key.sign(buf, hashAlgo);
if (signature instanceof Error) {
signature.message =
`Error while signing with key: ${signature.message}`;
signature.level = 'client-authentication';
this.emit('error', signature);
return tryNextAuth();
}
cb(signature);
});
break;
}
case 'agent':
curAuth.agentCtx.init((err) => {
if (err) {
err.level = 'agent';
this.emit('error', err);
return tryNextAuth();
}
tryNextAgentKey();
});
break;
case 'keyboard-interactive':
proto.authKeyboard(username);
break;
case 'none':
proto.authNone(username);
break;
}
} finally {
hasSentAuth = false;
}
};
function skipAuth(msg) {
debug && debug(msg);
process.nextTick(tryNextAuth);
}
function tryNextAuth() {
hasSentAuth = false;
const auth = authHandler(curAuthsLeft, curPartial, doNextAuth);
if (hasSentAuth || auth === undefined)
return;
doNextAuth(auth);
}
const tryNextAgentKey = () => {
if (curAuth.type === 'agent') {
const key = curAuth.agentCtx.nextKey();
if (key === false) {
debug && debug('Agent: No more keys left to try');
debug && debug('Client: agent auth failed');
tryNextAuth();
} else {
const pos = curAuth.agentCtx.pos();
let keyAlgo;
curAuth.keyAlgos = getKeyAlgos(this, key, serverSigAlgs);
if (curAuth.keyAlgos) {
if (curAuth.keyAlgos.length) {
keyAlgo = curAuth.keyAlgos[0][0];
} else {
debug && debug(
`Agent: Skipping key #${pos + 1} (no mutual hash algorithm)`
);
tryNextAgentKey();
return;
}
}
debug && debug(`Agent: Trying key #${pos + 1}`);
proto.authPK(curAuth.username, key, keyAlgo);
}
}
};
const startTimeout = () => {
if (this.config.readyTimeout > 0) {
this._readyTimeout = setTimeout(() => {
const err = new Error('Timed out while waiting for handshake');
err.level = 'client-timeout';
this.emit('error', err);
sock.destroy();
}, this.config.readyTimeout);
}
};
if (!cfg.sock) {
let host = this.config.host;
const forceIPv4 = this.config.forceIPv4;
const forceIPv6 = this.config.forceIPv6;
debug && debug(`Client: Trying ${host} on port ${this.config.port} ...`);
const doConnect = () => {
startTimeout();
sock.connect({
host,
port: this.config.port,
localAddress: this.config.localAddress,
localPort: this.config.localPort
});
sock.setMaxListeners(0);
sock.setTimeout(typeof cfg.timeout === 'number' ? cfg.timeout : 0);
};
if ((!forceIPv4 && !forceIPv6) || (forceIPv4 && forceIPv6)) {
doConnect();
} else {
dnsLookup(host, (forceIPv4 ? 4 : 6), (err, address, family) => {
if (err) {
const type = (forceIPv4 ? 'IPv4' : 'IPv6');
const error = new Error(
`Error while looking up ${type} address for '${host}': ${err}`
);
clearTimeout(this._readyTimeout);
error.level = 'client-dns';
this.emit('error', error);
this.emit('close');
return;
}
host = address;
doConnect();
});
}
} else {
// Custom socket passed in
startTimeout();
if (typeof sock.connecting === 'boolean') {
// net.Socket
if (!sock.connecting) {
// Already connected
onConnect();
}
} else {
// Assume socket/stream is already "connected"
onConnect();
}
}
return this;
}
end() {
if (this._sock && isWritable(this._sock)) {
this._protocol.disconnect(DISCONNECT_REASON.BY_APPLICATION);
this._sock.end();
}
return this;
}
destroy() {
this._sock && isWritable(this._sock) && this._sock.destroy();
return this;
}
exec(cmd, opts, cb) {
if (!this._sock || !isWritable(this._sock))
throw new Error('Not connected');
if (typeof opts === 'function') {
cb = opts;
opts = {};
}
const extraOpts = { allowHalfOpen: (opts.allowHalfOpen !== false) };
openChannel(this, 'session', extraOpts, (err, chan) => {
if (err) {
cb(err);
return;
}
const todo = [];
function reqCb(err) {
if (err) {
chan.close();
cb(err);
return;
}
if (todo.length)
todo.shift()();
}
if (this.config.allowAgentFwd === true
|| (opts
&& opts.agentForward === true
&& this._agent !== undefined)) {
todo.push(() => reqAgentFwd(chan, reqCb));
}
if (typeof opts === 'object' && opts !== null) {
if (typeof opts.env === 'object' && opts.env !== null)
reqEnv(chan, opts.env);
if ((typeof opts.pty === 'object' && opts.pty !== null)
|| opts.pty === true) {
todo.push(() => reqPty(chan, opts.pty, reqCb));
}
if ((typeof opts.x11 === 'object' && opts.x11 !== null)
|| opts.x11 === 'number'
|| opts.x11 === true) {
todo.push(() => reqX11(chan, opts.x11, reqCb));
}
}
todo.push(() => reqExec(chan, cmd, opts, cb));
todo.shift()();
});
return this;
}
shell(wndopts, opts, cb) {
if (!this._sock || !isWritable(this._sock))
throw new Error('Not connected');
if (typeof wndopts === 'function') {
cb = wndopts;
wndopts = opts = undefined;
} else if (typeof opts === 'function') {
cb = opts;
opts = undefined;
}
if (wndopts && (wndopts.x11 !== undefined || wndopts.env !== undefined)) {
opts = wndopts;
wndopts = undefined;
}
openChannel(this, 'session', (err, chan) => {
if (err) {
cb(err);
return;
}
const todo = [];
function reqCb(err) {
if (err) {
chan.close();
cb(err);
return;
}
if (todo.length)
todo.shift()();
}
if (this.config.allowAgentFwd === true
|| (opts
&& opts.agentForward === true
&& this._agent !== undefined)) {
todo.push(() => reqAgentFwd(chan, reqCb));
}
if (wndopts !== false)
todo.push(() => reqPty(chan, wndopts, reqCb));
if (typeof opts === 'object' && opts !== null) {
if (typeof opts.env === 'object' && opts.env !== null)
reqEnv(chan, opts.env);
if ((typeof opts.x11 === 'object' && opts.x11 !== null)
|| opts.x11 === 'number'
|| opts.x11 === true) {
todo.push(() => reqX11(chan, opts.x11, reqCb));
}
}
todo.push(() => reqShell(chan, cb));
todo.shift()();
});
return this;
}
subsys(name, cb) {
if (!this._sock || !isWritable(this._sock))
throw new Error('Not connected');
openChannel(this, 'session', (err, chan) => {
if (err) {
cb(err);
return;
}
reqSubsystem(chan, name, (err, stream) => {
if (err) {
cb(err);
return;
}
cb(undefined, stream);
});
});
return this;
}
forwardIn(bindAddr, bindPort, cb) {
if (!this._sock || !isWritable(this._sock))
throw new Error('Not connected');
// Send a request for the server to start forwarding TCP connections to us
// on a particular address and port
const wantReply = (typeof cb === 'function');
if (wantReply) {
this._callbacks.push((had_err, data) => {
if (had_err) {
cb(had_err !== true
? had_err
: new Error(`Unable to bind to ${bindAddr}:${bindPort}`));
return;
}
let realPort = bindPort;
if (bindPort === 0 && data && data.length >= 4) {
realPort = readUInt32BE(data, 0);
if (!(this._protocol._compatFlags & COMPAT.DYN_RPORT_BUG))
bindPort = realPort;
}
this._forwarding[`${bindAddr}:${bindPort}`] = realPort;
cb(undefined, realPort);
});
}
this._protocol.tcpipForward(bindAddr, bindPort, wantReply);
return this;
}
unforwardIn(bindAddr, bindPort, cb) {
if (!this._sock || !isWritable(this._sock))
throw new Error('Not connected');
// Send a request to stop forwarding us new connections for a particular
// address and port
const wantReply = (typeof cb === 'function');
if (wantReply) {
this._callbacks.push((had_err) => {
if (had_err) {
cb(had_err !== true
? had_err
: new Error(`Unable to unbind from ${bindAddr}:${bindPort}`));
return;
}
delete this._forwarding[`${bindAddr}:${bindPort}`];
cb();
});
}
this._protocol.cancelTcpipForward(bindAddr, bindPort, wantReply);
return this;
}
forwardOut(srcIP, srcPort, dstIP, dstPort, cb) {
if (!this._sock || !isWritable(this._sock))
throw new Error('Not connected');
// Send a request to forward a TCP connection to the server
const cfg = {
srcIP: srcIP,
srcPort: srcPort,
dstIP: dstIP,
dstPort: dstPort
};
if (typeof cb !== 'function')
cb = noop;
openChannel(this, 'direct-tcpip', cfg, cb);
return this;
}
openssh_noMoreSessions(cb) {
if (!this._sock || !isWritable(this._sock))
throw new Error('Not connected');
const wantReply = (typeof cb === 'function');
if (!this.config.strictVendor
|| (this.config.strictVendor && RE_OPENSSH.test(this._remoteVer))) {
if (wantReply) {
this._callbacks.push((had_err) => {
if (had_err) {
cb(had_err !== true
? had_err
: new Error('Unable to disable future sessions'));
return;
}
cb();
});
}
this._protocol.openssh_noMoreSessions(wantReply);
return this;
}
if (!wantReply)
return this;
process.nextTick(
cb,
new Error(
'strictVendor enabled and server is not OpenSSH or compatible version'
)
);
return this;
}
openssh_forwardInStreamLocal(socketPath, cb) {
if (!this._sock || !isWritable(this._sock))
throw new Error('Not connected');
const wantReply = (typeof cb === 'function');
if (!this.config.strictVendor
|| (this.config.strictVendor && RE_OPENSSH.test(this._remoteVer))) {
if (wantReply) {
this._callbacks.push((had_err) => {
if (had_err) {
cb(had_err !== true
? had_err
: new Error(`Unable to bind to ${socketPath}`));
return;
}
this._forwardingUnix[socketPath] = true;
cb();
});
}
this._protocol.openssh_streamLocalForward(socketPath, wantReply);
return this;
}
if (!wantReply)
return this;
process.nextTick(
cb,
new Error(
'strictVendor enabled and server is not OpenSSH or compatible version'
)
);
return this;
}
openssh_unforwardInStreamLocal(socketPath, cb) {
if (!this._sock || !isWritable(this._sock))
throw new Error('Not connected');
const wantReply = (typeof cb === 'function');
if (!this.config.strictVendor
|| (this.config.strictVendor && RE_OPENSSH.test(this._remoteVer))) {
if (wantReply) {
this._callbacks.push((had_err) => {
if (had_err) {
cb(had_err !== true
? had_err
: new Error(`Unable to unbind from ${socketPath}`));
return;
}
delete this._forwardingUnix[socketPath];
cb();
});
}
this._protocol.openssh_cancelStreamLocalForward(socketPath, wantReply);
return this;
}
if (!wantReply)
return this;
process.nextTick(
cb,
new Error(
'strictVendor enabled and server is not OpenSSH or compatible version'
)
);
return this;
}
openssh_forwardOutStreamLocal(socketPath, cb) {
if (!this._sock || !isWritable(this._sock))
throw new Error('Not connected');
if (typeof cb !== 'function')
cb = noop;
if (!this.config.strictVendor
|| (this.config.strictVendor && RE_OPENSSH.test(this._remoteVer))) {
openChannel(this, 'direct-streamlocal@openssh.com', { socketPath }, cb);
return this;
}
process.nextTick(
cb,
new Error(
'strictVendor enabled and server is not OpenSSH or compatible version'
)
);
return this;
}
sftp(env, cb) {
if (!this._sock || !isWritable(this._sock))
throw new Error('Not connected');
if (typeof env === 'function') {
cb = env;
env = undefined;
}
openChannel(this, 'sftp', (err, sftp) => {
if (err) {
cb(err);
return;
}
const reqSubsystemCb = (err, sftp_) => {
if (err) {
cb(err);
return;
}
function removeListeners() {
sftp.removeListener('ready', onReady);
sftp.removeListener('error', onError);
sftp.removeListener('exit', onExit);
sftp.removeListener('close', onExit);
}
function onReady() {
// TODO: do not remove exit/close in case remote end closes the
// channel abruptly and we need to notify outstanding callbacks
removeListeners();
cb(undefined, sftp);
}
function onError(err) {
removeListeners();
cb(err);
}
function onExit(code, signal) {
removeListeners();
let msg;
if (typeof code === 'number')
msg = `Received exit code ${code} while establishing SFTP session`;
else if (signal !== undefined)
msg = `Received signal ${signal} while establishing SFTP session`;
else
msg = 'Received unexpected SFTP session termination';
const err = new Error(msg);
err.code = code;
err.signal = signal;
cb(err);
}
sftp.on('ready', onReady)
.on('error', onError)
.on('exit', onExit)
.on('close', onExit);
sftp._init();
};
if (typeof env === 'object' && env !== null) {
reqEnv(sftp, env, (err) => {
if (err) {
cb(err);
return;
}
reqSubsystem(sftp, 'sftp', reqSubsystemCb);
});
} else {