1150 lines
37 KiB
JavaScript
1150 lines
37 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.toString('latin1', 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.toString('utf8', 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.toString('utf8', 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 '[email protected]':
|
|
case '[email protected]': {
|
|
/*
|
|
string socket path
|
|
*/
|
|
const socketPath = bufferParser.readString(true)
|
|
if (socketPath !== undefined) data = { socketPath }
|
|
break
|
|
}
|
|
case '[email protected]':
|
|
data = null
|
|
break
|
|
case '[email protected]': {
|
|
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 '[email protected]': // S->C
|
|
case '[email protected]': {
|
|
// C->S
|
|
/*
|
|
string socket path
|
|
string reserved for future use
|
|
|
|
([email protected] 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)
|
|
// '[email protected]' (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 '[email protected]': // 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)
|
|
}
|
|
}
|