1286 lines
38 KiB
JavaScript
1286 lines
38 KiB
JavaScript
'use strict';
|
|
|
|
const {
|
|
bufferSlice,
|
|
bufferParser,
|
|
doFatalError,
|
|
sigSSHToASN1,
|
|
writeUInt32BE,
|
|
} = require('./utils.js');
|
|
|
|
const {
|
|
CHANNEL_OPEN_FAILURE,
|
|
COMPAT,
|
|
MESSAGE,
|
|
TERMINAL_MODE,
|
|
} = require('./constants.js');
|
|
|
|
const {
|
|
parseKey,
|
|
} = require('./keyParser.js');
|
|
|
|
const TERMINAL_MODE_BY_VALUE =
|
|
Array.from(Object.entries(TERMINAL_MODE))
|
|
.reduce((obj, [key, value]) => ({ ...obj, [key]: value }), {});
|
|
|
|
module.exports = {
|
|
// Transport layer protocol ==================================================
|
|
[MESSAGE.DISCONNECT]: (self, payload) => {
|
|
/*
|
|
byte SSH_MSG_DISCONNECT
|
|
uint32 reason code
|
|
string description in ISO-10646 UTF-8 encoding
|
|
string language tag
|
|
*/
|
|
bufferParser.init(payload, 1);
|
|
const reason = bufferParser.readUInt32BE();
|
|
const desc = bufferParser.readString(true);
|
|
const lang = bufferParser.readString();
|
|
bufferParser.clear();
|
|
|
|
if (lang === undefined) {
|
|
return doFatalError(
|
|
self,
|
|
'Inbound: Malformed DISCONNECT packet'
|
|
);
|
|
}
|
|
|
|
self._debug && self._debug(
|
|
`Inbound: Received DISCONNECT (${reason}, "${desc}")`
|
|
);
|
|
|
|
const handler = self._handlers.DISCONNECT;
|
|
handler && handler(self, reason, desc);
|
|
},
|
|
[MESSAGE.IGNORE]: (self, payload) => {
|
|
/*
|
|
byte SSH_MSG_IGNORE
|
|
string data
|
|
*/
|
|
self._debug && self._debug('Inbound: Received IGNORE');
|
|
},
|
|
[MESSAGE.UNIMPLEMENTED]: (self, payload) => {
|
|
/*
|
|
byte SSH_MSG_UNIMPLEMENTED
|
|
uint32 packet sequence number of rejected message
|
|
*/
|
|
bufferParser.init(payload, 1);
|
|
const seqno = bufferParser.readUInt32BE();
|
|
bufferParser.clear();
|
|
|
|
if (seqno === undefined) {
|
|
return doFatalError(
|
|
self,
|
|
'Inbound: Malformed UNIMPLEMENTED packet'
|
|
);
|
|
}
|
|
|
|
self._debug
|
|
&& self._debug(`Inbound: Received UNIMPLEMENTED (seqno ${seqno})`);
|
|
},
|
|
[MESSAGE.DEBUG]: (self, payload) => {
|
|
/*
|
|
byte SSH_MSG_DEBUG
|
|
boolean always_display
|
|
string message in ISO-10646 UTF-8 encoding [RFC3629]
|
|
string language tag [RFC3066]
|
|
*/
|
|
bufferParser.init(payload, 1);
|
|
const display = bufferParser.readBool();
|
|
const msg = bufferParser.readString(true);
|
|
const lang = bufferParser.readString();
|
|
bufferParser.clear();
|
|
|
|
if (lang === undefined) {
|
|
return doFatalError(
|
|
self,
|
|
'Inbound: Malformed DEBUG packet'
|
|
);
|
|
}
|
|
|
|
self._debug && self._debug('Inbound: Received DEBUG');
|
|
|
|
const handler = self._handlers.DEBUG;
|
|
handler && handler(self, display, msg);
|
|
},
|
|
[MESSAGE.SERVICE_REQUEST]: (self, payload) => {
|
|
/*
|
|
byte SSH_MSG_SERVICE_REQUEST
|
|
string service name
|
|
*/
|
|
bufferParser.init(payload, 1);
|
|
const name = bufferParser.readString(true);
|
|
bufferParser.clear();
|
|
|
|
if (name === undefined) {
|
|
return doFatalError(
|
|
self,
|
|
'Inbound: Malformed SERVICE_REQUEST packet'
|
|
);
|
|
}
|
|
|
|
self._debug && self._debug(`Inbound: Received SERVICE_REQUEST (${name})`);
|
|
|
|
const handler = self._handlers.SERVICE_REQUEST;
|
|
handler && handler(self, name);
|
|
},
|
|
[MESSAGE.SERVICE_ACCEPT]: (self, payload) => {
|
|
// S->C
|
|
/*
|
|
byte SSH_MSG_SERVICE_ACCEPT
|
|
string service name
|
|
*/
|
|
bufferParser.init(payload, 1);
|
|
const name = bufferParser.readString(true);
|
|
bufferParser.clear();
|
|
|
|
if (name === undefined) {
|
|
return doFatalError(
|
|
self,
|
|
'Inbound: Malformed SERVICE_ACCEPT packet'
|
|
);
|
|
}
|
|
|
|
self._debug && self._debug(`Inbound: Received SERVICE_ACCEPT (${name})`);
|
|
|
|
const handler = self._handlers.SERVICE_ACCEPT;
|
|
handler && handler(self, name);
|
|
},
|
|
[MESSAGE.EXT_INFO]: (self, payload) => {
|
|
/*
|
|
byte SSH_MSG_EXT_INFO
|
|
uint32 nr-extensions
|
|
repeat the following 2 fields "nr-extensions" times:
|
|
string extension-name
|
|
string extension-value (binary)
|
|
*/
|
|
bufferParser.init(payload, 1);
|
|
const numExts = bufferParser.readUInt32BE();
|
|
let exts;
|
|
if (numExts !== undefined) {
|
|
exts = [];
|
|
for (let i = 0; i < numExts; ++i) {
|
|
const name = bufferParser.readString(true);
|
|
const data = bufferParser.readString();
|
|
if (data !== undefined) {
|
|
switch (name) {
|
|
case 'server-sig-algs': {
|
|
const algs = data.latin1Slice(0, data.length).split(',');
|
|
exts.push({ name, algs });
|
|
continue;
|
|
}
|
|
default:
|
|
continue;
|
|
}
|
|
}
|
|
// Malformed
|
|
exts = undefined;
|
|
break;
|
|
}
|
|
}
|
|
bufferParser.clear();
|
|
|
|
if (exts === undefined)
|
|
return doFatalError(self, 'Inbound: Malformed EXT_INFO packet');
|
|
|
|
self._debug && self._debug('Inbound: Received EXT_INFO');
|
|
|
|
const handler = self._handlers.EXT_INFO;
|
|
handler && handler(self, exts);
|
|
},
|
|
|
|
// User auth protocol -- generic =============================================
|
|
[MESSAGE.USERAUTH_REQUEST]: (self, payload) => {
|
|
/*
|
|
byte SSH_MSG_USERAUTH_REQUEST
|
|
string user name in ISO-10646 UTF-8 encoding [RFC3629]
|
|
string service name in US-ASCII
|
|
string method name in US-ASCII
|
|
.... method specific fields
|
|
*/
|
|
bufferParser.init(payload, 1);
|
|
const user = bufferParser.readString(true);
|
|
const service = bufferParser.readString(true);
|
|
const method = bufferParser.readString(true);
|
|
let methodData;
|
|
let methodDesc;
|
|
switch (method) {
|
|
case 'none':
|
|
methodData = null;
|
|
break;
|
|
case 'password': {
|
|
/*
|
|
boolean <new password follows (old) plaintext password?>
|
|
string plaintext password in ISO-10646 UTF-8 encoding [RFC3629]
|
|
[string new password]
|
|
*/
|
|
const isChange = bufferParser.readBool();
|
|
if (isChange !== undefined) {
|
|
methodData = bufferParser.readString(true);
|
|
if (methodData !== undefined && isChange) {
|
|
const newPassword = bufferParser.readString(true);
|
|
if (newPassword !== undefined)
|
|
methodData = { oldPassword: methodData, newPassword };
|
|
else
|
|
methodData = undefined;
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
case 'publickey': {
|
|
/*
|
|
boolean <signature follows public key blob?>
|
|
string public key algorithm name
|
|
string public key blob
|
|
[string signature]
|
|
*/
|
|
const hasSig = bufferParser.readBool();
|
|
if (hasSig !== undefined) {
|
|
const keyAlgo = bufferParser.readString(true);
|
|
let realKeyAlgo = keyAlgo;
|
|
const key = bufferParser.readString();
|
|
|
|
let hashAlgo;
|
|
switch (keyAlgo) {
|
|
case 'rsa-sha2-256':
|
|
realKeyAlgo = 'ssh-rsa';
|
|
hashAlgo = 'sha256';
|
|
break;
|
|
case 'rsa-sha2-512':
|
|
realKeyAlgo = 'ssh-rsa';
|
|
hashAlgo = 'sha512';
|
|
break;
|
|
}
|
|
|
|
if (hasSig) {
|
|
const blobEnd = bufferParser.pos();
|
|
let signature = bufferParser.readString();
|
|
if (signature !== undefined) {
|
|
if (signature.length > (4 + keyAlgo.length + 4)
|
|
&& signature.utf8Slice(4, 4 + keyAlgo.length) === keyAlgo) {
|
|
// Skip algoLen + algo + sigLen
|
|
signature = bufferSlice(signature, 4 + keyAlgo.length + 4);
|
|
}
|
|
|
|
signature = sigSSHToASN1(signature, realKeyAlgo);
|
|
if (signature) {
|
|
const sessionID = self._kex.sessionID;
|
|
const blob = Buffer.allocUnsafe(4 + sessionID.length + blobEnd);
|
|
writeUInt32BE(blob, sessionID.length, 0);
|
|
blob.set(sessionID, 4);
|
|
blob.set(
|
|
new Uint8Array(payload.buffer, payload.byteOffset, blobEnd),
|
|
4 + sessionID.length
|
|
);
|
|
methodData = {
|
|
keyAlgo: realKeyAlgo,
|
|
key,
|
|
signature,
|
|
blob,
|
|
hashAlgo,
|
|
};
|
|
}
|
|
}
|
|
} else {
|
|
methodData = { keyAlgo: realKeyAlgo, key, hashAlgo };
|
|
methodDesc = 'publickey -- check';
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
case 'hostbased': {
|
|
/*
|
|
string public key algorithm for host key
|
|
string public host key and certificates for client host
|
|
string client host name expressed as the FQDN in US-ASCII
|
|
string user name on the client host in ISO-10646 UTF-8 encoding
|
|
[RFC3629]
|
|
string signature
|
|
*/
|
|
const keyAlgo = bufferParser.readString(true);
|
|
let realKeyAlgo = keyAlgo;
|
|
const key = bufferParser.readString();
|
|
const localHostname = bufferParser.readString(true);
|
|
const localUsername = bufferParser.readString(true);
|
|
|
|
let hashAlgo;
|
|
switch (keyAlgo) {
|
|
case 'rsa-sha2-256':
|
|
realKeyAlgo = 'ssh-rsa';
|
|
hashAlgo = 'sha256';
|
|
break;
|
|
case 'rsa-sha2-512':
|
|
realKeyAlgo = 'ssh-rsa';
|
|
hashAlgo = 'sha512';
|
|
break;
|
|
}
|
|
|
|
const blobEnd = bufferParser.pos();
|
|
let signature = bufferParser.readString();
|
|
if (signature !== undefined) {
|
|
if (signature.length > (4 + keyAlgo.length + 4)
|
|
&& signature.utf8Slice(4, 4 + keyAlgo.length) === keyAlgo) {
|
|
// Skip algoLen + algo + sigLen
|
|
signature = bufferSlice(signature, 4 + keyAlgo.length + 4);
|
|
}
|
|
|
|
signature = sigSSHToASN1(signature, realKeyAlgo);
|
|
if (signature !== undefined) {
|
|
const sessionID = self._kex.sessionID;
|
|
const blob = Buffer.allocUnsafe(4 + sessionID.length + blobEnd);
|
|
writeUInt32BE(blob, sessionID.length, 0);
|
|
blob.set(sessionID, 4);
|
|
blob.set(
|
|
new Uint8Array(payload.buffer, payload.byteOffset, blobEnd),
|
|
4 + sessionID.length
|
|
);
|
|
methodData = {
|
|
keyAlgo: realKeyAlgo,
|
|
key,
|
|
signature,
|
|
blob,
|
|
localHostname,
|
|
localUsername,
|
|
hashAlgo
|
|
};
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
case 'keyboard-interactive':
|
|
/*
|
|
string language tag (as defined in [RFC-3066])
|
|
string submethods (ISO-10646 UTF-8)
|
|
*/
|
|
// Skip/ignore language field -- it's deprecated in RFC 4256
|
|
bufferParser.skipString();
|
|
|
|
methodData = bufferParser.readList();
|
|
break;
|
|
default:
|
|
if (method !== undefined)
|
|
methodData = bufferParser.readRaw();
|
|
}
|
|
bufferParser.clear();
|
|
|
|
if (methodData === undefined) {
|
|
return doFatalError(
|
|
self,
|
|
'Inbound: Malformed USERAUTH_REQUEST packet'
|
|
);
|
|
}
|
|
|
|
if (methodDesc === undefined)
|
|
methodDesc = method;
|
|
|
|
self._authsQueue.push(method);
|
|
|
|
self._debug
|
|
&& self._debug(`Inbound: Received USERAUTH_REQUEST (${methodDesc})`);
|
|
|
|
const handler = self._handlers.USERAUTH_REQUEST;
|
|
handler && handler(self, user, service, method, methodData);
|
|
},
|
|
[MESSAGE.USERAUTH_FAILURE]: (self, payload) => {
|
|
// S->C
|
|
/*
|
|
byte SSH_MSG_USERAUTH_FAILURE
|
|
name-list authentications that can continue
|
|
boolean partial success
|
|
*/
|
|
bufferParser.init(payload, 1);
|
|
const authMethods = bufferParser.readList();
|
|
const partialSuccess = bufferParser.readBool();
|
|
bufferParser.clear();
|
|
|
|
if (partialSuccess === undefined) {
|
|
return doFatalError(
|
|
self,
|
|
'Inbound: Malformed USERAUTH_FAILURE packet'
|
|
);
|
|
}
|
|
|
|
self._debug
|
|
&& self._debug(`Inbound: Received USERAUTH_FAILURE (${authMethods})`);
|
|
|
|
self._authsQueue.shift();
|
|
const handler = self._handlers.USERAUTH_FAILURE;
|
|
handler && handler(self, authMethods, partialSuccess);
|
|
},
|
|
[MESSAGE.USERAUTH_SUCCESS]: (self, payload) => {
|
|
// S->C
|
|
/*
|
|
byte SSH_MSG_USERAUTH_SUCCESS
|
|
*/
|
|
self._debug && self._debug('Inbound: Received USERAUTH_SUCCESS');
|
|
|
|
self._authsQueue.shift();
|
|
const handler = self._handlers.USERAUTH_SUCCESS;
|
|
handler && handler(self);
|
|
},
|
|
[MESSAGE.USERAUTH_BANNER]: (self, payload) => {
|
|
// S->C
|
|
/*
|
|
byte SSH_MSG_USERAUTH_BANNER
|
|
string message in ISO-10646 UTF-8 encoding [RFC3629]
|
|
string language tag [RFC3066]
|
|
*/
|
|
bufferParser.init(payload, 1);
|
|
const msg = bufferParser.readString(true);
|
|
const lang = bufferParser.readString();
|
|
bufferParser.clear();
|
|
|
|
if (lang === undefined) {
|
|
return doFatalError(
|
|
self,
|
|
'Inbound: Malformed USERAUTH_BANNER packet'
|
|
);
|
|
}
|
|
|
|
self._debug && self._debug('Inbound: Received USERAUTH_BANNER');
|
|
|
|
const handler = self._handlers.USERAUTH_BANNER;
|
|
handler && handler(self, msg);
|
|
},
|
|
|
|
// User auth protocol -- method-specific =====================================
|
|
60: (self, payload) => {
|
|
if (!self._authsQueue.length) {
|
|
self._debug
|
|
&& self._debug('Inbound: Received payload type 60 without auth');
|
|
return;
|
|
}
|
|
|
|
switch (self._authsQueue[0]) {
|
|
case 'password': {
|
|
// S->C
|
|
/*
|
|
byte SSH_MSG_USERAUTH_PASSWD_CHANGEREQ
|
|
string prompt in ISO-10646 UTF-8 encoding [RFC3629]
|
|
string language tag [RFC3066]
|
|
*/
|
|
bufferParser.init(payload, 1);
|
|
const prompt = bufferParser.readString(true);
|
|
const lang = bufferParser.readString();
|
|
bufferParser.clear();
|
|
|
|
if (lang === undefined) {
|
|
return doFatalError(
|
|
self,
|
|
'Inbound: Malformed USERAUTH_PASSWD_CHANGEREQ packet'
|
|
);
|
|
}
|
|
|
|
self._debug
|
|
&& self._debug('Inbound: Received USERAUTH_PASSWD_CHANGEREQ');
|
|
|
|
const handler = self._handlers.USERAUTH_PASSWD_CHANGEREQ;
|
|
handler && handler(self, prompt);
|
|
break;
|
|
}
|
|
case 'publickey': {
|
|
// S->C
|
|
/*
|
|
byte SSH_MSG_USERAUTH_PK_OK
|
|
string public key algorithm name from the request
|
|
string public key blob from the request
|
|
*/
|
|
bufferParser.init(payload, 1);
|
|
const keyAlgo = bufferParser.readString(true);
|
|
const key = bufferParser.readString();
|
|
bufferParser.clear();
|
|
|
|
if (key === undefined) {
|
|
return doFatalError(
|
|
self,
|
|
'Inbound: Malformed USERAUTH_PK_OK packet'
|
|
);
|
|
}
|
|
|
|
self._debug && self._debug('Inbound: Received USERAUTH_PK_OK');
|
|
|
|
self._authsQueue.shift();
|
|
const handler = self._handlers.USERAUTH_PK_OK;
|
|
handler && handler(self, keyAlgo, key);
|
|
break;
|
|
}
|
|
case 'keyboard-interactive': {
|
|
// S->C
|
|
/*
|
|
byte SSH_MSG_USERAUTH_INFO_REQUEST
|
|
string name (ISO-10646 UTF-8)
|
|
string instruction (ISO-10646 UTF-8)
|
|
string language tag (as defined in [RFC-3066])
|
|
int num-prompts
|
|
string prompt[1] (ISO-10646 UTF-8)
|
|
boolean echo[1]
|
|
...
|
|
string prompt[num-prompts] (ISO-10646 UTF-8)
|
|
boolean echo[num-prompts]
|
|
*/
|
|
bufferParser.init(payload, 1);
|
|
const name = bufferParser.readString(true);
|
|
const instructions = bufferParser.readString(true);
|
|
bufferParser.readString(); // skip lang
|
|
const numPrompts = bufferParser.readUInt32BE();
|
|
let prompts;
|
|
if (numPrompts !== undefined) {
|
|
prompts = new Array(numPrompts);
|
|
let i;
|
|
for (i = 0; i < numPrompts; ++i) {
|
|
const prompt = bufferParser.readString(true);
|
|
const echo = bufferParser.readBool();
|
|
if (echo === undefined)
|
|
break;
|
|
prompts[i] = { prompt, echo };
|
|
}
|
|
if (i !== numPrompts)
|
|
prompts = undefined;
|
|
}
|
|
bufferParser.clear();
|
|
|
|
if (prompts === undefined) {
|
|
return doFatalError(
|
|
self,
|
|
'Inbound: Malformed USERAUTH_INFO_REQUEST packet'
|
|
);
|
|
}
|
|
|
|
self._debug && self._debug('Inbound: Received USERAUTH_INFO_REQUEST');
|
|
|
|
const handler = self._handlers.USERAUTH_INFO_REQUEST;
|
|
handler && handler(self, name, instructions, prompts);
|
|
break;
|
|
}
|
|
default:
|
|
self._debug
|
|
&& self._debug('Inbound: Received unexpected payload type 60');
|
|
}
|
|
},
|
|
61: (self, payload) => {
|
|
if (!self._authsQueue.length) {
|
|
self._debug
|
|
&& self._debug('Inbound: Received payload type 61 without auth');
|
|
return;
|
|
}
|
|
/*
|
|
byte SSH_MSG_USERAUTH_INFO_RESPONSE
|
|
int num-responses
|
|
string response[1] (ISO-10646 UTF-8)
|
|
...
|
|
string response[num-responses] (ISO-10646 UTF-8)
|
|
*/
|
|
if (self._authsQueue[0] !== 'keyboard-interactive') {
|
|
return doFatalError(
|
|
self,
|
|
'Inbound: Received unexpected payload type 61'
|
|
);
|
|
}
|
|
bufferParser.init(payload, 1);
|
|
const numResponses = bufferParser.readUInt32BE();
|
|
let responses;
|
|
if (numResponses !== undefined) {
|
|
responses = new Array(numResponses);
|
|
let i;
|
|
for (i = 0; i < numResponses; ++i) {
|
|
const response = bufferParser.readString(true);
|
|
if (response === undefined)
|
|
break;
|
|
responses[i] = response;
|
|
}
|
|
if (i !== numResponses)
|
|
responses = undefined;
|
|
}
|
|
bufferParser.clear();
|
|
|
|
if (responses === undefined) {
|
|
return doFatalError(
|
|
self,
|
|
'Inbound: Malformed USERAUTH_INFO_RESPONSE packet'
|
|
);
|
|
}
|
|
|
|
self._debug && self._debug('Inbound: Received USERAUTH_INFO_RESPONSE');
|
|
|
|
const handler = self._handlers.USERAUTH_INFO_RESPONSE;
|
|
handler && handler(self, responses);
|
|
},
|
|
|
|
// Connection protocol -- generic ============================================
|
|
[MESSAGE.GLOBAL_REQUEST]: (self, payload) => {
|
|
/*
|
|
byte SSH_MSG_GLOBAL_REQUEST
|
|
string request name in US-ASCII only
|
|
boolean want reply
|
|
.... request-specific data follows
|
|
*/
|
|
bufferParser.init(payload, 1);
|
|
const name = bufferParser.readString(true);
|
|
const wantReply = bufferParser.readBool();
|
|
let data;
|
|
if (wantReply !== undefined) {
|
|
switch (name) {
|
|
case 'tcpip-forward':
|
|
case 'cancel-tcpip-forward': {
|
|
/*
|
|
string address to bind (e.g., "0.0.0.0")
|
|
uint32 port number to bind
|
|
*/
|
|
const bindAddr = bufferParser.readString(true);
|
|
const bindPort = bufferParser.readUInt32BE();
|
|
if (bindPort !== undefined)
|
|
data = { bindAddr, bindPort };
|
|
break;
|
|
}
|
|
case 'streamlocal-forward@openssh.com':
|
|
case 'cancel-streamlocal-forward@openssh.com': {
|
|
/*
|
|
string socket path
|
|
*/
|
|
const socketPath = bufferParser.readString(true);
|
|
if (socketPath !== undefined)
|
|
data = { socketPath };
|
|
break;
|
|
}
|
|
case 'no-more-sessions@openssh.com':
|
|
data = null;
|
|
break;
|
|
case 'hostkeys-00@openssh.com': {
|
|
data = [];
|
|
while (bufferParser.avail() > 0) {
|
|
const keyRaw = bufferParser.readString();
|
|
if (keyRaw === undefined) {
|
|
data = undefined;
|
|
break;
|
|
}
|
|
const key = parseKey(keyRaw);
|
|
if (!(key instanceof Error))
|
|
data.push(key);
|
|
}
|
|
break;
|
|
}
|
|
default:
|
|
data = bufferParser.readRaw();
|
|
}
|
|
}
|
|
bufferParser.clear();
|
|
|
|
if (data === undefined) {
|
|
return doFatalError(
|
|
self,
|
|
'Inbound: Malformed GLOBAL_REQUEST packet'
|
|
);
|
|
}
|
|
|
|
self._debug && self._debug(`Inbound: GLOBAL_REQUEST (${name})`);
|
|
|
|
const handler = self._handlers.GLOBAL_REQUEST;
|
|
if (handler)
|
|
handler(self, name, wantReply, data);
|
|
else
|
|
self.requestFailure(); // Auto reject
|
|
},
|
|
[MESSAGE.REQUEST_SUCCESS]: (self, payload) => {
|
|
/*
|
|
byte SSH_MSG_REQUEST_SUCCESS
|
|
.... response specific data
|
|
*/
|
|
const data = (payload.length > 1 ? bufferSlice(payload, 1) : null);
|
|
|
|
self._debug && self._debug('Inbound: REQUEST_SUCCESS');
|
|
|
|
const handler = self._handlers.REQUEST_SUCCESS;
|
|
handler && handler(self, data);
|
|
},
|
|
[MESSAGE.REQUEST_FAILURE]: (self, payload) => {
|
|
/*
|
|
byte SSH_MSG_REQUEST_FAILURE
|
|
*/
|
|
self._debug && self._debug('Inbound: Received REQUEST_FAILURE');
|
|
|
|
const handler = self._handlers.REQUEST_FAILURE;
|
|
handler && handler(self);
|
|
},
|
|
|
|
// Connection protocol -- channel-related ====================================
|
|
[MESSAGE.CHANNEL_OPEN]: (self, payload) => {
|
|
/*
|
|
byte SSH_MSG_CHANNEL_OPEN
|
|
string channel type in US-ASCII only
|
|
uint32 sender channel
|
|
uint32 initial window size
|
|
uint32 maximum packet size
|
|
.... channel type specific data follows
|
|
*/
|
|
bufferParser.init(payload, 1);
|
|
const type = bufferParser.readString(true);
|
|
const sender = bufferParser.readUInt32BE();
|
|
const window = bufferParser.readUInt32BE();
|
|
const packetSize = bufferParser.readUInt32BE();
|
|
let channelInfo;
|
|
|
|
switch (type) {
|
|
case 'forwarded-tcpip': // S->C
|
|
case 'direct-tcpip': { // C->S
|
|
/*
|
|
string address that was connected / host to connect
|
|
uint32 port that was connected / port to connect
|
|
string originator IP address
|
|
uint32 originator port
|
|
*/
|
|
const destIP = bufferParser.readString(true);
|
|
const destPort = bufferParser.readUInt32BE();
|
|
const srcIP = bufferParser.readString(true);
|
|
const srcPort = bufferParser.readUInt32BE();
|
|
if (srcPort !== undefined) {
|
|
channelInfo = {
|
|
type,
|
|
sender,
|
|
window,
|
|
packetSize,
|
|
data: { destIP, destPort, srcIP, srcPort }
|
|
};
|
|
}
|
|
break;
|
|
}
|
|
case 'forwarded-streamlocal@openssh.com': // S->C
|
|
case 'direct-streamlocal@openssh.com': { // C->S
|
|
/*
|
|
string socket path
|
|
string reserved for future use
|
|
|
|
(direct-streamlocal@openssh.com additionally has:)
|
|
uint32 reserved
|
|
*/
|
|
const socketPath = bufferParser.readString(true);
|
|
if (socketPath !== undefined) {
|
|
channelInfo = {
|
|
type,
|
|
sender,
|
|
window,
|
|
packetSize,
|
|
data: { socketPath }
|
|
};
|
|
}
|
|
break;
|
|
}
|
|
case 'x11': { // S->C
|
|
/*
|
|
string originator address (e.g., "192.168.7.38")
|
|
uint32 originator port
|
|
*/
|
|
const srcIP = bufferParser.readString(true);
|
|
const srcPort = bufferParser.readUInt32BE();
|
|
if (srcPort !== undefined) {
|
|
channelInfo = {
|
|
type,
|
|
sender,
|
|
window,
|
|
packetSize,
|
|
data: { srcIP, srcPort }
|
|
};
|
|
}
|
|
break;
|
|
}
|
|
default:
|
|
// Includes:
|
|
// 'session' (C->S)
|
|
// 'auth-agent@openssh.com' (S->C)
|
|
channelInfo = {
|
|
type,
|
|
sender,
|
|
window,
|
|
packetSize,
|
|
data: {}
|
|
};
|
|
}
|
|
bufferParser.clear();
|
|
|
|
if (channelInfo === undefined) {
|
|
return doFatalError(
|
|
self,
|
|
'Inbound: Malformed CHANNEL_OPEN packet'
|
|
);
|
|
}
|
|
|
|
self._debug && self._debug(`Inbound: CHANNEL_OPEN (s:${sender}, ${type})`);
|
|
|
|
const handler = self._handlers.CHANNEL_OPEN;
|
|
if (handler) {
|
|
handler(self, channelInfo);
|
|
} else {
|
|
self.channelOpenFail(
|
|
channelInfo.sender,
|
|
CHANNEL_OPEN_FAILURE.ADMINISTRATIVELY_PROHIBITED,
|
|
'',
|
|
''
|
|
);
|
|
}
|
|
},
|
|
[MESSAGE.CHANNEL_OPEN_CONFIRMATION]: (self, payload) => {
|
|
/*
|
|
byte SSH_MSG_CHANNEL_OPEN_CONFIRMATION
|
|
uint32 recipient channel
|
|
uint32 sender channel
|
|
uint32 initial window size
|
|
uint32 maximum packet size
|
|
.... channel type specific data follows
|
|
*/
|
|
// "The 'recipient channel' is the channel number given in the
|
|
// original open request, and 'sender channel' is the channel number
|
|
// allocated by the other side."
|
|
bufferParser.init(payload, 1);
|
|
const recipient = bufferParser.readUInt32BE();
|
|
const sender = bufferParser.readUInt32BE();
|
|
const window = bufferParser.readUInt32BE();
|
|
const packetSize = bufferParser.readUInt32BE();
|
|
const data = (bufferParser.avail() ? bufferParser.readRaw() : undefined);
|
|
bufferParser.clear();
|
|
|
|
if (packetSize === undefined) {
|
|
return doFatalError(
|
|
self,
|
|
'Inbound: Malformed CHANNEL_OPEN_CONFIRMATION packet'
|
|
);
|
|
}
|
|
|
|
self._debug && self._debug(
|
|
`Inbound: CHANNEL_OPEN_CONFIRMATION (r:${recipient}, s:${sender})`
|
|
);
|
|
|
|
const handler = self._handlers.CHANNEL_OPEN_CONFIRMATION;
|
|
if (handler)
|
|
handler(self, { recipient, sender, window, packetSize, data });
|
|
},
|
|
[MESSAGE.CHANNEL_OPEN_FAILURE]: (self, payload) => {
|
|
/*
|
|
byte SSH_MSG_CHANNEL_OPEN_FAILURE
|
|
uint32 recipient channel
|
|
uint32 reason code
|
|
string description in ISO-10646 UTF-8 encoding [RFC3629]
|
|
string language tag [RFC3066]
|
|
*/
|
|
bufferParser.init(payload, 1);
|
|
const recipient = bufferParser.readUInt32BE();
|
|
const reason = bufferParser.readUInt32BE();
|
|
const description = bufferParser.readString(true);
|
|
const lang = bufferParser.readString();
|
|
bufferParser.clear();
|
|
|
|
if (lang === undefined) {
|
|
return doFatalError(
|
|
self,
|
|
'Inbound: Malformed CHANNEL_OPEN_FAILURE packet'
|
|
);
|
|
}
|
|
|
|
self._debug
|
|
&& self._debug(`Inbound: CHANNEL_OPEN_FAILURE (r:${recipient})`);
|
|
|
|
const handler = self._handlers.CHANNEL_OPEN_FAILURE;
|
|
handler && handler(self, recipient, reason, description);
|
|
},
|
|
[MESSAGE.CHANNEL_WINDOW_ADJUST]: (self, payload) => {
|
|
/*
|
|
byte SSH_MSG_CHANNEL_WINDOW_ADJUST
|
|
uint32 recipient channel
|
|
uint32 bytes to add
|
|
*/
|
|
bufferParser.init(payload, 1);
|
|
const recipient = bufferParser.readUInt32BE();
|
|
const bytesToAdd = bufferParser.readUInt32BE();
|
|
bufferParser.clear();
|
|
|
|
if (bytesToAdd === undefined) {
|
|
return doFatalError(
|
|
self,
|
|
'Inbound: Malformed CHANNEL_WINDOW_ADJUST packet'
|
|
);
|
|
}
|
|
|
|
self._debug && self._debug(
|
|
`Inbound: CHANNEL_WINDOW_ADJUST (r:${recipient}, ${bytesToAdd})`
|
|
);
|
|
|
|
const handler = self._handlers.CHANNEL_WINDOW_ADJUST;
|
|
handler && handler(self, recipient, bytesToAdd);
|
|
},
|
|
[MESSAGE.CHANNEL_DATA]: (self, payload) => {
|
|
/*
|
|
byte SSH_MSG_CHANNEL_DATA
|
|
uint32 recipient channel
|
|
string data
|
|
*/
|
|
bufferParser.init(payload, 1);
|
|
const recipient = bufferParser.readUInt32BE();
|
|
const data = bufferParser.readString();
|
|
bufferParser.clear();
|
|
|
|
if (data === undefined) {
|
|
return doFatalError(
|
|
self,
|
|
'Inbound: Malformed CHANNEL_DATA packet'
|
|
);
|
|
}
|
|
|
|
self._debug
|
|
&& self._debug(`Inbound: CHANNEL_DATA (r:${recipient}, ${data.length})`);
|
|
|
|
const handler = self._handlers.CHANNEL_DATA;
|
|
handler && handler(self, recipient, data);
|
|
},
|
|
[MESSAGE.CHANNEL_EXTENDED_DATA]: (self, payload) => {
|
|
/*
|
|
byte SSH_MSG_CHANNEL_EXTENDED_DATA
|
|
uint32 recipient channel
|
|
uint32 data_type_code
|
|
string data
|
|
*/
|
|
bufferParser.init(payload, 1);
|
|
const recipient = bufferParser.readUInt32BE();
|
|
const type = bufferParser.readUInt32BE();
|
|
const data = bufferParser.readString();
|
|
bufferParser.clear();
|
|
|
|
if (data === undefined) {
|
|
return doFatalError(
|
|
self,
|
|
'Inbound: Malformed CHANNEL_EXTENDED_DATA packet'
|
|
);
|
|
}
|
|
|
|
self._debug && self._debug(
|
|
`Inbound: CHANNEL_EXTENDED_DATA (r:${recipient}, ${data.length})`
|
|
);
|
|
|
|
const handler = self._handlers.CHANNEL_EXTENDED_DATA;
|
|
handler && handler(self, recipient, data, type);
|
|
},
|
|
[MESSAGE.CHANNEL_EOF]: (self, payload) => {
|
|
/*
|
|
byte SSH_MSG_CHANNEL_EOF
|
|
uint32 recipient channel
|
|
*/
|
|
bufferParser.init(payload, 1);
|
|
const recipient = bufferParser.readUInt32BE();
|
|
bufferParser.clear();
|
|
|
|
if (recipient === undefined) {
|
|
return doFatalError(
|
|
self,
|
|
'Inbound: Malformed CHANNEL_EOF packet'
|
|
);
|
|
}
|
|
|
|
self._debug && self._debug(`Inbound: CHANNEL_EOF (r:${recipient})`);
|
|
|
|
const handler = self._handlers.CHANNEL_EOF;
|
|
handler && handler(self, recipient);
|
|
},
|
|
[MESSAGE.CHANNEL_CLOSE]: (self, payload) => {
|
|
/*
|
|
byte SSH_MSG_CHANNEL_CLOSE
|
|
uint32 recipient channel
|
|
*/
|
|
bufferParser.init(payload, 1);
|
|
const recipient = bufferParser.readUInt32BE();
|
|
bufferParser.clear();
|
|
|
|
if (recipient === undefined) {
|
|
return doFatalError(
|
|
self,
|
|
'Inbound: Malformed CHANNEL_CLOSE packet'
|
|
);
|
|
}
|
|
|
|
self._debug && self._debug(`Inbound: CHANNEL_CLOSE (r:${recipient})`);
|
|
|
|
const handler = self._handlers.CHANNEL_CLOSE;
|
|
handler && handler(self, recipient);
|
|
},
|
|
[MESSAGE.CHANNEL_REQUEST]: (self, payload) => {
|
|
/*
|
|
byte SSH_MSG_CHANNEL_REQUEST
|
|
uint32 recipient channel
|
|
string request type in US-ASCII characters only
|
|
boolean want reply
|
|
.... type-specific data follows
|
|
*/
|
|
bufferParser.init(payload, 1);
|
|
const recipient = bufferParser.readUInt32BE();
|
|
const type = bufferParser.readString(true);
|
|
const wantReply = bufferParser.readBool();
|
|
let data;
|
|
if (wantReply !== undefined) {
|
|
switch (type) {
|
|
case 'exit-status': // S->C
|
|
/*
|
|
uint32 exit_status
|
|
*/
|
|
data = bufferParser.readUInt32BE();
|
|
self._debug && self._debug(
|
|
`Inbound: CHANNEL_REQUEST (r:${recipient}, ${type}: ${data})`
|
|
);
|
|
break;
|
|
case 'exit-signal': { // S->C
|
|
/*
|
|
string signal name (without the "SIG" prefix)
|
|
boolean core dumped
|
|
string error message in ISO-10646 UTF-8 encoding
|
|
string language tag
|
|
*/
|
|
let signal;
|
|
let coreDumped;
|
|
if (self._compatFlags & COMPAT.OLD_EXIT) {
|
|
/*
|
|
Instead of `signal name` and `core dumped`, we have just:
|
|
uint32 signal number
|
|
*/
|
|
const num = bufferParser.readUInt32BE();
|
|
switch (num) {
|
|
case 1:
|
|
signal = 'HUP';
|
|
break;
|
|
case 2:
|
|
signal = 'INT';
|
|
break;
|
|
case 3:
|
|
signal = 'QUIT';
|
|
break;
|
|
case 6:
|
|
signal = 'ABRT';
|
|
break;
|
|
case 9:
|
|
signal = 'KILL';
|
|
break;
|
|
case 14:
|
|
signal = 'ALRM';
|
|
break;
|
|
case 15:
|
|
signal = 'TERM';
|
|
break;
|
|
default:
|
|
if (num !== undefined) {
|
|
// Unknown or OS-specific
|
|
signal = `UNKNOWN (${num})`;
|
|
}
|
|
}
|
|
coreDumped = false;
|
|
} else {
|
|
signal = bufferParser.readString(true);
|
|
coreDumped = bufferParser.readBool();
|
|
if (coreDumped === undefined)
|
|
signal = undefined;
|
|
}
|
|
const errorMessage = bufferParser.readString(true);
|
|
if (bufferParser.skipString() !== undefined)
|
|
data = { signal, coreDumped, errorMessage };
|
|
self._debug && self._debug(
|
|
`Inbound: CHANNEL_REQUEST (r:${recipient}, ${type}: ${signal})`
|
|
);
|
|
break;
|
|
}
|
|
case 'pty-req': { // C->S
|
|
/*
|
|
string TERM environment variable value (e.g., vt100)
|
|
uint32 terminal width, characters (e.g., 80)
|
|
uint32 terminal height, rows (e.g., 24)
|
|
uint32 terminal width, pixels (e.g., 640)
|
|
uint32 terminal height, pixels (e.g., 480)
|
|
string encoded terminal modes
|
|
*/
|
|
const term = bufferParser.readString(true);
|
|
const cols = bufferParser.readUInt32BE();
|
|
const rows = bufferParser.readUInt32BE();
|
|
const width = bufferParser.readUInt32BE();
|
|
const height = bufferParser.readUInt32BE();
|
|
const modesBinary = bufferParser.readString();
|
|
if (modesBinary !== undefined) {
|
|
bufferParser.init(modesBinary, 1);
|
|
let modes = {};
|
|
while (bufferParser.avail()) {
|
|
const opcode = bufferParser.readByte();
|
|
if (opcode === TERMINAL_MODE.TTY_OP_END)
|
|
break;
|
|
const name = TERMINAL_MODE_BY_VALUE[opcode];
|
|
const value = bufferParser.readUInt32BE();
|
|
if (opcode === undefined
|
|
|| name === undefined
|
|
|| value === undefined) {
|
|
modes = undefined;
|
|
break;
|
|
}
|
|
modes[name] = value;
|
|
}
|
|
if (modes !== undefined)
|
|
data = { term, cols, rows, width, height, modes };
|
|
}
|
|
self._debug && self._debug(
|
|
`Inbound: CHANNEL_REQUEST (r:${recipient}, ${type})`
|
|
);
|
|
break;
|
|
}
|
|
case 'window-change': { // C->S
|
|
/*
|
|
uint32 terminal width, columns
|
|
uint32 terminal height, rows
|
|
uint32 terminal width, pixels
|
|
uint32 terminal height, pixels
|
|
*/
|
|
const cols = bufferParser.readUInt32BE();
|
|
const rows = bufferParser.readUInt32BE();
|
|
const width = bufferParser.readUInt32BE();
|
|
const height = bufferParser.readUInt32BE();
|
|
if (height !== undefined)
|
|
data = { cols, rows, width, height };
|
|
self._debug && self._debug(
|
|
`Inbound: CHANNEL_REQUEST (r:${recipient}, ${type})`
|
|
);
|
|
break;
|
|
}
|
|
case 'x11-req': { // C->S
|
|
/*
|
|
boolean single connection
|
|
string x11 authentication protocol
|
|
string x11 authentication cookie
|
|
uint32 x11 screen number
|
|
*/
|
|
const single = bufferParser.readBool();
|
|
const protocol = bufferParser.readString(true);
|
|
const cookie = bufferParser.readString();
|
|
const screen = bufferParser.readUInt32BE();
|
|
if (screen !== undefined)
|
|
data = { single, protocol, cookie, screen };
|
|
self._debug && self._debug(
|
|
`Inbound: CHANNEL_REQUEST (r:${recipient}, ${type})`
|
|
);
|
|
break;
|
|
}
|
|
case 'env': { // C->S
|
|
/*
|
|
string variable name
|
|
string variable value
|
|
*/
|
|
const name = bufferParser.readString(true);
|
|
const value = bufferParser.readString(true);
|
|
if (value !== undefined)
|
|
data = { name, value };
|
|
if (self._debug) {
|
|
self._debug(
|
|
`Inbound: CHANNEL_REQUEST (r:${recipient}, ${type}: `
|
|
+ `${name}=${value})`
|
|
);
|
|
}
|
|
break;
|
|
}
|
|
case 'shell': // C->S
|
|
data = null; // No extra data
|
|
self._debug && self._debug(
|
|
`Inbound: CHANNEL_REQUEST (r:${recipient}, ${type})`
|
|
);
|
|
break;
|
|
case 'exec': // C->S
|
|
/*
|
|
string command
|
|
*/
|
|
data = bufferParser.readString(true);
|
|
self._debug && self._debug(
|
|
`Inbound: CHANNEL_REQUEST (r:${recipient}, ${type}: ${data})`
|
|
);
|
|
break;
|
|
case 'subsystem': // C->S
|
|
/*
|
|
string subsystem name
|
|
*/
|
|
data = bufferParser.readString(true);
|
|
self._debug && self._debug(
|
|
`Inbound: CHANNEL_REQUEST (r:${recipient}, ${type}: ${data})`
|
|
);
|
|
break;
|
|
case 'signal': // C->S
|
|
/*
|
|
string signal name (without the "SIG" prefix)
|
|
*/
|
|
data = bufferParser.readString(true);
|
|
self._debug && self._debug(
|
|
`Inbound: CHANNEL_REQUEST (r:${recipient}, ${type}: ${data})`
|
|
);
|
|
break;
|
|
case 'xon-xoff': // C->S
|
|
/*
|
|
boolean client can do
|
|
*/
|
|
data = bufferParser.readBool();
|
|
self._debug && self._debug(
|
|
`Inbound: CHANNEL_REQUEST (r:${recipient}, ${type}: ${data})`
|
|
);
|
|
break;
|
|
case 'auth-agent-req@openssh.com': // C-S
|
|
data = null; // No extra data
|
|
self._debug && self._debug(
|
|
`Inbound: CHANNEL_REQUEST (r:${recipient}, ${type})`
|
|
);
|
|
break;
|
|
default:
|
|
data = (bufferParser.avail() ? bufferParser.readRaw() : null);
|
|
self._debug && self._debug(
|
|
`Inbound: CHANNEL_REQUEST (r:${recipient}, ${type})`
|
|
);
|
|
}
|
|
}
|
|
bufferParser.clear();
|
|
|
|
if (data === undefined) {
|
|
return doFatalError(
|
|
self,
|
|
'Inbound: Malformed CHANNEL_REQUEST packet'
|
|
);
|
|
}
|
|
|
|
const handler = self._handlers.CHANNEL_REQUEST;
|
|
handler && handler(self, recipient, type, wantReply, data);
|
|
},
|
|
[MESSAGE.CHANNEL_SUCCESS]: (self, payload) => {
|
|
/*
|
|
byte SSH_MSG_CHANNEL_SUCCESS
|
|
uint32 recipient channel
|
|
*/
|
|
bufferParser.init(payload, 1);
|
|
const recipient = bufferParser.readUInt32BE();
|
|
bufferParser.clear();
|
|
|
|
if (recipient === undefined) {
|
|
return doFatalError(
|
|
self,
|
|
'Inbound: Malformed CHANNEL_SUCCESS packet'
|
|
);
|
|
}
|
|
|
|
self._debug && self._debug(`Inbound: CHANNEL_SUCCESS (r:${recipient})`);
|
|
|
|
const handler = self._handlers.CHANNEL_SUCCESS;
|
|
handler && handler(self, recipient);
|
|
},
|
|
[MESSAGE.CHANNEL_FAILURE]: (self, payload) => {
|
|
/*
|
|
byte SSH_MSG_CHANNEL_FAILURE
|
|
uint32 recipient channel
|
|
*/
|
|
bufferParser.init(payload, 1);
|
|
const recipient = bufferParser.readUInt32BE();
|
|
bufferParser.clear();
|
|
|
|
if (recipient === undefined) {
|
|
return doFatalError(
|
|
self,
|
|
'Inbound: Malformed CHANNEL_FAILURE packet'
|
|
);
|
|
}
|
|
|
|
self._debug && self._debug(`Inbound: CHANNEL_FAILURE (r:${recipient})`);
|
|
|
|
const handler = self._handlers.CHANNEL_FAILURE;
|
|
handler && handler(self, recipient);
|
|
},
|
|
};
|