2137 lines
64 KiB
JavaScript
2137 lines
64 KiB
JavaScript
/*
|
|
TODO:
|
|
* Replace `buffer._pos` usage in keyParser.js and elsewhere
|
|
* Utilize optional "writev" support when writing packets from
|
|
cipher.encrypt()
|
|
* Built-in support for automatic re-keying, on by default
|
|
* Revisit receiving unexpected/unknown packets
|
|
* Error (fatal or otherwise) or ignore or pass on to user (in some or all
|
|
cases)?
|
|
* Including server/client check for single directional packet types?
|
|
* Check packets for validity or bail as early as possible?
|
|
* Automatic re-key every 2**31 packets after the last key exchange (sent or
|
|
received), as suggested by RFC4344. OpenSSH currently does this.
|
|
* Automatic re-key every so many blocks depending on cipher. RFC4344:
|
|
Because of a birthday property of block ciphers and some modes of
|
|
operation, implementations must be careful not to encrypt too many
|
|
blocks with the same encryption key.
|
|
|
|
Let L be the block length (in bits) of an SSH encryption method's
|
|
block cipher (e.g., 128 for AES). If L is at least 128, then, after
|
|
rekeying, an SSH implementation SHOULD NOT encrypt more than 2**(L/4)
|
|
blocks before rekeying again. If L is at least 128, then SSH
|
|
implementations should also attempt to force a rekey before receiving
|
|
more than 2**(L/4) blocks. If L is less than 128 (which is the case
|
|
for older ciphers such as 3DES, Blowfish, CAST-128, and IDEA), then,
|
|
although it may be too expensive to rekey every 2**(L/4) blocks, it
|
|
is still advisable for SSH implementations to follow the original
|
|
recommendation in [RFC4253]: rekey at least once for every gigabyte
|
|
of transmitted data.
|
|
|
|
Note that if L is less than or equal to 128, then the recommendation
|
|
in this subsection supersedes the recommendation in Section 3.1. If
|
|
an SSH implementation uses a block cipher with a larger block size
|
|
(e.g., Rijndael with 256-bit blocks), then the recommendations in
|
|
Section 3.1 may supersede the recommendations in this subsection
|
|
(depending on the lengths of the packets).
|
|
*/
|
|
|
|
'use strict';
|
|
|
|
const { inspect } = require('util');
|
|
|
|
const { bindingAvailable, NullCipher, NullDecipher } = require('./crypto.js');
|
|
const {
|
|
COMPAT_CHECKS,
|
|
DISCONNECT_REASON,
|
|
eddsaSupported,
|
|
MESSAGE,
|
|
SIGNALS,
|
|
TERMINAL_MODE,
|
|
} = require('./constants.js');
|
|
const {
|
|
DEFAULT_KEXINIT_CLIENT,
|
|
DEFAULT_KEXINIT_SERVER,
|
|
KexInit,
|
|
kexinit,
|
|
onKEXPayload,
|
|
} = require('./kex.js');
|
|
const {
|
|
parseKey,
|
|
} = require('./keyParser.js');
|
|
const MESSAGE_HANDLERS = require('./handlers.js');
|
|
const {
|
|
bufferCopy,
|
|
bufferFill,
|
|
bufferSlice,
|
|
convertSignature,
|
|
sendPacket,
|
|
writeUInt32BE,
|
|
} = require('./utils.js');
|
|
const {
|
|
PacketReader,
|
|
PacketWriter,
|
|
ZlibPacketReader,
|
|
ZlibPacketWriter,
|
|
} = require('./zlib.js');
|
|
|
|
const MODULE_VER = require('../../package.json').version;
|
|
|
|
const VALID_DISCONNECT_REASONS = new Map(
|
|
Object.values(DISCONNECT_REASON).map((n) => [n, 1])
|
|
);
|
|
const IDENT_RAW = Buffer.from(`SSH-2.0-ssh2js${MODULE_VER}`);
|
|
const IDENT = Buffer.from(`${IDENT_RAW}\r\n`);
|
|
const MAX_LINE_LEN = 8192;
|
|
const MAX_LINES = 1024;
|
|
const PING_PAYLOAD = Buffer.from([
|
|
MESSAGE.GLOBAL_REQUEST,
|
|
// "keepalive@openssh.com"
|
|
0, 0, 0, 21,
|
|
107, 101, 101, 112, 97, 108, 105, 118, 101, 64, 111, 112, 101, 110, 115,
|
|
115, 104, 46, 99, 111, 109,
|
|
// Request a reply
|
|
1,
|
|
]);
|
|
const NO_TERMINAL_MODES_BUFFER = Buffer.from([ TERMINAL_MODE.TTY_OP_END ]);
|
|
|
|
function noop() {}
|
|
|
|
/*
|
|
Inbound:
|
|
* kexinit payload (needed only until exchange hash is generated)
|
|
* raw ident
|
|
* rekey packet queue
|
|
* expected packet (implemented as separate _parse() function?)
|
|
Outbound:
|
|
* kexinit payload (needed only until exchange hash is generated)
|
|
* rekey packet queue
|
|
* kex secret (needed only until NEWKEYS)
|
|
* exchange hash (needed only until NEWKEYS)
|
|
* session ID (set to exchange hash from initial handshake)
|
|
*/
|
|
class Protocol {
|
|
constructor(config) {
|
|
const onWrite = config.onWrite;
|
|
if (typeof onWrite !== 'function')
|
|
throw new Error('Missing onWrite function');
|
|
this._onWrite = (data) => { onWrite(data); };
|
|
|
|
const onError = config.onError;
|
|
if (typeof onError !== 'function')
|
|
throw new Error('Missing onError function');
|
|
this._onError = (err) => { onError(err); };
|
|
|
|
const debug = config.debug;
|
|
this._debug = (typeof debug === 'function'
|
|
? (msg) => { debug(msg); }
|
|
: undefined);
|
|
|
|
const onHeader = config.onHeader;
|
|
this._onHeader = (typeof onHeader === 'function'
|
|
? (...args) => { onHeader(...args); }
|
|
: noop);
|
|
|
|
const onPacket = config.onPacket;
|
|
this._onPacket = (typeof onPacket === 'function'
|
|
? () => { onPacket(); }
|
|
: noop);
|
|
|
|
let onHandshakeComplete = config.onHandshakeComplete;
|
|
if (typeof onHandshakeComplete !== 'function')
|
|
onHandshakeComplete = noop;
|
|
let firstHandshake;
|
|
this._onHandshakeComplete = (...args) => {
|
|
this._debug && this._debug('Handshake completed');
|
|
if (firstHandshake === undefined)
|
|
firstHandshake = true;
|
|
else
|
|
firstHandshake = false;
|
|
|
|
// Process packets queued during a rekey where necessary
|
|
const oldQueue = this._queue;
|
|
if (oldQueue) {
|
|
this._queue = undefined;
|
|
this._debug && this._debug(
|
|
`Draining outbound queue (${oldQueue.length}) ...`
|
|
);
|
|
for (let i = 0; i < oldQueue.length; ++i) {
|
|
const data = oldQueue[i];
|
|
// data === payload only
|
|
|
|
// XXX: hacky
|
|
let finalized = this._packetRW.write.finalize(data);
|
|
if (finalized === data) {
|
|
const packet = this._cipher.allocPacket(data.length);
|
|
packet.set(data, 5);
|
|
finalized = packet;
|
|
}
|
|
|
|
sendPacket(this, finalized);
|
|
}
|
|
this._debug && this._debug('... finished draining outbound queue');
|
|
}
|
|
|
|
if (firstHandshake && this._server && this._kex.remoteExtInfoEnabled)
|
|
sendExtInfo(this);
|
|
|
|
onHandshakeComplete(...args);
|
|
};
|
|
this._queue = undefined;
|
|
|
|
const messageHandlers = config.messageHandlers;
|
|
if (typeof messageHandlers === 'object' && messageHandlers !== null)
|
|
this._handlers = messageHandlers;
|
|
else
|
|
this._handlers = {};
|
|
|
|
this._onPayload = onPayload.bind(this);
|
|
|
|
this._server = !!config.server;
|
|
this._banner = undefined;
|
|
let greeting;
|
|
if (this._server) {
|
|
if (typeof config.hostKeys !== 'object' || config.hostKeys === null)
|
|
throw new Error('Missing server host key(s)');
|
|
this._hostKeys = config.hostKeys;
|
|
|
|
// Greeting displayed before the ssh identification string is sent, this
|
|
// is usually ignored by most clients
|
|
if (typeof config.greeting === 'string' && config.greeting.length) {
|
|
greeting = (config.greeting.slice(-2) === '\r\n'
|
|
? config.greeting
|
|
: `${config.greeting}\r\n`);
|
|
}
|
|
|
|
// Banner shown after the handshake completes, but before user
|
|
// authentication begins
|
|
if (typeof config.banner === 'string' && config.banner.length) {
|
|
this._banner = (config.banner.slice(-2) === '\r\n'
|
|
? config.banner
|
|
: `${config.banner}\r\n`);
|
|
}
|
|
} else {
|
|
this._hostKeys = undefined;
|
|
}
|
|
|
|
let offer = config.offer;
|
|
if (typeof offer !== 'object' || offer === null) {
|
|
offer = (this._server ? DEFAULT_KEXINIT_SERVER : DEFAULT_KEXINIT_CLIENT);
|
|
} else if (offer.constructor !== KexInit) {
|
|
if (this._server) {
|
|
offer.kex = offer.kex.concat(['kex-strict-s-v00@openssh.com']);
|
|
} else {
|
|
offer.kex = offer.kex.concat([
|
|
'ext-info-c',
|
|
'kex-strict-c-v00@openssh.com',
|
|
]);
|
|
}
|
|
offer = new KexInit(offer);
|
|
}
|
|
this._kex = undefined;
|
|
this._strictMode = undefined;
|
|
this._kexinit = undefined;
|
|
this._offer = offer;
|
|
this._cipher = new NullCipher(0, this._onWrite);
|
|
this._decipher = undefined;
|
|
this._skipNextInboundPacket = false;
|
|
this._packetRW = {
|
|
read: new PacketReader(),
|
|
write: new PacketWriter(this),
|
|
};
|
|
this._hostVerifier = (!this._server
|
|
&& typeof config.hostVerifier === 'function'
|
|
? config.hostVerifier
|
|
: undefined);
|
|
|
|
this._parse = parseHeader;
|
|
this._buffer = undefined;
|
|
this._authsQueue = [];
|
|
this._authenticated = false;
|
|
this._remoteIdentRaw = undefined;
|
|
let sentIdent;
|
|
if (typeof config.ident === 'string') {
|
|
this._identRaw = Buffer.from(`SSH-2.0-${config.ident}`);
|
|
|
|
sentIdent = Buffer.allocUnsafe(this._identRaw.length + 2);
|
|
sentIdent.set(this._identRaw, 0);
|
|
sentIdent[sentIdent.length - 2] = 13; // '\r'
|
|
sentIdent[sentIdent.length - 1] = 10; // '\n'
|
|
} else if (Buffer.isBuffer(config.ident)) {
|
|
const fullIdent = Buffer.allocUnsafe(8 + config.ident.length);
|
|
fullIdent.latin1Write('SSH-2.0-', 0, 8);
|
|
fullIdent.set(config.ident, 8);
|
|
this._identRaw = fullIdent;
|
|
|
|
sentIdent = Buffer.allocUnsafe(fullIdent.length + 2);
|
|
sentIdent.set(fullIdent, 0);
|
|
sentIdent[sentIdent.length - 2] = 13; // '\r'
|
|
sentIdent[sentIdent.length - 1] = 10; // '\n'
|
|
} else {
|
|
this._identRaw = IDENT_RAW;
|
|
sentIdent = IDENT;
|
|
}
|
|
this._compatFlags = 0;
|
|
|
|
if (this._debug) {
|
|
if (bindingAvailable)
|
|
this._debug('Custom crypto binding available');
|
|
else
|
|
this._debug('Custom crypto binding not available');
|
|
}
|
|
|
|
this._debug && this._debug(
|
|
`Local ident: ${inspect(this._identRaw.toString())}`
|
|
);
|
|
this.start = () => {
|
|
this.start = undefined;
|
|
if (greeting)
|
|
this._onWrite(greeting);
|
|
this._onWrite(sentIdent);
|
|
};
|
|
}
|
|
_destruct(reason) {
|
|
this._packetRW.read.cleanup();
|
|
this._packetRW.write.cleanup();
|
|
this._cipher && this._cipher.free();
|
|
this._decipher && this._decipher.free();
|
|
if (typeof reason !== 'string' || reason.length === 0)
|
|
reason = 'fatal error';
|
|
this.parse = () => {
|
|
throw new Error(`Instance unusable after ${reason}`);
|
|
};
|
|
this._onWrite = () => {
|
|
throw new Error(`Instance unusable after ${reason}`);
|
|
};
|
|
this._destruct = undefined;
|
|
}
|
|
cleanup() {
|
|
this._destruct && this._destruct();
|
|
}
|
|
parse(chunk, i, len) {
|
|
while (i < len)
|
|
i = this._parse(chunk, i, len);
|
|
}
|
|
|
|
// Protocol message API
|
|
|
|
// ===========================================================================
|
|
// Common/Shared =============================================================
|
|
// ===========================================================================
|
|
|
|
// Global
|
|
// ------
|
|
disconnect(reason) {
|
|
const pktLen = 1 + 4 + 4 + 4;
|
|
// We don't use _packetRW.write.* here because we need to make sure that
|
|
// we always get a full packet allocated because this message can be sent
|
|
// at any time -- even during a key exchange
|
|
let p = this._packetRW.write.allocStartKEX;
|
|
const packet = this._packetRW.write.alloc(pktLen, true);
|
|
const end = p + pktLen;
|
|
|
|
if (!VALID_DISCONNECT_REASONS.has(reason))
|
|
reason = DISCONNECT_REASON.PROTOCOL_ERROR;
|
|
|
|
packet[p] = MESSAGE.DISCONNECT;
|
|
writeUInt32BE(packet, reason, ++p);
|
|
packet.fill(0, p += 4, end);
|
|
|
|
this._debug && this._debug(`Outbound: Sending DISCONNECT (${reason})`);
|
|
sendPacket(this, this._packetRW.write.finalize(packet, true), true);
|
|
}
|
|
ping() {
|
|
const p = this._packetRW.write.allocStart;
|
|
const packet = this._packetRW.write.alloc(PING_PAYLOAD.length);
|
|
|
|
packet.set(PING_PAYLOAD, p);
|
|
|
|
this._debug && this._debug(
|
|
'Outbound: Sending ping (GLOBAL_REQUEST: keepalive@openssh.com)'
|
|
);
|
|
sendPacket(this, this._packetRW.write.finalize(packet));
|
|
}
|
|
rekey() {
|
|
if (this._kexinit === undefined) {
|
|
this._debug && this._debug('Outbound: Initiated explicit rekey');
|
|
this._queue = [];
|
|
kexinit(this);
|
|
} else {
|
|
this._debug && this._debug('Outbound: Ignoring rekey during handshake');
|
|
}
|
|
}
|
|
|
|
// 'ssh-connection' service-specific
|
|
// ---------------------------------
|
|
requestSuccess(data) {
|
|
let p = this._packetRW.write.allocStart;
|
|
let packet;
|
|
if (Buffer.isBuffer(data)) {
|
|
packet = this._packetRW.write.alloc(1 + data.length);
|
|
|
|
packet[p] = MESSAGE.REQUEST_SUCCESS;
|
|
|
|
packet.set(data, ++p);
|
|
} else {
|
|
packet = this._packetRW.write.alloc(1);
|
|
|
|
packet[p] = MESSAGE.REQUEST_SUCCESS;
|
|
}
|
|
|
|
this._debug && this._debug('Outbound: Sending REQUEST_SUCCESS');
|
|
sendPacket(this, this._packetRW.write.finalize(packet));
|
|
}
|
|
requestFailure() {
|
|
const p = this._packetRW.write.allocStart;
|
|
const packet = this._packetRW.write.alloc(1);
|
|
|
|
packet[p] = MESSAGE.REQUEST_FAILURE;
|
|
|
|
this._debug && this._debug('Outbound: Sending REQUEST_FAILURE');
|
|
sendPacket(this, this._packetRW.write.finalize(packet));
|
|
}
|
|
channelSuccess(chan) {
|
|
// Does not consume window space
|
|
|
|
let p = this._packetRW.write.allocStart;
|
|
const packet = this._packetRW.write.alloc(1 + 4);
|
|
|
|
packet[p] = MESSAGE.CHANNEL_SUCCESS;
|
|
|
|
writeUInt32BE(packet, chan, ++p);
|
|
|
|
this._debug && this._debug(`Outbound: Sending CHANNEL_SUCCESS (r:${chan})`);
|
|
sendPacket(this, this._packetRW.write.finalize(packet));
|
|
}
|
|
channelFailure(chan) {
|
|
// Does not consume window space
|
|
|
|
let p = this._packetRW.write.allocStart;
|
|
const packet = this._packetRW.write.alloc(1 + 4);
|
|
|
|
packet[p] = MESSAGE.CHANNEL_FAILURE;
|
|
|
|
writeUInt32BE(packet, chan, ++p);
|
|
|
|
this._debug && this._debug(`Outbound: Sending CHANNEL_FAILURE (r:${chan})`);
|
|
sendPacket(this, this._packetRW.write.finalize(packet));
|
|
}
|
|
channelEOF(chan) {
|
|
// Does not consume window space
|
|
|
|
let p = this._packetRW.write.allocStart;
|
|
const packet = this._packetRW.write.alloc(1 + 4);
|
|
|
|
packet[p] = MESSAGE.CHANNEL_EOF;
|
|
|
|
writeUInt32BE(packet, chan, ++p);
|
|
|
|
this._debug && this._debug(`Outbound: Sending CHANNEL_EOF (r:${chan})`);
|
|
sendPacket(this, this._packetRW.write.finalize(packet));
|
|
}
|
|
channelClose(chan) {
|
|
// Does not consume window space
|
|
|
|
let p = this._packetRW.write.allocStart;
|
|
const packet = this._packetRW.write.alloc(1 + 4);
|
|
|
|
packet[p] = MESSAGE.CHANNEL_CLOSE;
|
|
|
|
writeUInt32BE(packet, chan, ++p);
|
|
|
|
this._debug && this._debug(`Outbound: Sending CHANNEL_CLOSE (r:${chan})`);
|
|
sendPacket(this, this._packetRW.write.finalize(packet));
|
|
}
|
|
channelWindowAdjust(chan, amount) {
|
|
// Does not consume window space
|
|
|
|
let p = this._packetRW.write.allocStart;
|
|
const packet = this._packetRW.write.alloc(1 + 4 + 4);
|
|
|
|
packet[p] = MESSAGE.CHANNEL_WINDOW_ADJUST;
|
|
|
|
writeUInt32BE(packet, chan, ++p);
|
|
|
|
writeUInt32BE(packet, amount, p += 4);
|
|
|
|
this._debug && this._debug(
|
|
`Outbound: Sending CHANNEL_WINDOW_ADJUST (r:${chan}, ${amount})`
|
|
);
|
|
sendPacket(this, this._packetRW.write.finalize(packet));
|
|
}
|
|
channelData(chan, data) {
|
|
const isBuffer = Buffer.isBuffer(data);
|
|
const dataLen = (isBuffer ? data.length : Buffer.byteLength(data));
|
|
let p = this._packetRW.write.allocStart;
|
|
const packet = this._packetRW.write.alloc(1 + 4 + 4 + dataLen);
|
|
|
|
packet[p] = MESSAGE.CHANNEL_DATA;
|
|
|
|
writeUInt32BE(packet, chan, ++p);
|
|
|
|
writeUInt32BE(packet, dataLen, p += 4);
|
|
|
|
if (isBuffer)
|
|
packet.set(data, p += 4);
|
|
else
|
|
packet.utf8Write(data, p += 4, dataLen);
|
|
|
|
this._debug && this._debug(
|
|
`Outbound: Sending CHANNEL_DATA (r:${chan}, ${dataLen})`
|
|
);
|
|
sendPacket(this, this._packetRW.write.finalize(packet));
|
|
}
|
|
channelExtData(chan, data, type) {
|
|
const isBuffer = Buffer.isBuffer(data);
|
|
const dataLen = (isBuffer ? data.length : Buffer.byteLength(data));
|
|
let p = this._packetRW.write.allocStart;
|
|
const packet = this._packetRW.write.alloc(1 + 4 + 4 + 4 + dataLen);
|
|
|
|
packet[p] = MESSAGE.CHANNEL_EXTENDED_DATA;
|
|
|
|
writeUInt32BE(packet, chan, ++p);
|
|
|
|
writeUInt32BE(packet, type, p += 4);
|
|
|
|
writeUInt32BE(packet, dataLen, p += 4);
|
|
|
|
if (isBuffer)
|
|
packet.set(data, p += 4);
|
|
else
|
|
packet.utf8Write(data, p += 4, dataLen);
|
|
|
|
this._debug
|
|
&& this._debug(`Outbound: Sending CHANNEL_EXTENDED_DATA (r:${chan})`);
|
|
sendPacket(this, this._packetRW.write.finalize(packet));
|
|
}
|
|
channelOpenConfirm(remote, local, initWindow, maxPacket) {
|
|
let p = this._packetRW.write.allocStart;
|
|
const packet = this._packetRW.write.alloc(1 + 4 + 4 + 4 + 4);
|
|
|
|
packet[p] = MESSAGE.CHANNEL_OPEN_CONFIRMATION;
|
|
|
|
writeUInt32BE(packet, remote, ++p);
|
|
|
|
writeUInt32BE(packet, local, p += 4);
|
|
|
|
writeUInt32BE(packet, initWindow, p += 4);
|
|
|
|
writeUInt32BE(packet, maxPacket, p += 4);
|
|
|
|
this._debug && this._debug(
|
|
`Outbound: Sending CHANNEL_OPEN_CONFIRMATION (r:${remote}, l:${local})`
|
|
);
|
|
sendPacket(this, this._packetRW.write.finalize(packet));
|
|
}
|
|
channelOpenFail(remote, reason, desc) {
|
|
if (typeof desc !== 'string')
|
|
desc = '';
|
|
|
|
const descLen = Buffer.byteLength(desc);
|
|
let p = this._packetRW.write.allocStart;
|
|
const packet = this._packetRW.write.alloc(1 + 4 + 4 + 4 + descLen + 4);
|
|
|
|
packet[p] = MESSAGE.CHANNEL_OPEN_FAILURE;
|
|
|
|
writeUInt32BE(packet, remote, ++p);
|
|
|
|
writeUInt32BE(packet, reason, p += 4);
|
|
|
|
writeUInt32BE(packet, descLen, p += 4);
|
|
|
|
p += 4;
|
|
if (descLen) {
|
|
packet.utf8Write(desc, p, descLen);
|
|
p += descLen;
|
|
}
|
|
|
|
writeUInt32BE(packet, 0, p); // Empty language tag
|
|
|
|
this._debug
|
|
&& this._debug(`Outbound: Sending CHANNEL_OPEN_FAILURE (r:${remote})`);
|
|
sendPacket(this, this._packetRW.write.finalize(packet));
|
|
}
|
|
|
|
// ===========================================================================
|
|
// Client-specific ===========================================================
|
|
// ===========================================================================
|
|
|
|
// Global
|
|
// ------
|
|
service(name) {
|
|
if (this._server)
|
|
throw new Error('Client-only method called in server mode');
|
|
|
|
const nameLen = Buffer.byteLength(name);
|
|
let p = this._packetRW.write.allocStart;
|
|
const packet = this._packetRW.write.alloc(1 + 4 + nameLen);
|
|
|
|
packet[p] = MESSAGE.SERVICE_REQUEST;
|
|
|
|
writeUInt32BE(packet, nameLen, ++p);
|
|
packet.utf8Write(name, p += 4, nameLen);
|
|
|
|
this._debug && this._debug(`Outbound: Sending SERVICE_REQUEST (${name})`);
|
|
sendPacket(this, this._packetRW.write.finalize(packet));
|
|
}
|
|
|
|
// 'ssh-userauth' service-specific
|
|
// -------------------------------
|
|
authPassword(username, password, newPassword) {
|
|
if (this._server)
|
|
throw new Error('Client-only method called in server mode');
|
|
|
|
const userLen = Buffer.byteLength(username);
|
|
const passLen = Buffer.byteLength(password);
|
|
const newPassLen = (newPassword ? Buffer.byteLength(newPassword) : 0);
|
|
let p = this._packetRW.write.allocStart;
|
|
const packet = this._packetRW.write.alloc(
|
|
1 + 4 + userLen + 4 + 14 + 4 + 8 + 1 + 4 + passLen
|
|
+ (newPassword ? 4 + newPassLen : 0)
|
|
);
|
|
|
|
packet[p] = MESSAGE.USERAUTH_REQUEST;
|
|
|
|
writeUInt32BE(packet, userLen, ++p);
|
|
packet.utf8Write(username, p += 4, userLen);
|
|
|
|
writeUInt32BE(packet, 14, p += userLen);
|
|
packet.utf8Write('ssh-connection', p += 4, 14);
|
|
|
|
writeUInt32BE(packet, 8, p += 14);
|
|
packet.utf8Write('password', p += 4, 8);
|
|
|
|
packet[p += 8] = (newPassword ? 1 : 0);
|
|
|
|
writeUInt32BE(packet, passLen, ++p);
|
|
if (Buffer.isBuffer(password))
|
|
bufferCopy(password, packet, 0, passLen, p += 4);
|
|
else
|
|
packet.utf8Write(password, p += 4, passLen);
|
|
|
|
if (newPassword) {
|
|
writeUInt32BE(packet, newPassLen, p += passLen);
|
|
if (Buffer.isBuffer(newPassword))
|
|
bufferCopy(newPassword, packet, 0, newPassLen, p += 4);
|
|
else
|
|
packet.utf8Write(newPassword, p += 4, newPassLen);
|
|
this._debug && this._debug(
|
|
'Outbound: Sending USERAUTH_REQUEST (changed password)'
|
|
);
|
|
} else {
|
|
this._debug && this._debug(
|
|
'Outbound: Sending USERAUTH_REQUEST (password)'
|
|
);
|
|
}
|
|
|
|
this._authsQueue.push('password');
|
|
|
|
sendPacket(this, this._packetRW.write.finalize(packet));
|
|
}
|
|
authPK(username, pubKey, keyAlgo, cbSign) {
|
|
if (this._server)
|
|
throw new Error('Client-only method called in server mode');
|
|
|
|
pubKey = parseKey(pubKey);
|
|
if (pubKey instanceof Error)
|
|
throw new Error('Invalid key');
|
|
|
|
const keyType = pubKey.type;
|
|
pubKey = pubKey.getPublicSSH();
|
|
|
|
if (typeof keyAlgo === 'function') {
|
|
cbSign = keyAlgo;
|
|
keyAlgo = undefined;
|
|
}
|
|
if (!keyAlgo)
|
|
keyAlgo = keyType;
|
|
|
|
const userLen = Buffer.byteLength(username);
|
|
const algoLen = Buffer.byteLength(keyAlgo);
|
|
const pubKeyLen = pubKey.length;
|
|
const sessionID = this._kex.sessionID;
|
|
const sesLen = sessionID.length;
|
|
const payloadLen =
|
|
(cbSign ? 4 + sesLen : 0)
|
|
+ 1 + 4 + userLen + 4 + 14 + 4 + 9 + 1 + 4 + algoLen + 4 + pubKeyLen;
|
|
let packet;
|
|
let p;
|
|
if (cbSign) {
|
|
packet = Buffer.allocUnsafe(payloadLen);
|
|
p = 0;
|
|
writeUInt32BE(packet, sesLen, p);
|
|
packet.set(sessionID, p += 4);
|
|
p += sesLen;
|
|
} else {
|
|
packet = this._packetRW.write.alloc(payloadLen);
|
|
p = this._packetRW.write.allocStart;
|
|
}
|
|
|
|
packet[p] = MESSAGE.USERAUTH_REQUEST;
|
|
|
|
writeUInt32BE(packet, userLen, ++p);
|
|
packet.utf8Write(username, p += 4, userLen);
|
|
|
|
writeUInt32BE(packet, 14, p += userLen);
|
|
packet.utf8Write('ssh-connection', p += 4, 14);
|
|
|
|
writeUInt32BE(packet, 9, p += 14);
|
|
packet.utf8Write('publickey', p += 4, 9);
|
|
|
|
packet[p += 9] = (cbSign ? 1 : 0);
|
|
|
|
writeUInt32BE(packet, algoLen, ++p);
|
|
packet.utf8Write(keyAlgo, p += 4, algoLen);
|
|
|
|
writeUInt32BE(packet, pubKeyLen, p += algoLen);
|
|
packet.set(pubKey, p += 4);
|
|
|
|
if (!cbSign) {
|
|
this._authsQueue.push('publickey');
|
|
|
|
this._debug && this._debug(
|
|
'Outbound: Sending USERAUTH_REQUEST (publickey -- check)'
|
|
);
|
|
sendPacket(this, this._packetRW.write.finalize(packet));
|
|
return;
|
|
}
|
|
|
|
cbSign(packet, (signature) => {
|
|
signature = convertSignature(signature, keyType);
|
|
if (signature === false)
|
|
throw new Error('Error while converting handshake signature');
|
|
|
|
const sigLen = signature.length;
|
|
p = this._packetRW.write.allocStart;
|
|
packet = this._packetRW.write.alloc(
|
|
1 + 4 + userLen + 4 + 14 + 4 + 9 + 1 + 4 + algoLen + 4 + pubKeyLen + 4
|
|
+ 4 + algoLen + 4 + sigLen
|
|
);
|
|
|
|
// TODO: simply copy from original "packet" to new `packet` to avoid
|
|
// having to write each individual field a second time?
|
|
packet[p] = MESSAGE.USERAUTH_REQUEST;
|
|
|
|
writeUInt32BE(packet, userLen, ++p);
|
|
packet.utf8Write(username, p += 4, userLen);
|
|
|
|
writeUInt32BE(packet, 14, p += userLen);
|
|
packet.utf8Write('ssh-connection', p += 4, 14);
|
|
|
|
writeUInt32BE(packet, 9, p += 14);
|
|
packet.utf8Write('publickey', p += 4, 9);
|
|
|
|
packet[p += 9] = 1;
|
|
|
|
writeUInt32BE(packet, algoLen, ++p);
|
|
packet.utf8Write(keyAlgo, p += 4, algoLen);
|
|
|
|
writeUInt32BE(packet, pubKeyLen, p += algoLen);
|
|
packet.set(pubKey, p += 4);
|
|
|
|
writeUInt32BE(packet, 4 + algoLen + 4 + sigLen, p += pubKeyLen);
|
|
|
|
writeUInt32BE(packet, algoLen, p += 4);
|
|
packet.utf8Write(keyAlgo, p += 4, algoLen);
|
|
|
|
writeUInt32BE(packet, sigLen, p += algoLen);
|
|
packet.set(signature, p += 4);
|
|
|
|
// Servers shouldn't send packet type 60 in response to signed publickey
|
|
// attempts, but if they do, interpret as type 60.
|
|
this._authsQueue.push('publickey');
|
|
|
|
this._debug && this._debug(
|
|
'Outbound: Sending USERAUTH_REQUEST (publickey)'
|
|
);
|
|
sendPacket(this, this._packetRW.write.finalize(packet));
|
|
});
|
|
}
|
|
authHostbased(username, pubKey, hostname, userlocal, keyAlgo, cbSign) {
|
|
// TODO: Make DRY by sharing similar code with authPK()
|
|
if (this._server)
|
|
throw new Error('Client-only method called in server mode');
|
|
|
|
pubKey = parseKey(pubKey);
|
|
if (pubKey instanceof Error)
|
|
throw new Error('Invalid key');
|
|
|
|
const keyType = pubKey.type;
|
|
pubKey = pubKey.getPublicSSH();
|
|
|
|
if (typeof keyAlgo === 'function') {
|
|
cbSign = keyAlgo;
|
|
keyAlgo = undefined;
|
|
}
|
|
if (!keyAlgo)
|
|
keyAlgo = keyType;
|
|
|
|
const userLen = Buffer.byteLength(username);
|
|
const algoLen = Buffer.byteLength(keyAlgo);
|
|
const pubKeyLen = pubKey.length;
|
|
const sessionID = this._kex.sessionID;
|
|
const sesLen = sessionID.length;
|
|
const hostnameLen = Buffer.byteLength(hostname);
|
|
const userlocalLen = Buffer.byteLength(userlocal);
|
|
const data = Buffer.allocUnsafe(
|
|
4 + sesLen + 1 + 4 + userLen + 4 + 14 + 4 + 9 + 4 + algoLen
|
|
+ 4 + pubKeyLen + 4 + hostnameLen + 4 + userlocalLen
|
|
);
|
|
let p = 0;
|
|
|
|
writeUInt32BE(data, sesLen, p);
|
|
data.set(sessionID, p += 4);
|
|
|
|
data[p += sesLen] = MESSAGE.USERAUTH_REQUEST;
|
|
|
|
writeUInt32BE(data, userLen, ++p);
|
|
data.utf8Write(username, p += 4, userLen);
|
|
|
|
writeUInt32BE(data, 14, p += userLen);
|
|
data.utf8Write('ssh-connection', p += 4, 14);
|
|
|
|
writeUInt32BE(data, 9, p += 14);
|
|
data.utf8Write('hostbased', p += 4, 9);
|
|
|
|
writeUInt32BE(data, algoLen, p += 9);
|
|
data.utf8Write(keyAlgo, p += 4, algoLen);
|
|
|
|
writeUInt32BE(data, pubKeyLen, p += algoLen);
|
|
data.set(pubKey, p += 4);
|
|
|
|
writeUInt32BE(data, hostnameLen, p += pubKeyLen);
|
|
data.utf8Write(hostname, p += 4, hostnameLen);
|
|
|
|
writeUInt32BE(data, userlocalLen, p += hostnameLen);
|
|
data.utf8Write(userlocal, p += 4, userlocalLen);
|
|
|
|
cbSign(data, (signature) => {
|
|
signature = convertSignature(signature, keyType);
|
|
if (!signature)
|
|
throw new Error('Error while converting handshake signature');
|
|
|
|
const sigLen = signature.length;
|
|
const reqDataLen = (data.length - sesLen - 4);
|
|
p = this._packetRW.write.allocStart;
|
|
const packet = this._packetRW.write.alloc(
|
|
reqDataLen + 4 + 4 + algoLen + 4 + sigLen
|
|
);
|
|
|
|
bufferCopy(data, packet, 4 + sesLen, data.length, p);
|
|
|
|
writeUInt32BE(packet, 4 + algoLen + 4 + sigLen, p += reqDataLen);
|
|
writeUInt32BE(packet, algoLen, p += 4);
|
|
packet.utf8Write(keyAlgo, p += 4, algoLen);
|
|
writeUInt32BE(packet, sigLen, p += algoLen);
|
|
packet.set(signature, p += 4);
|
|
|
|
this._authsQueue.push('hostbased');
|
|
|
|
this._debug && this._debug(
|
|
'Outbound: Sending USERAUTH_REQUEST (hostbased)'
|
|
);
|
|
sendPacket(this, this._packetRW.write.finalize(packet));
|
|
});
|
|
}
|
|
authKeyboard(username) {
|
|
if (this._server)
|
|
throw new Error('Client-only method called in server mode');
|
|
|
|
const userLen = Buffer.byteLength(username);
|
|
let p = this._packetRW.write.allocStart;
|
|
const packet = this._packetRW.write.alloc(
|
|
1 + 4 + userLen + 4 + 14 + 4 + 20 + 4 + 4
|
|
);
|
|
|
|
packet[p] = MESSAGE.USERAUTH_REQUEST;
|
|
|
|
writeUInt32BE(packet, userLen, ++p);
|
|
packet.utf8Write(username, p += 4, userLen);
|
|
|
|
writeUInt32BE(packet, 14, p += userLen);
|
|
packet.utf8Write('ssh-connection', p += 4, 14);
|
|
|
|
writeUInt32BE(packet, 20, p += 14);
|
|
packet.utf8Write('keyboard-interactive', p += 4, 20);
|
|
|
|
writeUInt32BE(packet, 0, p += 20);
|
|
|
|
writeUInt32BE(packet, 0, p += 4);
|
|
|
|
this._authsQueue.push('keyboard-interactive');
|
|
|
|
this._debug && this._debug(
|
|
'Outbound: Sending USERAUTH_REQUEST (keyboard-interactive)'
|
|
);
|
|
sendPacket(this, this._packetRW.write.finalize(packet));
|
|
}
|
|
authNone(username) {
|
|
if (this._server)
|
|
throw new Error('Client-only method called in server mode');
|
|
|
|
const userLen = Buffer.byteLength(username);
|
|
let p = this._packetRW.write.allocStart;
|
|
const packet = this._packetRW.write.alloc(1 + 4 + userLen + 4 + 14 + 4 + 4);
|
|
|
|
packet[p] = MESSAGE.USERAUTH_REQUEST;
|
|
|
|
writeUInt32BE(packet, userLen, ++p);
|
|
packet.utf8Write(username, p += 4, userLen);
|
|
|
|
writeUInt32BE(packet, 14, p += userLen);
|
|
packet.utf8Write('ssh-connection', p += 4, 14);
|
|
|
|
writeUInt32BE(packet, 4, p += 14);
|
|
packet.utf8Write('none', p += 4, 4);
|
|
|
|
this._authsQueue.push('none');
|
|
|
|
this._debug && this._debug('Outbound: Sending USERAUTH_REQUEST (none)');
|
|
sendPacket(this, this._packetRW.write.finalize(packet));
|
|
}
|
|
authInfoRes(responses) {
|
|
if (this._server)
|
|
throw new Error('Client-only method called in server mode');
|
|
|
|
let responsesTotalLen = 0;
|
|
let responseLens;
|
|
|
|
if (responses) {
|
|
responseLens = new Array(responses.length);
|
|
for (let i = 0; i < responses.length; ++i) {
|
|
const len = Buffer.byteLength(responses[i]);
|
|
responseLens[i] = len;
|
|
responsesTotalLen += 4 + len;
|
|
}
|
|
}
|
|
|
|
let p = this._packetRW.write.allocStart;
|
|
const packet = this._packetRW.write.alloc(1 + 4 + responsesTotalLen);
|
|
|
|
packet[p] = MESSAGE.USERAUTH_INFO_RESPONSE;
|
|
|
|
if (responses) {
|
|
writeUInt32BE(packet, responses.length, ++p);
|
|
p += 4;
|
|
for (let i = 0; i < responses.length; ++i) {
|
|
const len = responseLens[i];
|
|
writeUInt32BE(packet, len, p);
|
|
p += 4;
|
|
if (len) {
|
|
packet.utf8Write(responses[i], p, len);
|
|
p += len;
|
|
}
|
|
}
|
|
} else {
|
|
writeUInt32BE(packet, 0, ++p);
|
|
}
|
|
|
|
this._debug && this._debug('Outbound: Sending USERAUTH_INFO_RESPONSE');
|
|
sendPacket(this, this._packetRW.write.finalize(packet));
|
|
}
|
|
|
|
// 'ssh-connection' service-specific
|
|
// ---------------------------------
|
|
tcpipForward(bindAddr, bindPort, wantReply) {
|
|
if (this._server)
|
|
throw new Error('Client-only method called in server mode');
|
|
|
|
const addrLen = Buffer.byteLength(bindAddr);
|
|
let p = this._packetRW.write.allocStart;
|
|
const packet = this._packetRW.write.alloc(1 + 4 + 13 + 1 + 4 + addrLen + 4);
|
|
|
|
packet[p] = MESSAGE.GLOBAL_REQUEST;
|
|
|
|
writeUInt32BE(packet, 13, ++p);
|
|
packet.utf8Write('tcpip-forward', p += 4, 13);
|
|
|
|
packet[p += 13] = (wantReply === undefined || wantReply === true ? 1 : 0);
|
|
|
|
writeUInt32BE(packet, addrLen, ++p);
|
|
packet.utf8Write(bindAddr, p += 4, addrLen);
|
|
|
|
writeUInt32BE(packet, bindPort, p += addrLen);
|
|
|
|
this._debug
|
|
&& this._debug('Outbound: Sending GLOBAL_REQUEST (tcpip-forward)');
|
|
sendPacket(this, this._packetRW.write.finalize(packet));
|
|
}
|
|
cancelTcpipForward(bindAddr, bindPort, wantReply) {
|
|
if (this._server)
|
|
throw new Error('Client-only method called in server mode');
|
|
|
|
const addrLen = Buffer.byteLength(bindAddr);
|
|
let p = this._packetRW.write.allocStart;
|
|
const packet = this._packetRW.write.alloc(1 + 4 + 20 + 1 + 4 + addrLen + 4);
|
|
|
|
packet[p] = MESSAGE.GLOBAL_REQUEST;
|
|
|
|
writeUInt32BE(packet, 20, ++p);
|
|
packet.utf8Write('cancel-tcpip-forward', p += 4, 20);
|
|
|
|
packet[p += 20] = (wantReply === undefined || wantReply === true ? 1 : 0);
|
|
|
|
writeUInt32BE(packet, addrLen, ++p);
|
|
packet.utf8Write(bindAddr, p += 4, addrLen);
|
|
|
|
writeUInt32BE(packet, bindPort, p += addrLen);
|
|
|
|
this._debug
|
|
&& this._debug('Outbound: Sending GLOBAL_REQUEST (cancel-tcpip-forward)');
|
|
sendPacket(this, this._packetRW.write.finalize(packet));
|
|
}
|
|
openssh_streamLocalForward(socketPath, wantReply) {
|
|
if (this._server)
|
|
throw new Error('Client-only method called in server mode');
|
|
|
|
const socketPathLen = Buffer.byteLength(socketPath);
|
|
let p = this._packetRW.write.allocStart;
|
|
const packet = this._packetRW.write.alloc(
|
|
1 + 4 + 31 + 1 + 4 + socketPathLen
|
|
);
|
|
|
|
packet[p] = MESSAGE.GLOBAL_REQUEST;
|
|
|
|
writeUInt32BE(packet, 31, ++p);
|
|
packet.utf8Write('streamlocal-forward@openssh.com', p += 4, 31);
|
|
|
|
packet[p += 31] = (wantReply === undefined || wantReply === true ? 1 : 0);
|
|
|
|
writeUInt32BE(packet, socketPathLen, ++p);
|
|
packet.utf8Write(socketPath, p += 4, socketPathLen);
|
|
|
|
this._debug && this._debug(
|
|
'Outbound: Sending GLOBAL_REQUEST (streamlocal-forward@openssh.com)'
|
|
);
|
|
sendPacket(this, this._packetRW.write.finalize(packet));
|
|
}
|
|
openssh_cancelStreamLocalForward(socketPath, wantReply) {
|
|
if (this._server)
|
|
throw new Error('Client-only method called in server mode');
|
|
|
|
const socketPathLen = Buffer.byteLength(socketPath);
|
|
let p = this._packetRW.write.allocStart;
|
|
const packet = this._packetRW.write.alloc(
|
|
1 + 4 + 38 + 1 + 4 + socketPathLen
|
|
);
|
|
|
|
packet[p] = MESSAGE.GLOBAL_REQUEST;
|
|
|
|
writeUInt32BE(packet, 38, ++p);
|
|
packet.utf8Write('cancel-streamlocal-forward@openssh.com', p += 4, 38);
|
|
|
|
packet[p += 38] = (wantReply === undefined || wantReply === true ? 1 : 0);
|
|
|
|
writeUInt32BE(packet, socketPathLen, ++p);
|
|
packet.utf8Write(socketPath, p += 4, socketPathLen);
|
|
|
|
if (this._debug) {
|
|
this._debug(
|
|
'Outbound: Sending GLOBAL_REQUEST '
|
|
+ '(cancel-streamlocal-forward@openssh.com)'
|
|
);
|
|
}
|
|
sendPacket(this, this._packetRW.write.finalize(packet));
|
|
}
|
|
directTcpip(chan, initWindow, maxPacket, cfg) {
|
|
if (this._server)
|
|
throw new Error('Client-only method called in server mode');
|
|
|
|
const srcLen = Buffer.byteLength(cfg.srcIP);
|
|
const dstLen = Buffer.byteLength(cfg.dstIP);
|
|
let p = this._packetRW.write.allocStart;
|
|
const packet = this._packetRW.write.alloc(
|
|
1 + 4 + 12 + 4 + 4 + 4 + 4 + srcLen + 4 + 4 + dstLen + 4
|
|
);
|
|
|
|
packet[p] = MESSAGE.CHANNEL_OPEN;
|
|
|
|
writeUInt32BE(packet, 12, ++p);
|
|
packet.utf8Write('direct-tcpip', p += 4, 12);
|
|
|
|
writeUInt32BE(packet, chan, p += 12);
|
|
|
|
writeUInt32BE(packet, initWindow, p += 4);
|
|
|
|
writeUInt32BE(packet, maxPacket, p += 4);
|
|
|
|
writeUInt32BE(packet, dstLen, p += 4);
|
|
packet.utf8Write(cfg.dstIP, p += 4, dstLen);
|
|
|
|
writeUInt32BE(packet, cfg.dstPort, p += dstLen);
|
|
|
|
writeUInt32BE(packet, srcLen, p += 4);
|
|
packet.utf8Write(cfg.srcIP, p += 4, srcLen);
|
|
|
|
writeUInt32BE(packet, cfg.srcPort, p += srcLen);
|
|
|
|
this._debug && this._debug(
|
|
`Outbound: Sending CHANNEL_OPEN (r:${chan}, direct-tcpip)`
|
|
);
|
|
sendPacket(this, this._packetRW.write.finalize(packet));
|
|
}
|
|
openssh_directStreamLocal(chan, initWindow, maxPacket, cfg) {
|
|
if (this._server)
|
|
throw new Error('Client-only method called in server mode');
|
|
|
|
const pathLen = Buffer.byteLength(cfg.socketPath);
|
|
let p = this._packetRW.write.allocStart;
|
|
const packet = this._packetRW.write.alloc(
|
|
1 + 4 + 30 + 4 + 4 + 4 + 4 + pathLen + 4 + 4
|
|
);
|
|
|
|
packet[p] = MESSAGE.CHANNEL_OPEN;
|
|
|
|
writeUInt32BE(packet, 30, ++p);
|
|
packet.utf8Write('direct-streamlocal@openssh.com', p += 4, 30);
|
|
|
|
writeUInt32BE(packet, chan, p += 30);
|
|
|
|
writeUInt32BE(packet, initWindow, p += 4);
|
|
|
|
writeUInt32BE(packet, maxPacket, p += 4);
|
|
|
|
writeUInt32BE(packet, pathLen, p += 4);
|
|
packet.utf8Write(cfg.socketPath, p += 4, pathLen);
|
|
|
|
// zero-fill reserved fields (string and uint32)
|
|
bufferFill(packet, 0, p += pathLen, p + 8);
|
|
|
|
if (this._debug) {
|
|
this._debug(
|
|
'Outbound: Sending CHANNEL_OPEN '
|
|
+ `(r:${chan}, direct-streamlocal@openssh.com)`
|
|
);
|
|
}
|
|
sendPacket(this, this._packetRW.write.finalize(packet));
|
|
}
|
|
openssh_noMoreSessions(wantReply) {
|
|
if (this._server)
|
|
throw new Error('Client-only method called in server mode');
|
|
|
|
let p = this._packetRW.write.allocStart;
|
|
const packet = this._packetRW.write.alloc(1 + 4 + 28 + 1);
|
|
|
|
packet[p] = MESSAGE.GLOBAL_REQUEST;
|
|
|
|
writeUInt32BE(packet, 28, ++p);
|
|
packet.utf8Write('no-more-sessions@openssh.com', p += 4, 28);
|
|
|
|
packet[p += 28] = (wantReply === undefined || wantReply === true ? 1 : 0);
|
|
|
|
this._debug && this._debug(
|
|
'Outbound: Sending GLOBAL_REQUEST (no-more-sessions@openssh.com)'
|
|
);
|
|
sendPacket(this, this._packetRW.write.finalize(packet));
|
|
}
|
|
session(chan, initWindow, maxPacket) {
|
|
if (this._server)
|
|
throw new Error('Client-only method called in server mode');
|
|
|
|
// Does not consume window space
|
|
|
|
let p = this._packetRW.write.allocStart;
|
|
const packet = this._packetRW.write.alloc(1 + 4 + 7 + 4 + 4 + 4);
|
|
|
|
packet[p] = MESSAGE.CHANNEL_OPEN;
|
|
|
|
writeUInt32BE(packet, 7, ++p);
|
|
packet.utf8Write('session', p += 4, 7);
|
|
|
|
writeUInt32BE(packet, chan, p += 7);
|
|
|
|
writeUInt32BE(packet, initWindow, p += 4);
|
|
|
|
writeUInt32BE(packet, maxPacket, p += 4);
|
|
|
|
this._debug
|
|
&& this._debug(`Outbound: Sending CHANNEL_OPEN (r:${chan}, session)`);
|
|
sendPacket(this, this._packetRW.write.finalize(packet));
|
|
}
|
|
windowChange(chan, rows, cols, height, width) {
|
|
if (this._server)
|
|
throw new Error('Client-only method called in server mode');
|
|
|
|
// Does not consume window space
|
|
|
|
let p = this._packetRW.write.allocStart;
|
|
const packet = this._packetRW.write.alloc(
|
|
1 + 4 + 4 + 13 + 1 + 4 + 4 + 4 + 4
|
|
);
|
|
|
|
packet[p] = MESSAGE.CHANNEL_REQUEST;
|
|
|
|
writeUInt32BE(packet, chan, ++p);
|
|
|
|
writeUInt32BE(packet, 13, p += 4);
|
|
packet.utf8Write('window-change', p += 4, 13);
|
|
|
|
packet[p += 13] = 0;
|
|
|
|
writeUInt32BE(packet, cols, ++p);
|
|
|
|
writeUInt32BE(packet, rows, p += 4);
|
|
|
|
writeUInt32BE(packet, width, p += 4);
|
|
|
|
writeUInt32BE(packet, height, p += 4);
|
|
|
|
this._debug && this._debug(
|
|
`Outbound: Sending CHANNEL_REQUEST (r:${chan}, window-change)`
|
|
);
|
|
sendPacket(this, this._packetRW.write.finalize(packet));
|
|
}
|
|
pty(chan, rows, cols, height, width, term, modes, wantReply) {
|
|
if (this._server)
|
|
throw new Error('Client-only method called in server mode');
|
|
|
|
// Does not consume window space
|
|
|
|
if (!term || !term.length)
|
|
term = 'vt100';
|
|
if (modes
|
|
&& !Buffer.isBuffer(modes)
|
|
&& !Array.isArray(modes)
|
|
&& typeof modes === 'object'
|
|
&& modes !== null) {
|
|
modes = modesToBytes(modes);
|
|
}
|
|
if (!modes || !modes.length)
|
|
modes = NO_TERMINAL_MODES_BUFFER;
|
|
|
|
const termLen = term.length;
|
|
const modesLen = modes.length;
|
|
let p = this._packetRW.write.allocStart;
|
|
const packet = this._packetRW.write.alloc(
|
|
1 + 4 + 4 + 7 + 1 + 4 + termLen + 4 + 4 + 4 + 4 + 4 + modesLen
|
|
);
|
|
|
|
packet[p] = MESSAGE.CHANNEL_REQUEST;
|
|
|
|
writeUInt32BE(packet, chan, ++p);
|
|
|
|
writeUInt32BE(packet, 7, p += 4);
|
|
packet.utf8Write('pty-req', p += 4, 7);
|
|
|
|
packet[p += 7] = (wantReply === undefined || wantReply === true ? 1 : 0);
|
|
|
|
writeUInt32BE(packet, termLen, ++p);
|
|
packet.utf8Write(term, p += 4, termLen);
|
|
|
|
writeUInt32BE(packet, cols, p += termLen);
|
|
|
|
writeUInt32BE(packet, rows, p += 4);
|
|
|
|
writeUInt32BE(packet, width, p += 4);
|
|
|
|
writeUInt32BE(packet, height, p += 4);
|
|
|
|
writeUInt32BE(packet, modesLen, p += 4);
|
|
p += 4;
|
|
if (Array.isArray(modes)) {
|
|
for (let i = 0; i < modesLen; ++i)
|
|
packet[p++] = modes[i];
|
|
} else if (Buffer.isBuffer(modes)) {
|
|
packet.set(modes, p);
|
|
}
|
|
|
|
this._debug
|
|
&& this._debug(`Outbound: Sending CHANNEL_REQUEST (r:${chan}, pty-req)`);
|
|
sendPacket(this, this._packetRW.write.finalize(packet));
|
|
}
|
|
shell(chan, wantReply) {
|
|
if (this._server)
|
|
throw new Error('Client-only method called in server mode');
|
|
|
|
// Does not consume window space
|
|
|
|
let p = this._packetRW.write.allocStart;
|
|
const packet = this._packetRW.write.alloc(1 + 4 + 4 + 5 + 1);
|
|
|
|
packet[p] = MESSAGE.CHANNEL_REQUEST;
|
|
|
|
writeUInt32BE(packet, chan, ++p);
|
|
|
|
writeUInt32BE(packet, 5, p += 4);
|
|
packet.utf8Write('shell', p += 4, 5);
|
|
|
|
packet[p += 5] = (wantReply === undefined || wantReply === true ? 1 : 0);
|
|
|
|
this._debug
|
|
&& this._debug(`Outbound: Sending CHANNEL_REQUEST (r:${chan}, shell)`);
|
|
sendPacket(this, this._packetRW.write.finalize(packet));
|
|
}
|
|
exec(chan, cmd, wantReply) {
|
|
if (this._server)
|
|
throw new Error('Client-only method called in server mode');
|
|
|
|
// Does not consume window space
|
|
|
|
const isBuf = Buffer.isBuffer(cmd);
|
|
const cmdLen = (isBuf ? cmd.length : Buffer.byteLength(cmd));
|
|
let p = this._packetRW.write.allocStart;
|
|
const packet = this._packetRW.write.alloc(1 + 4 + 4 + 4 + 1 + 4 + cmdLen);
|
|
|
|
packet[p] = MESSAGE.CHANNEL_REQUEST;
|
|
|
|
writeUInt32BE(packet, chan, ++p);
|
|
|
|
writeUInt32BE(packet, 4, p += 4);
|
|
packet.utf8Write('exec', p += 4, 4);
|
|
|
|
packet[p += 4] = (wantReply === undefined || wantReply === true ? 1 : 0);
|
|
|
|
writeUInt32BE(packet, cmdLen, ++p);
|
|
if (isBuf)
|
|
packet.set(cmd, p += 4);
|
|
else
|
|
packet.utf8Write(cmd, p += 4, cmdLen);
|
|
|
|
this._debug && this._debug(
|
|
`Outbound: Sending CHANNEL_REQUEST (r:${chan}, exec: ${cmd})`
|
|
);
|
|
sendPacket(this, this._packetRW.write.finalize(packet));
|
|
}
|
|
signal(chan, signal) {
|
|
if (this._server)
|
|
throw new Error('Client-only method called in server mode');
|
|
|
|
// Does not consume window space
|
|
|
|
const origSignal = signal;
|
|
|
|
signal = signal.toUpperCase();
|
|
if (signal.slice(0, 3) === 'SIG')
|
|
signal = signal.slice(3);
|
|
|
|
if (SIGNALS[signal] !== 1)
|
|
throw new Error(`Invalid signal: ${origSignal}`);
|
|
|
|
const signalLen = signal.length;
|
|
let p = this._packetRW.write.allocStart;
|
|
const packet = this._packetRW.write.alloc(
|
|
1 + 4 + 4 + 6 + 1 + 4 + signalLen
|
|
);
|
|
|
|
packet[p] = MESSAGE.CHANNEL_REQUEST;
|
|
|
|
writeUInt32BE(packet, chan, ++p);
|
|
|
|
writeUInt32BE(packet, 6, p += 4);
|
|
packet.utf8Write('signal', p += 4, 6);
|
|
|
|
packet[p += 6] = 0;
|
|
|
|
writeUInt32BE(packet, signalLen, ++p);
|
|
packet.utf8Write(signal, p += 4, signalLen);
|
|
|
|
this._debug && this._debug(
|
|
`Outbound: Sending CHANNEL_REQUEST (r:${chan}, signal: ${signal})`
|
|
);
|
|
sendPacket(this, this._packetRW.write.finalize(packet));
|
|
}
|
|
env(chan, key, val, wantReply) {
|
|
if (this._server)
|
|
throw new Error('Client-only method called in server mode');
|
|
|
|
// Does not consume window space
|
|
|
|
const keyLen = Buffer.byteLength(key);
|
|
const isBuf = Buffer.isBuffer(val);
|
|
const valLen = (isBuf ? val.length : Buffer.byteLength(val));
|
|
let p = this._packetRW.write.allocStart;
|
|
const packet = this._packetRW.write.alloc(
|
|
1 + 4 + 4 + 3 + 1 + 4 + keyLen + 4 + valLen
|
|
);
|
|
|
|
packet[p] = MESSAGE.CHANNEL_REQUEST;
|
|
|
|
writeUInt32BE(packet, chan, ++p);
|
|
|
|
writeUInt32BE(packet, 3, p += 4);
|
|
packet.utf8Write('env', p += 4, 3);
|
|
|
|
packet[p += 3] = (wantReply === undefined || wantReply === true ? 1 : 0);
|
|
|
|
writeUInt32BE(packet, keyLen, ++p);
|
|
packet.utf8Write(key, p += 4, keyLen);
|
|
|
|
writeUInt32BE(packet, valLen, p += keyLen);
|
|
if (isBuf)
|
|
packet.set(val, p += 4);
|
|
else
|
|
packet.utf8Write(val, p += 4, valLen);
|
|
|
|
this._debug && this._debug(
|
|
`Outbound: Sending CHANNEL_REQUEST (r:${chan}, env: ${key}=${val})`
|
|
);
|
|
sendPacket(this, this._packetRW.write.finalize(packet));
|
|
}
|
|
x11Forward(chan, cfg, wantReply) {
|
|
if (this._server)
|
|
throw new Error('Client-only method called in server mode');
|
|
|
|
// Does not consume window space
|
|
|
|
const protocol = cfg.protocol;
|
|
const cookie = cfg.cookie;
|
|
const isBufProto = Buffer.isBuffer(protocol);
|
|
const protoLen = (isBufProto
|
|
? protocol.length
|
|
: Buffer.byteLength(protocol));
|
|
const isBufCookie = Buffer.isBuffer(cookie);
|
|
const cookieLen = (isBufCookie
|
|
? cookie.length
|
|
: Buffer.byteLength(cookie));
|
|
let p = this._packetRW.write.allocStart;
|
|
const packet = this._packetRW.write.alloc(
|
|
1 + 4 + 4 + 7 + 1 + 1 + 4 + protoLen + 4 + cookieLen + 4
|
|
);
|
|
|
|
packet[p] = MESSAGE.CHANNEL_REQUEST;
|
|
|
|
writeUInt32BE(packet, chan, ++p);
|
|
|
|
writeUInt32BE(packet, 7, p += 4);
|
|
packet.utf8Write('x11-req', p += 4, 7);
|
|
|
|
packet[p += 7] = (wantReply === undefined || wantReply === true ? 1 : 0);
|
|
|
|
packet[++p] = (cfg.single ? 1 : 0);
|
|
|
|
writeUInt32BE(packet, protoLen, ++p);
|
|
if (isBufProto)
|
|
packet.set(protocol, p += 4);
|
|
else
|
|
packet.utf8Write(protocol, p += 4, protoLen);
|
|
|
|
writeUInt32BE(packet, cookieLen, p += protoLen);
|
|
if (isBufCookie)
|
|
packet.set(cookie, p += 4);
|
|
else
|
|
packet.latin1Write(cookie, p += 4, cookieLen);
|
|
|
|
writeUInt32BE(packet, (cfg.screen || 0), p += cookieLen);
|
|
|
|
this._debug
|
|
&& this._debug(`Outbound: Sending CHANNEL_REQUEST (r:${chan}, x11-req)`);
|
|
sendPacket(this, this._packetRW.write.finalize(packet));
|
|
}
|
|
subsystem(chan, name, wantReply) {
|
|
if (this._server)
|
|
throw new Error('Client-only method called in server mode');
|
|
|
|
// Does not consume window space
|
|
const nameLen = Buffer.byteLength(name);
|
|
let p = this._packetRW.write.allocStart;
|
|
const packet = this._packetRW.write.alloc(1 + 4 + 4 + 9 + 1 + 4 + nameLen);
|
|
|
|
packet[p] = MESSAGE.CHANNEL_REQUEST;
|
|
|
|
writeUInt32BE(packet, chan, ++p);
|
|
|
|
writeUInt32BE(packet, 9, p += 4);
|
|
packet.utf8Write('subsystem', p += 4, 9);
|
|
|
|
packet[p += 9] = (wantReply === undefined || wantReply === true ? 1 : 0);
|
|
|
|
writeUInt32BE(packet, nameLen, ++p);
|
|
packet.utf8Write(name, p += 4, nameLen);
|
|
|
|
this._debug && this._debug(
|
|
`Outbound: Sending CHANNEL_REQUEST (r:${chan}, subsystem: ${name})`
|
|
);
|
|
sendPacket(this, this._packetRW.write.finalize(packet));
|
|
}
|
|
openssh_agentForward(chan, wantReply) {
|
|
if (this._server)
|
|
throw new Error('Client-only method called in server mode');
|
|
|
|
// Does not consume window space
|
|
|
|
let p = this._packetRW.write.allocStart;
|
|
const packet = this._packetRW.write.alloc(1 + 4 + 4 + 26 + 1);
|
|
|
|
packet[p] = MESSAGE.CHANNEL_REQUEST;
|
|
|
|
writeUInt32BE(packet, chan, ++p);
|
|
|
|
writeUInt32BE(packet, 26, p += 4);
|
|
packet.utf8Write('auth-agent-req@openssh.com', p += 4, 26);
|
|
|
|
packet[p += 26] = (wantReply === undefined || wantReply === true ? 1 : 0);
|
|
|
|
if (this._debug) {
|
|
this._debug(
|
|
'Outbound: Sending CHANNEL_REQUEST '
|
|
+ `(r:${chan}, auth-agent-req@openssh.com)`
|
|
);
|
|
}
|
|
sendPacket(this, this._packetRW.write.finalize(packet));
|
|
}
|
|
openssh_hostKeysProve(keys) {
|
|
if (this._server)
|
|
throw new Error('Client-only method called in server mode');
|
|
|
|
let keysTotal = 0;
|
|
const publicKeys = [];
|
|
for (const key of keys) {
|
|
const publicKey = key.getPublicSSH();
|
|
keysTotal += 4 + publicKey.length;
|
|
publicKeys.push(publicKey);
|
|
}
|
|
|
|
let p = this._packetRW.write.allocStart;
|
|
const packet = this._packetRW.write.alloc(1 + 4 + 29 + 1 + keysTotal);
|
|
|
|
packet[p] = MESSAGE.GLOBAL_REQUEST;
|
|
|
|
writeUInt32BE(packet, 29, ++p);
|
|
packet.utf8Write('hostkeys-prove-00@openssh.com', p += 4, 29);
|
|
|
|
packet[p += 29] = 1; // want reply
|
|
|
|
++p;
|
|
for (const buf of publicKeys) {
|
|
writeUInt32BE(packet, buf.length, p);
|
|
bufferCopy(buf, packet, 0, buf.length, p += 4);
|
|
p += buf.length;
|
|
}
|
|
|
|
if (this._debug) {
|
|
this._debug(
|
|
'Outbound: Sending GLOBAL_REQUEST (hostkeys-prove-00@openssh.com)'
|
|
);
|
|
}
|
|
sendPacket(this, this._packetRW.write.finalize(packet));
|
|
}
|
|
|
|
// ===========================================================================
|
|
// Server-specific ===========================================================
|
|
// ===========================================================================
|
|
|
|
// Global
|
|
// ------
|
|
serviceAccept(svcName) {
|
|
if (!this._server)
|
|
throw new Error('Server-only method called in client mode');
|
|
|
|
const svcNameLen = Buffer.byteLength(svcName);
|
|
let p = this._packetRW.write.allocStart;
|
|
const packet = this._packetRW.write.alloc(1 + 4 + svcNameLen);
|
|
|
|
packet[p] = MESSAGE.SERVICE_ACCEPT;
|
|
|
|
writeUInt32BE(packet, svcNameLen, ++p);
|
|
packet.utf8Write(svcName, p += 4, svcNameLen);
|
|
|
|
this._debug && this._debug(`Outbound: Sending SERVICE_ACCEPT (${svcName})`);
|
|
sendPacket(this, this._packetRW.write.finalize(packet));
|
|
|
|
if (this._server && this._banner && svcName === 'ssh-userauth') {
|
|
const banner = this._banner;
|
|
this._banner = undefined; // Prevent banner from being displayed again
|
|
const bannerLen = Buffer.byteLength(banner);
|
|
p = this._packetRW.write.allocStart;
|
|
const packet = this._packetRW.write.alloc(1 + 4 + bannerLen + 4);
|
|
|
|
packet[p] = MESSAGE.USERAUTH_BANNER;
|
|
|
|
writeUInt32BE(packet, bannerLen, ++p);
|
|
packet.utf8Write(banner, p += 4, bannerLen);
|
|
|
|
writeUInt32BE(packet, 0, p += bannerLen); // Empty language tag
|
|
|
|
this._debug && this._debug('Outbound: Sending USERAUTH_BANNER');
|
|
sendPacket(this, this._packetRW.write.finalize(packet));
|
|
}
|
|
}
|
|
// 'ssh-connection' service-specific
|
|
forwardedTcpip(chan, initWindow, maxPacket, cfg) {
|
|
if (!this._server)
|
|
throw new Error('Server-only method called in client mode');
|
|
|
|
const boundAddrLen = Buffer.byteLength(cfg.boundAddr);
|
|
const remoteAddrLen = Buffer.byteLength(cfg.remoteAddr);
|
|
let p = this._packetRW.write.allocStart;
|
|
const packet = this._packetRW.write.alloc(
|
|
1 + 4 + 15 + 4 + 4 + 4 + 4 + boundAddrLen + 4 + 4 + remoteAddrLen + 4
|
|
);
|
|
|
|
packet[p] = MESSAGE.CHANNEL_OPEN;
|
|
|
|
writeUInt32BE(packet, 15, ++p);
|
|
packet.utf8Write('forwarded-tcpip', p += 4, 15);
|
|
|
|
writeUInt32BE(packet, chan, p += 15);
|
|
|
|
writeUInt32BE(packet, initWindow, p += 4);
|
|
|
|
writeUInt32BE(packet, maxPacket, p += 4);
|
|
|
|
writeUInt32BE(packet, boundAddrLen, p += 4);
|
|
packet.utf8Write(cfg.boundAddr, p += 4, boundAddrLen);
|
|
|
|
writeUInt32BE(packet, cfg.boundPort, p += boundAddrLen);
|
|
|
|
writeUInt32BE(packet, remoteAddrLen, p += 4);
|
|
packet.utf8Write(cfg.remoteAddr, p += 4, remoteAddrLen);
|
|
|
|
writeUInt32BE(packet, cfg.remotePort, p += remoteAddrLen);
|
|
|
|
this._debug && this._debug(
|
|
`Outbound: Sending CHANNEL_OPEN (r:${chan}, forwarded-tcpip)`
|
|
);
|
|
sendPacket(this, this._packetRW.write.finalize(packet));
|
|
}
|
|
x11(chan, initWindow, maxPacket, cfg) {
|
|
if (!this._server)
|
|
throw new Error('Server-only method called in client mode');
|
|
|
|
const addrLen = Buffer.byteLength(cfg.originAddr);
|
|
let p = this._packetRW.write.allocStart;
|
|
const packet = this._packetRW.write.alloc(
|
|
1 + 4 + 3 + 4 + 4 + 4 + 4 + addrLen + 4
|
|
);
|
|
|
|
packet[p] = MESSAGE.CHANNEL_OPEN;
|
|
|
|
writeUInt32BE(packet, 3, ++p);
|
|
packet.utf8Write('x11', p += 4, 3);
|
|
|
|
writeUInt32BE(packet, chan, p += 3);
|
|
|
|
writeUInt32BE(packet, initWindow, p += 4);
|
|
|
|
writeUInt32BE(packet, maxPacket, p += 4);
|
|
|
|
writeUInt32BE(packet, addrLen, p += 4);
|
|
packet.utf8Write(cfg.originAddr, p += 4, addrLen);
|
|
|
|
writeUInt32BE(packet, cfg.originPort, p += addrLen);
|
|
|
|
this._debug && this._debug(
|
|
`Outbound: Sending CHANNEL_OPEN (r:${chan}, x11)`
|
|
);
|
|
sendPacket(this, this._packetRW.write.finalize(packet));
|
|
}
|
|
openssh_authAgent(chan, initWindow, maxPacket) {
|
|
if (!this._server)
|
|
throw new Error('Server-only method called in client mode');
|
|
|
|
let p = this._packetRW.write.allocStart;
|
|
const packet = this._packetRW.write.alloc(1 + 4 + 22 + 4 + 4 + 4);
|
|
|
|
packet[p] = MESSAGE.CHANNEL_OPEN;
|
|
|
|
writeUInt32BE(packet, 22, ++p);
|
|
packet.utf8Write('auth-agent@openssh.com', p += 4, 22);
|
|
|
|
writeUInt32BE(packet, chan, p += 22);
|
|
|
|
writeUInt32BE(packet, initWindow, p += 4);
|
|
|
|
writeUInt32BE(packet, maxPacket, p += 4);
|
|
|
|
this._debug && this._debug(
|
|
`Outbound: Sending CHANNEL_OPEN (r:${chan}, auth-agent@openssh.com)`
|
|
);
|
|
sendPacket(this, this._packetRW.write.finalize(packet));
|
|
}
|
|
openssh_forwardedStreamLocal(chan, initWindow, maxPacket, cfg) {
|
|
if (!this._server)
|
|
throw new Error('Server-only method called in client mode');
|
|
|
|
const pathLen = Buffer.byteLength(cfg.socketPath);
|
|
let p = this._packetRW.write.allocStart;
|
|
const packet = this._packetRW.write.alloc(
|
|
1 + 4 + 33 + 4 + 4 + 4 + 4 + pathLen + 4
|
|
);
|
|
|
|
packet[p] = MESSAGE.CHANNEL_OPEN;
|
|
|
|
writeUInt32BE(packet, 33, ++p);
|
|
packet.utf8Write('forwarded-streamlocal@openssh.com', p += 4, 33);
|
|
|
|
writeUInt32BE(packet, chan, p += 33);
|
|
|
|
writeUInt32BE(packet, initWindow, p += 4);
|
|
|
|
writeUInt32BE(packet, maxPacket, p += 4);
|
|
|
|
writeUInt32BE(packet, pathLen, p += 4);
|
|
packet.utf8Write(cfg.socketPath, p += 4, pathLen);
|
|
|
|
writeUInt32BE(packet, 0, p += pathLen);
|
|
|
|
if (this._debug) {
|
|
this._debug(
|
|
'Outbound: Sending CHANNEL_OPEN '
|
|
+ `(r:${chan}, forwarded-streamlocal@openssh.com)`
|
|
);
|
|
}
|
|
sendPacket(this, this._packetRW.write.finalize(packet));
|
|
}
|
|
exitStatus(chan, status) {
|
|
if (!this._server)
|
|
throw new Error('Server-only method called in client mode');
|
|
|
|
// Does not consume window space
|
|
let p = this._packetRW.write.allocStart;
|
|
const packet = this._packetRW.write.alloc(1 + 4 + 4 + 11 + 1 + 4);
|
|
|
|
packet[p] = MESSAGE.CHANNEL_REQUEST;
|
|
|
|
writeUInt32BE(packet, chan, ++p);
|
|
|
|
writeUInt32BE(packet, 11, p += 4);
|
|
packet.utf8Write('exit-status', p += 4, 11);
|
|
|
|
packet[p += 11] = 0;
|
|
|
|
writeUInt32BE(packet, status, ++p);
|
|
|
|
this._debug && this._debug(
|
|
`Outbound: Sending CHANNEL_REQUEST (r:${chan}, exit-status: ${status})`
|
|
);
|
|
sendPacket(this, this._packetRW.write.finalize(packet));
|
|
}
|
|
exitSignal(chan, name, coreDumped, msg) {
|
|
if (!this._server)
|
|
throw new Error('Server-only method called in client mode');
|
|
|
|
// Does not consume window space
|
|
|
|
const origSignal = name;
|
|
|
|
if (typeof origSignal !== 'string' || !origSignal)
|
|
throw new Error(`Invalid signal: ${origSignal}`);
|
|
|
|
let signal = name.toUpperCase();
|
|
if (signal.slice(0, 3) === 'SIG')
|
|
signal = signal.slice(3);
|
|
|
|
if (SIGNALS[signal] !== 1)
|
|
throw new Error(`Invalid signal: ${origSignal}`);
|
|
|
|
const nameLen = Buffer.byteLength(signal);
|
|
const msgLen = (msg ? Buffer.byteLength(msg) : 0);
|
|
let p = this._packetRW.write.allocStart;
|
|
const packet = this._packetRW.write.alloc(
|
|
1 + 4 + 4 + 11 + 1 + 4 + nameLen + 1 + 4 + msgLen + 4
|
|
);
|
|
|
|
packet[p] = MESSAGE.CHANNEL_REQUEST;
|
|
|
|
writeUInt32BE(packet, chan, ++p);
|
|
|
|
writeUInt32BE(packet, 11, p += 4);
|
|
packet.utf8Write('exit-signal', p += 4, 11);
|
|
|
|
packet[p += 11] = 0;
|
|
|
|
writeUInt32BE(packet, nameLen, ++p);
|
|
packet.utf8Write(signal, p += 4, nameLen);
|
|
|
|
packet[p += nameLen] = (coreDumped ? 1 : 0);
|
|
|
|
writeUInt32BE(packet, msgLen, ++p);
|
|
|
|
p += 4;
|
|
if (msgLen) {
|
|
packet.utf8Write(msg, p, msgLen);
|
|
p += msgLen;
|
|
}
|
|
|
|
writeUInt32BE(packet, 0, p);
|
|
|
|
this._debug && this._debug(
|
|
`Outbound: Sending CHANNEL_REQUEST (r:${chan}, exit-signal: ${name})`
|
|
);
|
|
sendPacket(this, this._packetRW.write.finalize(packet));
|
|
}
|
|
// 'ssh-userauth' service-specific
|
|
authFailure(authMethods, isPartial) {
|
|
if (!this._server)
|
|
throw new Error('Server-only method called in client mode');
|
|
|
|
if (this._authsQueue.length === 0)
|
|
throw new Error('No auth in progress');
|
|
|
|
let methods;
|
|
|
|
if (typeof authMethods === 'boolean') {
|
|
isPartial = authMethods;
|
|
authMethods = undefined;
|
|
}
|
|
|
|
if (authMethods) {
|
|
methods = [];
|
|
for (let i = 0; i < authMethods.length; ++i) {
|
|
if (authMethods[i].toLowerCase() === 'none')
|
|
continue;
|
|
methods.push(authMethods[i]);
|
|
}
|
|
methods = methods.join(',');
|
|
} else {
|
|
methods = '';
|
|
}
|
|
|
|
const methodsLen = methods.length;
|
|
let p = this._packetRW.write.allocStart;
|
|
const packet = this._packetRW.write.alloc(1 + 4 + methodsLen + 1);
|
|
|
|
packet[p] = MESSAGE.USERAUTH_FAILURE;
|
|
|
|
writeUInt32BE(packet, methodsLen, ++p);
|
|
packet.utf8Write(methods, p += 4, methodsLen);
|
|
|
|
packet[p += methodsLen] = (isPartial === true ? 1 : 0);
|
|
|
|
this._authsQueue.shift();
|
|
|
|
this._debug && this._debug('Outbound: Sending USERAUTH_FAILURE');
|
|
sendPacket(this, this._packetRW.write.finalize(packet));
|
|
}
|
|
authSuccess() {
|
|
if (!this._server)
|
|
throw new Error('Server-only method called in client mode');
|
|
|
|
if (this._authsQueue.length === 0)
|
|
throw new Error('No auth in progress');
|
|
|
|
const p = this._packetRW.write.allocStart;
|
|
const packet = this._packetRW.write.alloc(1);
|
|
|
|
packet[p] = MESSAGE.USERAUTH_SUCCESS;
|
|
|
|
this._authsQueue.shift();
|
|
this._authenticated = true;
|
|
|
|
this._debug && this._debug('Outbound: Sending USERAUTH_SUCCESS');
|
|
sendPacket(this, this._packetRW.write.finalize(packet));
|
|
|
|
if (this._kex.negotiated.cs.compress === 'zlib@openssh.com')
|
|
this._packetRW.read = new ZlibPacketReader();
|
|
if (this._kex.negotiated.sc.compress === 'zlib@openssh.com')
|
|
this._packetRW.write = new ZlibPacketWriter(this);
|
|
}
|
|
authPKOK(keyAlgo, key) {
|
|
if (!this._server)
|
|
throw new Error('Server-only method called in client mode');
|
|
|
|
if (this._authsQueue.length === 0 || this._authsQueue[0] !== 'publickey')
|
|
throw new Error('"publickey" auth not in progress');
|
|
|
|
// TODO: support parsed key for `key`
|
|
|
|
const keyAlgoLen = Buffer.byteLength(keyAlgo);
|
|
const keyLen = key.length;
|
|
let p = this._packetRW.write.allocStart;
|
|
const packet = this._packetRW.write.alloc(1 + 4 + keyAlgoLen + 4 + keyLen);
|
|
|
|
packet[p] = MESSAGE.USERAUTH_PK_OK;
|
|
|
|
writeUInt32BE(packet, keyAlgoLen, ++p);
|
|
packet.utf8Write(keyAlgo, p += 4, keyAlgoLen);
|
|
|
|
writeUInt32BE(packet, keyLen, p += keyAlgoLen);
|
|
packet.set(key, p += 4);
|
|
|
|
this._authsQueue.shift();
|
|
|
|
this._debug && this._debug('Outbound: Sending USERAUTH_PK_OK');
|
|
sendPacket(this, this._packetRW.write.finalize(packet));
|
|
}
|
|
authPasswdChg(prompt) {
|
|
if (!this._server)
|
|
throw new Error('Server-only method called in client mode');
|
|
|
|
const promptLen = Buffer.byteLength(prompt);
|
|
let p = this._packetRW.write.allocStart;
|
|
const packet = this._packetRW.write.alloc(1 + 4 + promptLen + 4);
|
|
|
|
packet[p] = MESSAGE.USERAUTH_PASSWD_CHANGEREQ;
|
|
|
|
writeUInt32BE(packet, promptLen, ++p);
|
|
packet.utf8Write(prompt, p += 4, promptLen);
|
|
|
|
writeUInt32BE(packet, 0, p += promptLen); // Empty language tag
|
|
|
|
this._debug && this._debug('Outbound: Sending USERAUTH_PASSWD_CHANGEREQ');
|
|
sendPacket(this, this._packetRW.write.finalize(packet));
|
|
}
|
|
authInfoReq(name, instructions, prompts) {
|
|
if (!this._server)
|
|
throw new Error('Server-only method called in client mode');
|
|
|
|
let promptsLen = 0;
|
|
const nameLen = name ? Buffer.byteLength(name) : 0;
|
|
const instrLen = instructions ? Buffer.byteLength(instructions) : 0;
|
|
|
|
for (let i = 0; i < prompts.length; ++i)
|
|
promptsLen += 4 + Buffer.byteLength(prompts[i].prompt) + 1;
|
|
|
|
let p = this._packetRW.write.allocStart;
|
|
const packet = this._packetRW.write.alloc(
|
|
1 + 4 + nameLen + 4 + instrLen + 4 + 4 + promptsLen
|
|
);
|
|
|
|
packet[p] = MESSAGE.USERAUTH_INFO_REQUEST;
|
|
|
|
writeUInt32BE(packet, nameLen, ++p);
|
|
p += 4;
|
|
if (name) {
|
|
packet.utf8Write(name, p, nameLen);
|
|
p += nameLen;
|
|
}
|
|
|
|
writeUInt32BE(packet, instrLen, p);
|
|
p += 4;
|
|
if (instructions) {
|
|
packet.utf8Write(instructions, p, instrLen);
|
|
p += instrLen;
|
|
}
|
|
|
|
writeUInt32BE(packet, 0, p);
|
|
|
|
writeUInt32BE(packet, prompts.length, p += 4);
|
|
p += 4;
|
|
for (let i = 0; i < prompts.length; ++i) {
|
|
const prompt = prompts[i];
|
|
const promptLen = Buffer.byteLength(prompt.prompt);
|
|
|
|
writeUInt32BE(packet, promptLen, p);
|
|
p += 4;
|
|
if (promptLen) {
|
|
packet.utf8Write(prompt.prompt, p, promptLen);
|
|
p += promptLen;
|
|
}
|
|
packet[p++] = (prompt.echo ? 1 : 0);
|
|
}
|
|
|
|
this._debug && this._debug('Outbound: Sending USERAUTH_INFO_REQUEST');
|
|
sendPacket(this, this._packetRW.write.finalize(packet));
|
|
}
|
|
}
|
|
|
|
// SSH-protoversion-softwareversion (SP comments) CR LF
|
|
const RE_IDENT = /^SSH-(2\.0|1\.99)-([^ ]+)(?: (.*))?$/;
|
|
|
|
// TODO: optimize this by starting n bytes from the end of this._buffer instead
|
|
// of the beginning
|
|
function parseHeader(chunk, p, len) {
|
|
let data;
|
|
let chunkOffset;
|
|
if (this._buffer) {
|
|
data = Buffer.allocUnsafe(this._buffer.length + (len - p));
|
|
data.set(this._buffer, 0);
|
|
if (p === 0) {
|
|
data.set(chunk, this._buffer.length);
|
|
} else {
|
|
data.set(new Uint8Array(chunk.buffer,
|
|
chunk.byteOffset + p,
|
|
(len - p)),
|
|
this._buffer.length);
|
|
}
|
|
chunkOffset = this._buffer.length;
|
|
p = 0;
|
|
} else {
|
|
data = chunk;
|
|
chunkOffset = 0;
|
|
}
|
|
const op = p;
|
|
let start = p;
|
|
let end = p;
|
|
let needNL = false;
|
|
let lineLen = 0;
|
|
let lines = 0;
|
|
for (; p < data.length; ++p) {
|
|
const ch = data[p];
|
|
|
|
if (ch === 13 /* '\r' */) {
|
|
needNL = true;
|
|
continue;
|
|
}
|
|
|
|
if (ch === 10 /* '\n' */) {
|
|
if (end > start
|
|
&& end - start > 4
|
|
&& data[start] === 83 /* 'S' */
|
|
&& data[start + 1] === 83 /* 'S' */
|
|
&& data[start + 2] === 72 /* 'H' */
|
|
&& data[start + 3] === 45 /* '-' */) {
|
|
|
|
const full = data.latin1Slice(op, end + 1);
|
|
const identRaw = (start === op ? full : full.slice(start - op));
|
|
const m = RE_IDENT.exec(identRaw);
|
|
if (!m)
|
|
throw new Error('Invalid identification string');
|
|
|
|
const header = {
|
|
greeting: (start === op ? '' : full.slice(0, start - op)),
|
|
identRaw,
|
|
versions: {
|
|
protocol: m[1],
|
|
software: m[2],
|
|
},
|
|
comments: m[3]
|
|
};
|
|
|
|
// Needed during handshake
|
|
this._remoteIdentRaw = Buffer.from(identRaw);
|
|
|
|
this._debug && this._debug(`Remote ident: ${inspect(identRaw)}`);
|
|
this._compatFlags = getCompatFlags(header);
|
|
|
|
this._buffer = undefined;
|
|
this._decipher =
|
|
new NullDecipher(0, onKEXPayload.bind(this, { firstPacket: true }));
|
|
this._parse = parsePacket;
|
|
|
|
this._onHeader(header);
|
|
if (!this._destruct) {
|
|
// We disconnected inside _onHeader
|
|
return len;
|
|
}
|
|
|
|
kexinit(this);
|
|
|
|
return p + 1 - chunkOffset;
|
|
}
|
|
|
|
// Only allow pre-ident greetings when we're a client
|
|
if (this._server)
|
|
throw new Error('Greetings from clients not permitted');
|
|
|
|
if (++lines > MAX_LINES)
|
|
throw new Error('Max greeting lines exceeded');
|
|
|
|
needNL = false;
|
|
start = p + 1;
|
|
lineLen = 0;
|
|
} else if (needNL) {
|
|
throw new Error('Invalid header: expected newline');
|
|
} else if (++lineLen >= MAX_LINE_LEN) {
|
|
throw new Error('Header line too long');
|
|
}
|
|
|
|
end = p;
|
|
}
|
|
if (!this._buffer)
|
|
this._buffer = bufferSlice(data, op);
|
|
|
|
return p - chunkOffset;
|
|
}
|
|
|
|
function parsePacket(chunk, p, len) {
|
|
return this._decipher.decrypt(chunk, p, len);
|
|
}
|
|
|
|
function onPayload(payload) {
|
|
// XXX: move this to the Decipher implementations?
|
|
|
|
this._onPacket();
|
|
|
|
if (payload.length === 0) {
|
|
this._debug && this._debug('Inbound: Skipping empty packet payload');
|
|
return;
|
|
}
|
|
|
|
payload = this._packetRW.read.read(payload);
|
|
|
|
const type = payload[0];
|
|
if (type === MESSAGE.USERAUTH_SUCCESS
|
|
&& !this._server
|
|
&& !this._authenticated) {
|
|
this._authenticated = true;
|
|
if (this._kex.negotiated.cs.compress === 'zlib@openssh.com')
|
|
this._packetRW.write = new ZlibPacketWriter(this);
|
|
if (this._kex.negotiated.sc.compress === 'zlib@openssh.com')
|
|
this._packetRW.read = new ZlibPacketReader();
|
|
}
|
|
const handler = MESSAGE_HANDLERS[type];
|
|
if (handler === undefined) {
|
|
this._debug && this._debug(`Inbound: Unsupported message type: ${type}`);
|
|
return;
|
|
}
|
|
|
|
return handler(this, payload);
|
|
}
|
|
|
|
function getCompatFlags(header) {
|
|
const software = header.versions.software;
|
|
|
|
let flags = 0;
|
|
|
|
for (const rule of COMPAT_CHECKS) {
|
|
if (typeof rule[0] === 'string') {
|
|
if (software === rule[0])
|
|
flags |= rule[1];
|
|
} else if (rule[0].test(software)) {
|
|
flags |= rule[1];
|
|
}
|
|
}
|
|
|
|
return flags;
|
|
}
|
|
|
|
function modesToBytes(modes) {
|
|
const keys = Object.keys(modes);
|
|
const bytes = Buffer.allocUnsafe((5 * keys.length) + 1);
|
|
let b = 0;
|
|
|
|
for (let i = 0; i < keys.length; ++i) {
|
|
const key = keys[i];
|
|
if (key === 'TTY_OP_END')
|
|
continue;
|
|
|
|
const opcode = TERMINAL_MODE[key];
|
|
if (opcode === undefined)
|
|
continue;
|
|
|
|
const val = modes[key];
|
|
if (typeof val === 'number' && isFinite(val)) {
|
|
bytes[b++] = opcode;
|
|
bytes[b++] = val >>> 24;
|
|
bytes[b++] = val >>> 16;
|
|
bytes[b++] = val >>> 8;
|
|
bytes[b++] = val;
|
|
}
|
|
}
|
|
|
|
bytes[b++] = TERMINAL_MODE.TTY_OP_END;
|
|
|
|
if (b < bytes.length)
|
|
return bufferSlice(bytes, 0, b);
|
|
|
|
return bytes;
|
|
}
|
|
|
|
function sendExtInfo(proto) {
|
|
let serverSigAlgs =
|
|
'ecdsa-sha2-nistp256,ecdsa-sha2-nistp384,ecdsa-sha2-nistp521'
|
|
+ 'rsa-sha2-512,rsa-sha2-256,ssh-rsa,ssh-dss';
|
|
if (eddsaSupported)
|
|
serverSigAlgs = `ssh-ed25519,${serverSigAlgs}`;
|
|
const algsLen = Buffer.byteLength(serverSigAlgs);
|
|
|
|
let p = proto._packetRW.write.allocStart;
|
|
const packet = proto._packetRW.write.alloc(1 + 4 + 4 + 15 + 4 + algsLen);
|
|
|
|
packet[p] = MESSAGE.EXT_INFO;
|
|
|
|
writeUInt32BE(packet, 1, ++p);
|
|
|
|
writeUInt32BE(packet, 15, p += 4);
|
|
packet.utf8Write('server-sig-algs', p += 4, 15);
|
|
|
|
writeUInt32BE(packet, algsLen, p += 15);
|
|
packet.utf8Write(serverSigAlgs, p += 4, algsLen);
|
|
|
|
proto._debug && proto._debug('Outbound: Sending EXT_INFO');
|
|
sendPacket(proto, proto._packetRW.write.finalize(packet));
|
|
}
|
|
|
|
module.exports = Protocol;
|