123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773177417751776177717781779178017811782178317841785178617871788178917901791179217931794179517961797179817991800180118021803180418051806180718081809181018111812181318141815181618171818181918201821182218231824182518261827182818291830183118321833183418351836183718381839184018411842184318441845184618471848184918501851185218531854185518561857185818591860186118621863186418651866186718681869187018711872187318741875187618771878187918801881188218831884188518861887188818891890189118921893189418951896189718981899190019011902190319041905190619071908190919101911191219131914191519161917191819191920192119221923192419251926192719281929193019311932193319341935193619371938193919401941194219431944194519461947194819491950195119521953195419551956195719581959196019611962196319641965196619671968196919701971197219731974197519761977197819791980198119821983198419851986198719881989199019911992199319941995199619971998199920002001200220032004200520062007200820092010201120122013 |
- // 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 hasher;
- if (HASHES.indexOf(cfg.hostHash) !== -1) {
- // Default to old behavior of hashing on user's behalf
- hasher = createHash(cfg.hostHash);
- }
- hostVerifier = (key, verify) => {
- if (hasher) {
- hasher.update(key);
- key = hasher.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)}`);
- });
- 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();
- },
- 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) => {
- 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) => {
- if (curAuth.type === 'agent') {
- const key = curAuth.agentCtx.currentKey();
- proto.authPK(curAuth.username, key, (buf, cb) => {
- curAuth.agentCtx.sign(key, buf, {}, (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, (buf, cb) => {
- const signature = curAuth.key.sign(buf);
- 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(() => {
- 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':
- proto.authPK(username, curAuth.key);
- break;
- case 'hostbased':
- proto.authHostbased(username,
- curAuth.key,
- curAuth.localHostname,
- curAuth.localUsername,
- (buf, cb) => {
- const signature = curAuth.key.sign(buf);
- 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();
- debug && debug(`Agent: Trying key #${pos + 1}`);
- proto.authPK(curAuth.username, key);
- }
- }
- };
- 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.setNoDelay(true);
- 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(cb) {
- if (!this._sock || !isWritable(this._sock))
- throw new Error('Not connected');
- openChannel(this, 'sftp', (err, sftp) => {
- if (err) {
- cb(err);
- return;
- }
- reqSubsystem(sftp, 'sftp', (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();
- });
- });
- return this;
- }
- }
- 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
- const initWindow = MAX_WINDOW;
- const maxPacket = PACKET_SIZE;
- if (typeof opts === 'function') {
- cb = opts;
- opts = {};
- }
- const wrapper = (err, stream) => {
- cb(err, stream);
- };
- wrapper.type = type;
- const localChan = self._chanMgr.add(wrapper);
- if (localChan === -1) {
- cb(new Error('No free channels available'));
- return;
- }
- switch (type) {
- case 'session':
- case 'sftp':
- self._protocol.session(localChan, initWindow, maxPacket);
- break;
- case 'direct-tcpip':
- self._protocol.directTcpip(localChan, initWindow, maxPacket, opts);
- break;
- case 'direct-streamlocal@openssh.com':
- self._protocol.openssh_directStreamLocal(
- localChan, initWindow, maxPacket, opts
- );
- break;
- default:
- throw new Error(`Unsupported channel type: ${type}`);
- }
- }
- function reqX11(chan, screen, cb) {
- // Asks server to start sending us X11 connections
- const 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.hexSlice(0, screen.cookie.length);
- }
- if (cfg.cookie === undefined)
- cfg.cookie = randomCookie();
- const wantReply = (typeof cb === 'function');
- if (chan.outgoing.state !== 'open') {
- if (wantReply)
- cb(new Error('Channel is not open'));
- return;
- }
- if (wantReply) {
- chan._callbacks.push((had_err) => {
- if (had_err) {
- cb(had_err !== true ? had_err : new Error('Unable to request X11'));
- return;
- }
- chan._hasX11 = true;
- ++chan._client._acceptX11;
- chan.once('close', () => {
- if (chan._client._acceptX11)
- --chan._client._acceptX11;
- });
- cb();
- });
- }
- chan._client._protocol.x11Forward(chan.outgoing.id, cfg, wantReply);
- }
- function reqPty(chan, opts, cb) {
- let rows = 24;
- let cols = 80;
- let width = 640;
- let height = 480;
- let term = 'vt100';
- let modes = null;
- 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;
- if (typeof opts.modes === 'object')
- modes = opts.modes;
- }
- const wantReply = (typeof cb === 'function');
- if (chan.outgoing.state !== 'open') {
- if (wantReply)
- cb(new Error('Channel is not open'));
- return;
- }
- if (wantReply) {
- chan._callbacks.push((had_err) => {
- if (had_err) {
- cb(had_err !== true
- ? had_err
- : new Error('Unable to request a pseudo-terminal'));
- return;
- }
- cb();
- });
- }
- chan._client._protocol.pty(chan.outgoing.id,
- rows,
- cols,
- height,
- width,
- term,
- modes,
- wantReply);
- }
- function reqAgentFwd(chan, cb) {
- const wantReply = (typeof cb === 'function');
- if (chan.outgoing.state !== 'open') {
- wantReply && cb(new Error('Channel is not open'));
- return;
- }
- if (chan._client._agentFwdEnabled) {
- wantReply && cb(false);
- return;
- }
- chan._client._agentFwdEnabled = true;
- chan._callbacks.push((had_err) => {
- if (had_err) {
- chan._client._agentFwdEnabled = false;
- if (wantReply) {
- cb(had_err !== true
- ? had_err
- : new Error('Unable to request agent forwarding'));
- }
- return;
- }
- if (wantReply)
- cb();
- });
- chan._client._protocol.openssh_agentForward(chan.outgoing.id, true);
- }
- function reqShell(chan, cb) {
- if (chan.outgoing.state !== 'open') {
- cb(new Error('Channel is not open'));
- return;
- }
- chan._callbacks.push((had_err) => {
- if (had_err) {
- cb(had_err !== true ? had_err : new Error('Unable to open shell'));
- return;
- }
- chan.subtype = 'shell';
- cb(undefined, chan);
- });
- chan._client._protocol.shell(chan.outgoing.id, true);
- }
- function reqExec(chan, cmd, opts, cb) {
- if (chan.outgoing.state !== 'open') {
- cb(new Error('Channel is not open'));
- return;
- }
- chan._callbacks.push((had_err) => {
- if (had_err) {
- cb(had_err !== true ? had_err : new Error('Unable to exec'));
- return;
- }
- chan.subtype = 'exec';
- chan.allowHalfOpen = (opts.allowHalfOpen !== false);
- cb(undefined, chan);
- });
- chan._client._protocol.exec(chan.outgoing.id, cmd, true);
- }
- function reqEnv(chan, env) {
- if (chan.outgoing.state !== 'open')
- return;
- const keys = Object.keys(env || {});
- for (let i = 0; i < keys.length; ++i) {
- const key = keys[i];
- const val = env[key];
- chan._client._protocol.env(chan.outgoing.id, key, val, false);
- }
- }
- function reqSubsystem(chan, name, cb) {
- if (chan.outgoing.state !== 'open') {
- cb(new Error('Channel is not open'));
- return;
- }
- chan._callbacks.push((had_err) => {
- if (had_err) {
- cb(had_err !== true
- ? had_err
- : new Error(`Unable to start subsystem: ${name}`));
- return;
- }
- chan.subtype = 'subsystem';
- cb(undefined, chan);
- });
- chan._client._protocol.subsystem(chan.outgoing.id, name, true);
- }
- // TODO: inline implementation into single call site
- 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
- let localChan = -1;
- let reason;
- const accept = () => {
- const chanInfo = {
- type: info.type,
- incoming: {
- id: localChan,
- window: MAX_WINDOW,
- packetSize: PACKET_SIZE,
- state: 'open'
- },
- outgoing: {
- id: info.sender,
- window: info.window,
- packetSize: info.packetSize,
- state: 'open'
- }
- };
- const stream = new Channel(self, chanInfo);
- self._chanMgr.update(localChan, stream);
- self._protocol.channelOpenConfirm(info.sender,
- localChan,
- MAX_WINDOW,
- PACKET_SIZE);
- return stream;
- };
- const reject = () => {
- if (reason === undefined) {
- if (localChan === -1)
- reason = CHANNEL_OPEN_FAILURE.RESOURCE_SHORTAGE;
- else
- reason = CHANNEL_OPEN_FAILURE.CONNECT_FAILED;
- }
- if (localChan !== -1)
- self._chanMgr.remove(localChan);
- self._protocol.channelOpenFail(info.sender, reason, '');
- };
- const reserveChannel = () => {
- localChan = self._chanMgr.add();
- if (localChan === -1) {
- reason = CHANNEL_OPEN_FAILURE.RESOURCE_SHORTAGE;
- if (self.config.debug) {
- self.config.debug(
- 'Client: Automatic rejection of incoming channel open: '
- + 'no channels available'
- );
- }
- }
- return (localChan !== -1);
- };
- const data = info.data;
- switch (info.type) {
- case 'forwarded-tcpip': {
- const val = self._forwarding[`${data.destIP}:${data.destPort}`];
- if (val !== undefined && reserveChannel()) {
- if (data.destPort === 0)
- data.destPort = val;
- self.emit('tcp connection', data, accept, reject);
- return;
- }
- break;
- }
- case 'forwarded-streamlocal@openssh.com':
- if (self._forwardingUnix[data.socketPath] !== undefined
- && reserveChannel()) {
- self.emit('unix connection', data, accept, reject);
- return;
- }
- break;
- case 'auth-agent@openssh.com':
- if (self._agentFwdEnabled
- && typeof self._agent.getStream === 'function'
- && reserveChannel()) {
- self._agent.getStream((err, stream) => {
- if (err)
- return reject();
- const upstream = accept();
- upstream.pipe(stream).pipe(upstream);
- });
- return;
- }
- break;
- case 'x11':
- if (self._acceptX11 !== 0 && reserveChannel()) {
- self.emit('x11', data, accept, reject);
- return;
- }
- break;
- default:
- // Automatically reject any unsupported channel open requests
- reason = CHANNEL_OPEN_FAILURE.UNKNOWN_CHANNEL_TYPE;
- if (self.config.debug) {
- self.config.debug(
- 'Client: Automatic rejection of unsupported incoming channel open '
- + `type: ${info.type}`
- );
- }
- }
- if (reason === undefined) {
- reason = CHANNEL_OPEN_FAILURE.ADMINISTRATIVELY_PROHIBITED;
- if (self.config.debug) {
- self.config.debug(
- 'Client: Automatic rejection of unexpected incoming channel open for: '
- + info.type
- );
- }
- }
- reject();
- }
- const randomCookie = (() => {
- const buffer = Buffer.allocUnsafe(16);
- return () => {
- randomFillSync(buffer, 0, 16);
- return buffer.hexSlice(0, 16);
- };
- })();
- function makeSimpleAuthHandler(authList) {
- if (!Array.isArray(authList))
- throw new Error('authList must be an array');
- let a = 0;
- return (authsLeft, partialSuccess, cb) => {
- if (a === authList.length)
- return false;
- return authList[a++];
- };
- }
- function hostKeysProve(client, keys_, cb) {
- if (!client._sock || !isWritable(client._sock))
- return;
- if (typeof cb !== 'function')
- cb = noop;
- if (!Array.isArray(keys_))
- throw new TypeError('Invalid keys argument type');
- const keys = [];
- for (const key of keys_) {
- const parsed = parseKey(key);
- if (parsed instanceof Error)
- throw parsed;
- keys.push(parsed);
- }
- if (!client.config.strictVendor
- || (client.config.strictVendor && RE_OPENSSH.test(client._remoteVer))) {
- client._callbacks.push((had_err, data) => {
- if (had_err) {
- cb(had_err !== true
- ? had_err
- : new Error('Server failed to prove supplied keys'));
- return;
- }
- // TODO: move all of this parsing/verifying logic out of the client?
- const ret = [];
- let keyIdx = 0;
- bufferParser.init(data, 0);
- while (bufferParser.avail()) {
- if (keyIdx === keys.length)
- break;
- const key = keys[keyIdx++];
- const keyPublic = key.getPublicSSH();
- const sigEntry = bufferParser.readString();
- sigParser.init(sigEntry, 0);
- const type = sigParser.readString(true);
- let value = sigParser.readString();
- let algo;
- if (type !== key.type) {
- if (key.type === 'ssh-rsa') {
- switch (type) {
- case 'rsa-sha2-256':
- algo = 'sha256';
- break;
- case 'rsa-sha2-512':
- algo = 'sha512';
- break;
- default:
- continue;
- }
- } else {
- continue;
- }
- }
- const sessionID = client._protocol._kex.sessionID;
- const verifyData = Buffer.allocUnsafe(
- 4 + 29 + 4 + sessionID.length + 4 + keyPublic.length
- );
- let p = 0;
- writeUInt32BE(verifyData, 29, p);
- verifyData.utf8Write('hostkeys-prove-00@openssh.com', p += 4, 29);
- writeUInt32BE(verifyData, sessionID.length, p += 29);
- bufferCopy(sessionID, verifyData, 0, sessionID.length, p += 4);
- writeUInt32BE(verifyData, keyPublic.length, p += sessionID.length);
- bufferCopy(keyPublic, verifyData, 0, keyPublic.length, p += 4);
- if (!(value = sigSSHToASN1(value, type)))
- continue;
- if (key.verify(verifyData, value, algo) === true)
- ret.push(key);
- }
- sigParser.clear();
- bufferParser.clear();
- cb(null, ret);
- });
- client._protocol.openssh_hostKeysProve(keys);
- return;
- }
- process.nextTick(
- cb,
- new Error(
- 'strictVendor enabled and server is not OpenSSH or compatible version'
- )
- );
- }
- module.exports = Client;
|