var crypto = require('crypto'); var Socket = require('net').Socket; var dnsLookup = require('dns').lookup; var EventEmitter = require('events').EventEmitter; var inherits = require('util').inherits; var HASHES = crypto.getHashes(); var ssh2_streams = require('ssh2-streams'); var SSH2Stream = ssh2_streams.SSH2Stream; var SFTPStream = ssh2_streams.SFTPStream; var consts = ssh2_streams.constants; var BUGS = consts.BUGS; var ALGORITHMS = consts.ALGORITHMS; var EDDSA_SUPPORTED = consts.EDDSA_SUPPORTED; var parseKey = ssh2_streams.utils.parseKey; var Channel = require('./Channel'); var agentQuery = require('./agent'); var SFTPWrapper = require('./SFTPWrapper'); var readUInt32BE = require('./buffer-helpers').readUInt32BE; var MAX_CHANNEL = Math.pow(2, 32) - 1; var RE_OPENSSH = /^OpenSSH_(?:(?![0-4])\d)|(?:\d{2,})/; var DEBUG_NOOP = function(msg) {}; function Client() { if (!(this instanceof Client)) return new Client(); EventEmitter.call(this); this.config = { host: undefined, port: undefined, localAddress: undefined, localPort: undefined, forceIPv4: undefined, forceIPv6: undefined, keepaliveCountMax: undefined, keepaliveInterval: undefined, readyTimeout: undefined, username: undefined, password: undefined, privateKey: undefined, tryKeyboard: undefined, agent: undefined, allowAgentFwd: undefined, authHandler: undefined, hostHashAlgo: undefined, hostHashCb: undefined, strictVendor: undefined, debug: undefined }; this._readyTimeout = undefined; this._channels = undefined; this._callbacks = undefined; this._forwarding = undefined; this._forwardingUnix = undefined; this._acceptX11 = undefined; this._agentFwdEnabled = undefined; this._curChan = undefined; this._remoteVer = undefined; this._sshstream = undefined; this._sock = undefined; this._resetKA = undefined; } inherits(Client, EventEmitter); Client.prototype.connect = function(cfg) { var self = this; if (this._sock && this._sock.writable) { this.once('close', function() { self.connect(cfg); }); this.end(); return; } 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); var algorithms = { kex: undefined, kexBuf: undefined, cipher: undefined, cipherBuf: undefined, serverHostKey: undefined, serverHostKeyBuf: undefined, hmac: undefined, hmacBuf: undefined, compress: undefined, compressBuf: undefined }; var i; if (typeof cfg.algorithms === 'object' && cfg.algorithms !== null) { var algosSupported; var algoList; algoList = cfg.algorithms.kex; if (Array.isArray(algoList) && algoList.length > 0) { algosSupported = ALGORITHMS.SUPPORTED_KEX; for (i = 0; i < algoList.length; ++i) { if (algosSupported.indexOf(algoList[i]) === -1) throw new Error('Unsupported key exchange algorithm: ' + algoList[i]); } algorithms.kex = algoList; } algoList = cfg.algorithms.cipher; if (Array.isArray(algoList) && algoList.length > 0) { algosSupported = ALGORITHMS.SUPPORTED_CIPHER; for (i = 0; i < algoList.length; ++i) { if (algosSupported.indexOf(algoList[i]) === -1) throw new Error('Unsupported cipher algorithm: ' + algoList[i]); } algorithms.cipher = algoList; } algoList = cfg.algorithms.serverHostKey; if (Array.isArray(algoList) && algoList.length > 0) { algosSupported = ALGORITHMS.SUPPORTED_SERVER_HOST_KEY; for (i = 0; i < algoList.length; ++i) { if (algosSupported.indexOf(algoList[i]) === -1) { throw new Error('Unsupported server host key algorithm: ' + algoList[i]); } } algorithms.serverHostKey = algoList; } algoList = cfg.algorithms.hmac; if (Array.isArray(algoList) && algoList.length > 0) { algosSupported = ALGORITHMS.SUPPORTED_HMAC; for (i = 0; i < algoList.length; ++i) { if (algosSupported.indexOf(algoList[i]) === -1) throw new Error('Unsupported HMAC algorithm: ' + algoList[i]); } algorithms.hmac = algoList; } algoList = cfg.algorithms.compress; if (Array.isArray(algoList) && algoList.length > 0) { algosSupported = ALGORITHMS.SUPPORTED_COMPRESS; for (i = 0; i < algoList.length; ++i) { if (algosSupported.indexOf(algoList[i]) === -1) throw new Error('Unsupported compression algorithm: ' + algoList[i]); } algorithms.compress = algoList; } } if (algorithms.compress === undefined) { if (cfg.compress) { algorithms.compress = ['zlib@openssh.com', 'zlib']; if (cfg.compress !== 'force') algorithms.compress.push('none'); } else if (cfg.compress === false) algorithms.compress = ['none']; } 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.length ? cfg.localHostname : undefined); this.config.localUsername = (typeof cfg.localUsername === 'string' && cfg.localUsername.length ? cfg.localUsername : undefined); this.config.tryKeyboard = (cfg.tryKeyboard === true); this.config.agent = (typeof cfg.agent === 'string' && cfg.agent.length ? cfg.agent : undefined); this.config.allowAgentFwd = (cfg.agentForward === true && this.config.agent !== undefined); var authHandler = this.config.authHandler = ( typeof cfg.authHandler === 'function' ? cfg.authHandler : undefined ); this.config.strictVendor = (typeof cfg.strictVendor === 'boolean' ? cfg.strictVendor : true); var debug = this.config.debug = (typeof cfg.debug === 'function' ? cfg.debug : DEBUG_NOOP); if (cfg.agentForward === true && !this.config.allowAgentFwd) throw new Error('You must set a valid agent path to allow agent forwarding'); var callbacks = this._callbacks = []; this._channels = {}; this._forwarding = {}; this._forwardingUnix = {}; this._acceptX11 = 0; this._agentFwdEnabled = false; this._curChan = -1; this._remoteVer = undefined; var 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)) privateKey = privateKey[0]; // OpenSSH's newer format only stores 1 key for now if (privateKey.getPrivatePEM() === null) throw new Error('privateKey value does not contain a (valid) private key'); } var stream = this._sshstream = new SSH2Stream({ algorithms: algorithms, debug: (debug === DEBUG_NOOP ? undefined : debug) }); var sock = this._sock = (cfg.sock || new Socket()); // drain stderr if we are connection hopping using an exec stream if (this._sock.stderr) this._sock.stderr.resume(); // keepalive-related var kainterval = this.config.keepaliveInterval; var kacountmax = this.config.keepaliveCountMax; var kacount = 0; var katimer; function sendKA() { if (++kacount > kacountmax) { clearInterval(katimer); if (sock.readable) { var err = new Error('Keepalive timeout'); err.level = 'client-timeout'; self.emit('error', err); sock.destroy(); } return; } if (sock.writable) { // append dummy callback to keep correct callback order callbacks.push(resetKA); stream.ping(); } else clearInterval(katimer); } function resetKA() { if (kainterval > 0) { kacount = 0; clearInterval(katimer); if (sock.writable) katimer = setInterval(sendKA, kainterval); } } this._resetKA = resetKA; stream.on('USERAUTH_BANNER', function(msg) { self.emit('banner', msg); }); sock.on('connect', function() { debug('DEBUG: Client: Connected'); self.emit('connect'); if (!cfg.sock) stream.pipe(sock).pipe(stream); }).on('timeout', function() { self.emit('timeout'); }).on('error', function(err) { clearTimeout(self._readyTimeout); err.level = 'client-socket'; self.emit('error', err); }).on('end', function() { stream.unpipe(sock); clearTimeout(self._readyTimeout); clearInterval(katimer); self.emit('end'); }).on('close', function() { stream.unpipe(sock); clearTimeout(self._readyTimeout); clearInterval(katimer); self.emit('close'); // notify outstanding channel requests of disconnection ... var callbacks_ = callbacks; var err = new Error('No response from server'); callbacks = self._callbacks = []; for (i = 0; i < callbacks_.length; ++i) callbacks_[i](err); // simulate error for any channels waiting to be opened. this is safe // against successfully opened channels because the success and failure // event handlers are automatically removed when a success/failure response // is received var channels = self._channels; var chanNos = Object.keys(channels); self._channels = {}; for (i = 0; i < chanNos.length; ++i) { var ev1 = stream.emit('CHANNEL_OPEN_FAILURE:' + chanNos[i], err); // emitting CHANNEL_CLOSE should be safe too and should help for any // special channels which might otherwise keep the process alive, such // as agent forwarding channels which have open unix sockets ... var ev2 = stream.emit('CHANNEL_CLOSE:' + chanNos[i]); var earlyCb; if (!ev1 && !ev2 && (earlyCb = channels[chanNos[i]]) && typeof earlyCb === 'function') { earlyCb(err); } } }); stream.on('drain', function() { self.emit('drain'); }).once('header', function(header) { self._remoteVer = header.versions.software; if (header.greeting) self.emit('greeting', header.greeting); }).on('continue', function() { self.emit('continue'); }).on('error', function(err) { if (err.level === undefined) err.level = 'protocol'; else if (err.level === 'handshake') clearTimeout(self._readyTimeout); self.emit('error', err); }).on('end', function() { sock.resume(); }); if (typeof cfg.hostVerifier === 'function') { if (HASHES.indexOf(cfg.hostHash) === -1) throw new Error('Invalid host hash algorithm: ' + cfg.hostHash); var hashCb = cfg.hostVerifier; var hasher = crypto.createHash(cfg.hostHash); stream.once('fingerprint', function(key, verify) { hasher.update(key); var ret = hashCb(hasher.digest('hex'), verify); if (ret !== undefined) verify(ret); }); } // begin authentication handling ============================================= var curAuth; var curPartial = null; var curAuthsLeft = null; var agentKeys; var agentKeyPos = 0; var authsAllowed = ['none']; if (this.config.password !== undefined) authsAllowed.push('password'); if (privateKey !== undefined) authsAllowed.push('publickey'); if (this.config.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 (authHandler === undefined) { var authPos = 0; authHandler = function authHandler(authsLeft, partial, cb) { if (authPos === authsAllowed.length) return false; return authsAllowed[authPos++]; }; } var hasSentAuth = false; function doNextAuth(authName) { hasSentAuth = true; if (authName === false) { stream.removeListener('USERAUTH_FAILURE', onUSERAUTH_FAILURE); stream.removeListener('USERAUTH_PK_OK', onUSERAUTH_PK_OK); var err = new Error('All configured authentication methods failed'); err.level = 'client-authentication'; self.emit('error', err); if (stream.writable) self.end(); return; } if (authsAllowed.indexOf(authName) === -1) throw new Error('Authentication method not allowed: ' + authName); curAuth = authName; switch (curAuth) { case 'password': stream.authPassword(self.config.username, self.config.password); break; case 'publickey': stream.authPK(self.config.username, privateKey); stream.once('USERAUTH_PK_OK', onUSERAUTH_PK_OK); break; case 'hostbased': function hostbasedCb(buf, cb) { var signature = privateKey.sign(buf); if (signature instanceof Error) { signature.message = 'Error while signing data with privateKey: ' + signature.message; signature.level = 'client-authentication'; self.emit('error', signature); return tryNextAuth(); } cb(signature); } stream.authHostbased(self.config.username, privateKey, self.config.localHostname, self.config.localUsername, hostbasedCb); break; case 'agent': agentQuery(self.config.agent, function(err, keys) { if (err) { err.level = 'agent'; self.emit('error', err); agentKeys = undefined; return tryNextAuth(); } else if (keys.length === 0) { debug('DEBUG: Agent: No keys stored in agent'); agentKeys = undefined; return tryNextAuth(); } agentKeys = keys; agentKeyPos = 0; stream.authPK(self.config.username, keys[0]); stream.once('USERAUTH_PK_OK', onUSERAUTH_PK_OK); }); break; case 'keyboard-interactive': stream.authKeyboard(self.config.username); stream.on('USERAUTH_INFO_REQUEST', onUSERAUTH_INFO_REQUEST); break; case 'none': stream.authNone(self.config.username); break; } } function tryNextAuth() { hasSentAuth = false; var auth = authHandler(curAuthsLeft, curPartial, doNextAuth); if (hasSentAuth || auth === undefined) return; doNextAuth(auth); } function tryNextAgentKey() { if (curAuth === 'agent') { if (agentKeyPos >= agentKeys.length) return; if (++agentKeyPos >= agentKeys.length) { debug('DEBUG: Agent: No more keys left to try'); debug('DEBUG: Client: agent auth failed'); agentKeys = undefined; tryNextAuth(); } else { debug('DEBUG: Agent: Trying key #' + (agentKeyPos + 1)); stream.authPK(self.config.username, agentKeys[agentKeyPos]); stream.once('USERAUTH_PK_OK', onUSERAUTH_PK_OK); } } } function onUSERAUTH_INFO_REQUEST(name, instructions, lang, prompts) { var nprompts = (Array.isArray(prompts) ? prompts.length : 0); if (nprompts === 0) { debug('DEBUG: Client: Sending automatic USERAUTH_INFO_RESPONSE'); return stream.authInfoRes(); } // we sent a keyboard-interactive user authentication request and now the // server is sending us the prompts we need to present to the user self.emit('keyboard-interactive', name, instructions, lang, prompts, function(answers) { stream.authInfoRes(answers); } ); } function onUSERAUTH_PK_OK() { if (curAuth === 'agent') { var agentKey = agentKeys[agentKeyPos]; var keyLen = readUInt32BE(agentKey, 0); var pubKeyFullType = agentKey.toString('ascii', 4, 4 + keyLen); var pubKeyType = pubKeyFullType.slice(4); // Check that we support the key type first // TODO: move key type checking logic to ssh2-streams switch (pubKeyFullType) { case 'ssh-rsa': case 'ssh-dss': case 'ecdsa-sha2-nistp256': case 'ecdsa-sha2-nistp384': case 'ecdsa-sha2-nistp521': break; default: if (EDDSA_SUPPORTED && pubKeyFullType === 'ssh-ed25519') break; debug('DEBUG: Agent: Skipping unsupported key type: ' + pubKeyFullType); return tryNextAgentKey(); } stream.authPK(self.config.username, agentKey, function(buf, cb) { agentQuery(self.config.agent, agentKey, pubKeyType, buf, function(err, signed) { if (err) { err.level = 'agent'; self.emit('error', err); } else { var sigFullTypeLen = readUInt32BE(signed, 0); if (4 + sigFullTypeLen + 4 < signed.length) { var sigFullType = signed.toString('ascii', 4, 4 + sigFullTypeLen); if (sigFullType !== pubKeyFullType) { err = new Error('Agent key/signature type mismatch'); err.level = 'agent'; self.emit('error', err); } else { // skip algoLen + algo + sigLen return cb(signed.slice(4 + sigFullTypeLen + 4)); } } } tryNextAgentKey(); }); }); } else if (curAuth === 'publickey') { stream.authPK(self.config.username, privateKey, function(buf, cb) { var signature = privateKey.sign(buf); if (signature instanceof Error) { signature.message = 'Error while signing data with privateKey: ' + signature.message; signature.level = 'client-authentication'; self.emit('error', signature); return tryNextAuth(); } cb(signature); }); } } function onUSERAUTH_FAILURE(authsLeft, partial) { stream.removeListener('USERAUTH_PK_OK', onUSERAUTH_PK_OK); stream.removeListener('USERAUTH_INFO_REQUEST', onUSERAUTH_INFO_REQUEST); if (curAuth === 'agent') { debug('DEBUG: Client: Agent key #' + (agentKeyPos + 1) + ' failed'); return tryNextAgentKey(); } else { debug('DEBUG: Client: ' + curAuth + ' auth failed'); } curPartial = partial; curAuthsLeft = authsLeft; tryNextAuth(); } stream.once('USERAUTH_SUCCESS', function() { stream.removeListener('USERAUTH_FAILURE', onUSERAUTH_FAILURE); stream.removeListener('USERAUTH_INFO_REQUEST', onUSERAUTH_INFO_REQUEST); // start keepalive mechanism resetKA(); clearTimeout(self._readyTimeout); self.emit('ready'); }).on('USERAUTH_FAILURE', onUSERAUTH_FAILURE); // end authentication handling =============================================== // handle initial handshake completion stream.once('ready', function() { stream.service('ssh-userauth'); stream.once('SERVICE_ACCEPT', function(svcName) { if (svcName === 'ssh-userauth') tryNextAuth(); }); }); // handle incoming requests from server, typically a forwarded TCP or X11 // connection stream.on('CHANNEL_OPEN', function(info) { onCHANNEL_OPEN(self, info); }); // handle responses for tcpip-forward and other global requests stream.on('REQUEST_SUCCESS', function(data) { if (callbacks.length) callbacks.shift()(false, data); }).on('REQUEST_FAILURE', function() { if (callbacks.length) callbacks.shift()(true); }); stream.on('GLOBAL_REQUEST', function(name, wantReply, data) { // auto-reject all global requests, this can be especially useful if the // server is sending us dummy keepalive global requests if (wantReply) stream.requestFailure(); }); if (!cfg.sock) { var host = this.config.host; var forceIPv4 = this.config.forceIPv4; var forceIPv6 = this.config.forceIPv6; debug('DEBUG: Client: Trying ' + host + ' on port ' + this.config.port + ' ...'); function doConnect() { startTimeout(); self._sock.connect({ host: host, port: self.config.port, localAddress: self.config.localAddress, localPort: self.config.localPort }); self._sock.setNoDelay(true); self._sock.setMaxListeners(0); self._sock.setTimeout(typeof cfg.timeout === 'number' ? cfg.timeout : 0); } if ((!forceIPv4 && !forceIPv6) || (forceIPv4 && forceIPv6)) doConnect(); else { dnsLookup(host, (forceIPv4 ? 4 : 6), function(err, address, family) { if (err) { var error = new Error('Error while looking up ' + (forceIPv4 ? 'IPv4' : 'IPv6') + ' address for host ' + host + ': ' + err); clearTimeout(self._readyTimeout); error.level = 'client-dns'; self.emit('error', error); self.emit('close'); return; } host = address; doConnect(); }); } } else { startTimeout(); stream.pipe(sock).pipe(stream); } function startTimeout() { if (self.config.readyTimeout > 0) { self._readyTimeout = setTimeout(function() { var err = new Error('Timed out while waiting for handshake'); err.level = 'client-timeout'; self.emit('error', err); sock.destroy(); }, self.config.readyTimeout); } } }; Client.prototype.end = function() { if (this._sock && this._sock.writable && this._sshstream && this._sshstream.writable) return this._sshstream.disconnect(); return false; }; Client.prototype.destroy = function() { this._sock && this._sock.destroy(); }; Client.prototype.exec = function(cmd, opts, cb) { if (!this._sock || !this._sock.writable || !this._sshstream || !this._sshstream.writable) throw new Error('Not connected'); if (typeof opts === 'function') { cb = opts; opts = {}; } var self = this; var extraOpts = { allowHalfOpen: (opts.allowHalfOpen !== false) }; return openChannel(this, 'session', extraOpts, function(err, chan) { if (err) return cb(err); var todo = []; function reqCb(err) { if (err) { chan.close(); return cb(err); } if (todo.length) todo.shift()(); } if (self.config.allowAgentFwd === true || (opts && opts.agentForward === true && self.config.agent !== undefined)) { todo.push(function() { 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(function() { reqPty(chan, opts.pty, reqCb); }); } if ((typeof opts.x11 === 'object' && opts.x11 !== null) || opts.x11 === 'number' || opts.x11 === true) { todo.push(function() { reqX11(chan, opts.x11, reqCb); }); } } todo.push(function() { reqExec(chan, cmd, opts, cb); }); todo.shift()(); }); }; Client.prototype.shell = function(wndopts, opts, cb) { if (!this._sock || !this._sock.writable || !this._sshstream || !this._sshstream.writable) throw new Error('Not connected'); // start an interactive terminal/shell session var self = this; 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; } return openChannel(this, 'session', function(err, chan) { if (err) return cb(err); var todo = []; function reqCb(err) { if (err) { chan.close(); return cb(err); } if (todo.length) todo.shift()(); } if (self.config.allowAgentFwd === true || (opts && opts.agentForward === true && self.config.agent !== undefined)) { todo.push(function() { reqAgentFwd(chan, reqCb); }); } if (wndopts !== false) todo.push(function() { 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(function() { reqX11(chan, opts.x11, reqCb); }); } } todo.push(function() { reqShell(chan, cb); }); todo.shift()(); }); }; Client.prototype.subsys = function(name, cb) { if (!this._sock || !this._sock.writable || !this._sshstream || !this._sshstream.writable) throw new Error('Not connected'); return openChannel(this, 'session', function(err, chan) { if (err) return cb(err); reqSubsystem(chan, name, function(err, stream) { if (err) return cb(err); cb(undefined, stream); }); }); }; Client.prototype.sftp = function(cb) { if (!this._sock || !this._sock.writable || !this._sshstream || !this._sshstream.writable) throw new Error('Not connected'); var self = this; // start an SFTP session return openChannel(this, 'session', function(err, chan) { if (err) return cb(err); reqSubsystem(chan, 'sftp', function(err, stream) { if (err) return cb(err); var serverIdentRaw = self._sshstream._state.incoming.identRaw; var cfg = { debug: self.config.debug }; var sftp = new SFTPStream(cfg, serverIdentRaw); function onError(err) { sftp.removeListener('ready', onReady); stream.removeListener('exit', onExit); cb(err); } function onReady() { sftp.removeListener('error', onError); stream.removeListener('exit', onExit); cb(undefined, new SFTPWrapper(sftp)); } function onExit(code, signal) { sftp.removeListener('ready', onReady); sftp.removeListener('error', onError); var msg; if (typeof code === 'number') { msg = 'Received exit code ' + code + ' while establishing SFTP session'; } else { msg = 'Received signal ' + signal + ' while establishing SFTP session'; } var err = new Error(msg); err.code = code; err.signal = signal; cb(err); } sftp.once('error', onError) .once('ready', onReady) .once('close', function() { stream.end(); }); // OpenSSH server sends an exit-status if there was a problem spinning up // an sftp server child process, so we listen for that here in order to // properly raise an error. stream.once('exit', onExit); sftp.pipe(stream).pipe(sftp); }); }); }; Client.prototype.forwardIn = function(bindAddr, bindPort, cb) { if (!this._sock || !this._sock.writable || !this._sshstream || !this._sshstream.writable) throw new Error('Not connected'); // send a request for the server to start forwarding TCP connections to us // on a particular address and port var self = this; var wantReply = (typeof cb === 'function'); if (wantReply) { this._callbacks.push(function(had_err, data) { if (had_err) { return cb(had_err !== true ? had_err : new Error('Unable to bind to ' + bindAddr + ':' + bindPort)); } var realPort = bindPort; if (bindPort === 0 && data && data.length >= 4) { realPort = readUInt32BE(data, 0); if (!(self._sshstream.remoteBugs & BUGS.DYN_RPORT_BUG)) bindPort = realPort; } self._forwarding[bindAddr + ':' + bindPort] = realPort; cb(undefined, realPort); }); } return this._sshstream.tcpipForward(bindAddr, bindPort, wantReply); }; Client.prototype.unforwardIn = function(bindAddr, bindPort, cb) { if (!this._sock || !this._sock.writable || !this._sshstream || !this._sshstream.writable) throw new Error('Not connected'); // send a request to stop forwarding us new connections for a particular // address and port var self = this; var wantReply = (typeof cb === 'function'); if (wantReply) { this._callbacks.push(function(had_err) { if (had_err) { return cb(had_err !== true ? had_err : new Error('Unable to unbind from ' + bindAddr + ':' + bindPort)); } delete self._forwarding[bindAddr + ':' + bindPort]; cb(); }); } return this._sshstream.cancelTcpipForward(bindAddr, bindPort, wantReply); }; Client.prototype.forwardOut = function(srcIP, srcPort, dstIP, dstPort, cb) { if (!this._sock || !this._sock.writable || !this._sshstream || !this._sshstream.writable) throw new Error('Not connected'); // send a request to forward a TCP connection to the server var cfg = { srcIP: srcIP, srcPort: srcPort, dstIP: dstIP, dstPort: dstPort }; return openChannel(this, 'direct-tcpip', cfg, cb); }; Client.prototype.openssh_noMoreSessions = function(cb) { if (!this._sock || !this._sock.writable || !this._sshstream || !this._sshstream.writable) throw new Error('Not connected'); var wantReply = (typeof cb === 'function'); if (!this.config.strictVendor || (this.config.strictVendor && RE_OPENSSH.test(this._remoteVer))) { if (wantReply) { this._callbacks.push(function(had_err) { if (had_err) { return cb(had_err !== true ? had_err : new Error('Unable to disable future sessions')); } cb(); }); } return this._sshstream.openssh_noMoreSessions(wantReply); } else if (wantReply) { process.nextTick(function() { cb(new Error('strictVendor enabled and server is not OpenSSH or compatible version')); }); } return true; }; Client.prototype.openssh_forwardInStreamLocal = function(socketPath, cb) { if (!this._sock || !this._sock.writable || !this._sshstream || !this._sshstream.writable) throw new Error('Not connected'); var wantReply = (typeof cb === 'function'); var self = this; if (!this.config.strictVendor || (this.config.strictVendor && RE_OPENSSH.test(this._remoteVer))) { if (wantReply) { this._callbacks.push(function(had_err) { if (had_err) { return cb(had_err !== true ? had_err : new Error('Unable to bind to ' + socketPath)); } self._forwardingUnix[socketPath] = true; cb(); }); } return this._sshstream.openssh_streamLocalForward(socketPath, wantReply); } else if (wantReply) { process.nextTick(function() { cb(new Error('strictVendor enabled and server is not OpenSSH or compatible version')); }); } return true; }; Client.prototype.openssh_unforwardInStreamLocal = function(socketPath, cb) { if (!this._sock || !this._sock.writable || !this._sshstream || !this._sshstream.writable) throw new Error('Not connected'); var wantReply = (typeof cb === 'function'); var self = this; if (!this.config.strictVendor || (this.config.strictVendor && RE_OPENSSH.test(this._remoteVer))) { if (wantReply) { this._callbacks.push(function(had_err) { if (had_err) { return cb(had_err !== true ? had_err : new Error('Unable to unbind on ' + socketPath)); } delete self._forwardingUnix[socketPath]; cb(); }); } return this._sshstream.openssh_cancelStreamLocalForward(socketPath, wantReply); } else if (wantReply) { process.nextTick(function() { cb(new Error('strictVendor enabled and server is not OpenSSH or compatible version')); }); } return true; }; Client.prototype.openssh_forwardOutStreamLocal = function(socketPath, cb) { if (!this._sock || !this._sock.writable || !this._sshstream || !this._sshstream.writable) throw new Error('Not connected'); if (!this.config.strictVendor || (this.config.strictVendor && RE_OPENSSH.test(this._remoteVer))) { var cfg = { socketPath: socketPath }; return openChannel(this, 'direct-streamlocal@openssh.com', cfg, cb); } else { process.nextTick(function() { cb(new Error('strictVendor enabled and server is not OpenSSH or compatible version')); }); } return true; }; function openChannel(self, type, opts, cb) { // ask the server to open a channel for some purpose // (e.g. session (sftp, exec, shell), or forwarding a TCP connection var localChan = nextChannel(self); var initWindow = Channel.MAX_WINDOW; var maxPacket = Channel.PACKET_SIZE; var ret = true; if (localChan === false) return cb(new Error('No free channels available')); if (typeof opts === 'function') { cb = opts; opts = {}; } self._channels[localChan] = cb; var sshstream = self._sshstream; sshstream.once('CHANNEL_OPEN_CONFIRMATION:' + localChan, onSuccess) .once('CHANNEL_OPEN_FAILURE:' + localChan, onFailure) .once('CHANNEL_CLOSE:' + localChan, onFailure); if (type === 'session') ret = sshstream.session(localChan, initWindow, maxPacket); else if (type === 'direct-tcpip') ret = sshstream.directTcpip(localChan, initWindow, maxPacket, opts); else if (type === 'direct-streamlocal@openssh.com') { ret = sshstream.openssh_directStreamLocal(localChan, initWindow, maxPacket, opts); } return ret; function onSuccess(info) { sshstream.removeListener('CHANNEL_OPEN_FAILURE:' + localChan, onFailure); sshstream.removeListener('CHANNEL_CLOSE:' + localChan, onFailure); var chaninfo = { type: type, incoming: { id: localChan, window: initWindow, packetSize: maxPacket, state: 'open' }, outgoing: { id: info.sender, window: info.window, packetSize: info.packetSize, state: 'open' } }; cb(undefined, new Channel(chaninfo, self)); } function onFailure(info) { sshstream.removeListener('CHANNEL_OPEN_CONFIRMATION:' + localChan, onSuccess); sshstream.removeListener('CHANNEL_OPEN_FAILURE:' + localChan, onFailure); sshstream.removeListener('CHANNEL_CLOSE:' + localChan, onFailure); delete self._channels[localChan]; var err; if (info instanceof Error) err = info; else if (typeof info === 'object' && info !== null) { err = new Error('(SSH) Channel open failure: ' + info.description); err.reason = info.reason; err.lang = info.lang; } else { err = new Error('(SSH) Channel open failure: ' + 'server closed channel unexpectedly'); err.reason = err.lang = ''; } cb(err); } } function nextChannel(self) { // get the next available channel number // optimized path if (self._curChan < MAX_CHANNEL) return ++self._curChan; // slower lookup path for (var i = 0, channels = self._channels; i < MAX_CHANNEL; ++i) if (!channels[i]) return i; return false; } function reqX11(chan, screen, cb) { // asks server to start sending us X11 connections var cfg = { single: false, protocol: 'MIT-MAGIC-COOKIE-1', cookie: undefined, screen: 0 }; if (typeof screen === 'function') { cb = screen; } else if (typeof screen === 'object' && screen !== null) { if (typeof screen.single === 'boolean') cfg.single = screen.single; if (typeof screen.screen === 'number') cfg.screen = screen.screen; if (typeof screen.protocol === 'string') cfg.protocol = screen.protocol; if (typeof screen.cookie === 'string') cfg.cookie = screen.cookie; else if (Buffer.isBuffer(screen.cookie)) cfg.cookie = screen.cookie.toString('hex'); } if (cfg.cookie === undefined) cfg.cookie = randomCookie(); var wantReply = (typeof cb === 'function'); if (chan.outgoing.state !== 'open') { wantReply && cb(new Error('Channel is not open')); return true; } if (wantReply) { chan._callbacks.push(function(had_err) { if (had_err) { return cb(had_err !== true ? had_err : new Error('Unable to request X11')); } chan._hasX11 = true; ++chan._client._acceptX11; chan.once('close', function() { if (chan._client._acceptX11) --chan._client._acceptX11; }); cb(); }); } return chan._client._sshstream.x11Forward(chan.outgoing.id, cfg, wantReply); } function reqPty(chan, opts, cb) { var rows = 24; var cols = 80; var width = 640; var height = 480; var term = 'vt100'; if (typeof opts === 'function') cb = opts; else if (typeof opts === 'object' && opts !== null) { if (typeof opts.rows === 'number') rows = opts.rows; if (typeof opts.cols === 'number') cols = opts.cols; if (typeof opts.width === 'number') width = opts.width; if (typeof opts.height === 'number') height = opts.height; if (typeof opts.term === 'string') term = opts.term; } var wantReply = (typeof cb === 'function'); if (chan.outgoing.state !== 'open') { wantReply && cb(new Error('Channel is not open')); return true; } if (wantReply) { chan._callbacks.push(function(had_err) { if (had_err) { return cb(had_err !== true ? had_err : new Error('Unable to request a pseudo-terminal')); } cb(); }); } return chan._client._sshstream.pty(chan.outgoing.id, rows, cols, height, width, term, null, wantReply); } function reqAgentFwd(chan, cb) { var wantReply = (typeof cb === 'function'); if (chan.outgoing.state !== 'open') { wantReply && cb(new Error('Channel is not open')); return true; } else if (chan._client._agentFwdEnabled) { wantReply && cb(false); return true; } chan._client._agentFwdEnabled = true; chan._callbacks.push(function(had_err) { if (had_err) { chan._client._agentFwdEnabled = false; wantReply && cb(had_err !== true ? had_err : new Error('Unable to request agent forwarding')); return; } wantReply && cb(); }); return chan._client._sshstream.openssh_agentForward(chan.outgoing.id, true); } function reqShell(chan, cb) { if (chan.outgoing.state !== 'open') { cb(new Error('Channel is not open')); return true; } chan._callbacks.push(function(had_err) { if (had_err) { return cb(had_err !== true ? had_err : new Error('Unable to open shell')); } chan.subtype = 'shell'; cb(undefined, chan); }); return chan._client._sshstream.shell(chan.outgoing.id, true); } function reqExec(chan, cmd, opts, cb) { if (chan.outgoing.state !== 'open') { cb(new Error('Channel is not open')); return true; } chan._callbacks.push(function(had_err) { if (had_err) { return cb(had_err !== true ? had_err : new Error('Unable to exec')); } chan.subtype = 'exec'; chan.allowHalfOpen = (opts.allowHalfOpen !== false); cb(undefined, chan); }); return chan._client._sshstream.exec(chan.outgoing.id, cmd, true); } function reqEnv(chan, env) { if (chan.outgoing.state !== 'open') return true; var ret = true; var keys = Object.keys(env || {}); var key; var val; for (var i = 0, len = keys.length; i < len; ++i) { key = keys[i]; val = env[key]; ret = chan._client._sshstream.env(chan.outgoing.id, key, val, false); } return ret; } function reqSubsystem(chan, name, cb) { if (chan.outgoing.state !== 'open') { cb(new Error('Channel is not open')); return true; } chan._callbacks.push(function(had_err) { if (had_err) { return cb(had_err !== true ? had_err : new Error('Unable to start subsystem: ' + name)); } chan.subtype = 'subsystem'; cb(undefined, chan); }); return chan._client._sshstream.subsystem(chan.outgoing.id, name, true); } function onCHANNEL_OPEN(self, info) { // the server is trying to open a channel with us, this is usually when // we asked the server to forward us connections on some port and now they // are asking us to accept/deny an incoming connection on their side var localChan = false; var reason; function accept() { var chaninfo = { type: info.type, incoming: { id: localChan, window: Channel.MAX_WINDOW, packetSize: Channel.PACKET_SIZE, state: 'open' }, outgoing: { id: info.sender, window: info.window, packetSize: info.packetSize, state: 'open' } }; var stream = new Channel(chaninfo, self); self._sshstream.channelOpenConfirm(info.sender, localChan, Channel.MAX_WINDOW, Channel.PACKET_SIZE); return stream; } function reject() { if (reason === undefined) { if (localChan === false) reason = consts.CHANNEL_OPEN_FAILURE.RESOURCE_SHORTAGE; else reason = consts.CHANNEL_OPEN_FAILURE.CONNECT_FAILED; } self._sshstream.channelOpenFail(info.sender, reason, '', ''); } if (info.type === 'forwarded-tcpip' || info.type === 'x11' || info.type === 'auth-agent@openssh.com' || info.type === 'forwarded-streamlocal@openssh.com') { // check for conditions for automatic rejection var rejectConn = ( (info.type === 'forwarded-tcpip' && self._forwarding[info.data.destIP + ':' + info.data.destPort] === undefined) || (info.type === 'forwarded-streamlocal@openssh.com' && self._forwardingUnix[info.data.socketPath] === undefined) || (info.type === 'x11' && self._acceptX11 === 0) || (info.type === 'auth-agent@openssh.com' && !self._agentFwdEnabled) ); if (!rejectConn) { localChan = nextChannel(self); if (localChan === false) { self.config.debug('DEBUG: Client: Automatic rejection of incoming channel open: no channels available'); rejectConn = true; } else self._channels[localChan] = true; } else { reason = consts.CHANNEL_OPEN_FAILURE.ADMINISTRATIVELY_PROHIBITED; self.config.debug('DEBUG: Client: Automatic rejection of incoming channel open: unexpected channel open for: ' + info.type); } // TODO: automatic rejection after some timeout? if (rejectConn) reject(); if (localChan !== false) { if (info.type === 'forwarded-tcpip') { if (info.data.destPort === 0) { info.data.destPort = self._forwarding[info.data.destIP + ':' + info.data.destPort]; } self.emit('tcp connection', info.data, accept, reject); } else if (info.type === 'x11') { self.emit('x11', info.data, accept, reject); } else if (info.type === 'forwarded-streamlocal@openssh.com') { self.emit('unix connection', info.data, accept, reject); } else { agentQuery(self.config.agent, accept, reject); } } } else { // automatically reject any unsupported channel open requests self.config.debug('DEBUG: Client: Automatic rejection of incoming channel open: unsupported type: ' + info.type); reason = consts.CHANNEL_OPEN_FAILURE.UNKNOWN_CHANNEL_TYPE; reject(); } } var randomCookie = (function() { if (typeof crypto.randomFillSync === 'function') { var buffer = Buffer.alloc(16); return function randomCookie() { crypto.randomFillSync(buffer, 0, 16); return buffer.toString('hex'); }; } else { return function randomCookie() { return crypto.randomBytes(16).toString('hex'); }; } })(); Client.Client = Client; Client.Server = require('./server'); // pass some useful utilities on to end user (e.g. parseKey()) Client.utils = ssh2_streams.utils; // expose useful SFTPStream constants for sftp server usage Client.SFTP_STATUS_CODE = SFTPStream.STATUS_CODE; Client.SFTP_OPEN_MODE = SFTPStream.OPEN_MODE; module.exports = Client; // backwards compatibility