From 4606d0e127e3d27ecde1850a2649f33eef9970ab Mon Sep 17 00:00:00 2001 From: Brian White Date: Mon, 23 Aug 2021 00:02:55 -0400 Subject: [PATCH] SFTP: increase max packet length, add missing OpenSSH extensions --- README.md | 2 +- lib/protocol/SFTP.js | 187 +++++++++++++++++++++++++++++++++++++++++-- test/test-sftp.js | 4 +- 3 files changed, 183 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index 76a75be3..e39ec733 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ SSH2 client and server modules written in pure JavaScript for [node.js](http://nodejs.org/). -Development/testing is done against OpenSSH (8.0 currently). +Development/testing is done against OpenSSH (8.7 currently). Changes (breaking or otherwise) in v1.0.0 can be found [here](https://github.com/mscdex/ssh2/issues/935). diff --git a/lib/protocol/SFTP.js b/lib/protocol/SFTP.js index c01da159..b10d9e53 100644 --- a/lib/protocol/SFTP.js +++ b/lib/protocol/SFTP.js @@ -154,7 +154,14 @@ class SFTP extends EventEmitter { this._pktData = undefined; this._writeReqid = -1; this._requests = {}; - this._maxPktLen = (this._isOpenSSH ? OPENSSH_MAX_PKT_LEN : 34000); + this._maxInPktLen = OPENSSH_MAX_PKT_LEN; + this._maxOutPktLen = 34000; + this._maxReadLen = + (this._isOpenSSH ? OPENSSH_MAX_PKT_LEN : 34000) - PKT_RW_OVERHEAD; + this._maxWriteLen = + (this._isOpenSSH ? OPENSSH_MAX_PKT_LEN : 34000) - PKT_RW_OVERHEAD; + + this.maxOpenHandles = undefined; // Channel compatibility this._client = client; @@ -208,8 +215,8 @@ class SFTP extends EventEmitter { return; if (this._pktLen === 0) return doFatalSFTPError(this, 'Invalid packet length'); - if (this._pktLen > this._maxPktLen) { - const max = this._maxPktLen; + if (this._pktLen > this._maxInPktLen) { + const max = this._maxInPktLen; return doFatalSFTPError( this, `Packet length ${this._pktLen} exceeds max length of ${max}` @@ -432,7 +439,7 @@ class SFTP extends EventEmitter { return; } - const maxDataLen = this._maxPktLen - PKT_RW_OVERHEAD; + const maxDataLen = this._maxWriteLen; const overflow = Math.max(len - maxDataLen, 0); const origPosition = position; @@ -1421,7 +1428,7 @@ class SFTP extends EventEmitter { throw new Error('Client-only method called in server mode'); const ext = this._extensions['hardlink@openssh.com']; - if (!ext || ext.indexOf('1') === -1) + if (ext !== '1') throw new Error('Server does not support this extended request'); /* @@ -1461,7 +1468,7 @@ class SFTP extends EventEmitter { throw new Error('Client-only method called in server mode'); const ext = this._extensions['fsync@openssh.com']; - if (!ext || ext.indexOf('1') === -1) + if (ext !== '1') throw new Error('Server does not support this extended request'); if (!Buffer.isBuffer(handle)) throw new Error('handle is not a Buffer'); @@ -1492,6 +1499,103 @@ class SFTP extends EventEmitter { `SFTP: Outbound: ${isBuffered ? 'Buffered' : 'Sending'} fsync@openssh.com` ); } + ext_openssh_lsetstat(path, attrs, cb) { + if (this.server) + throw new Error('Client-only method called in server mode'); + + const ext = this._extensions['lsetstat@openssh.com']; + if (ext !== '1') + throw new Error('Server does not support this extended request'); + + let flags = 0; + let attrsLen = 0; + + if (typeof attrs === 'object' && attrs !== null) { + attrs = attrsToBytes(attrs); + flags = attrs.flags; + attrsLen = attrs.nb; + } else if (typeof attrs === 'function') { + cb = attrs; + } + + /* + uint32 id + string "lsetstat@openssh.com" + string path + ATTRS attrs + */ + const pathLen = Buffer.byteLength(path); + let p = 9; + const buf = + Buffer.allocUnsafe(4 + 1 + 4 + 4 + 20 + 4 + pathLen + 4 + attrsLen); + + writeUInt32BE(buf, buf.length - 4, 0); + buf[4] = REQUEST.EXTENDED; + const reqid = this._writeReqid = (this._writeReqid + 1) & MAX_REQID; + writeUInt32BE(buf, reqid, 5); + + writeUInt32BE(buf, 20, p); + buf.utf8Write('lsetstat@openssh.com', p += 4, 20); + + writeUInt32BE(buf, pathLen, p += 20); + buf.utf8Write(path, p += 4, pathLen); + + writeUInt32BE(buf, flags, p += pathLen); + if (attrsLen) { + p += 4; + + if (attrsLen === ATTRS_BUF.length) + buf.set(ATTRS_BUF, p); + else + bufferCopy(ATTRS_BUF, buf, 0, attrsLen, p); + + p += attrsLen; + } + + this._requests[reqid] = { cb }; + + const isBuffered = sendOrBuffer(this, buf); + if (this._debug) { + const status = (isBuffered ? 'Buffered' : 'Sending'); + this._debug(`SFTP: Outbound: ${status} lsetstat@openssh.com`); + } + } + ext_openssh_expandPath(path, cb) { + if (this.server) + throw new Error('Client-only method called in server mode'); + + const ext = this._extensions['expand-path@openssh.com']; + if (ext !== '1') + throw new Error('Server does not support this extended request'); + + /* + uint32 id + string "expand-path@openssh.com" + string path + */ + const pathLen = Buffer.byteLength(path); + let p = 9; + const buf = Buffer.allocUnsafe(4 + 1 + 4 + 4 + 23 + 4 + pathLen); + + writeUInt32BE(buf, buf.length - 4, 0); + buf[4] = REQUEST.EXTENDED; + const reqid = this._writeReqid = (this._writeReqid + 1) & MAX_REQID; + writeUInt32BE(buf, reqid, 5); + + writeUInt32BE(buf, 23, p); + buf.utf8Write('expand-path@openssh.com', p += 4, 23); + + writeUInt32BE(buf, pathLen, p += 20); + buf.utf8Write(path, p += 4, pathLen); + + this._requests[reqid] = { cb }; + + const isBuffered = sendOrBuffer(this, buf); + if (this._debug) { + const status = (isBuffered ? 'Buffered' : 'Sending'); + this._debug(`SFTP: Outbound: ${status} expand-path@openssh.com`); + } + } // =========================================================================== // Server-specific =========================================================== // =========================================================================== @@ -1760,7 +1864,7 @@ function tryCreateBuffer(size) { } function read_(self, handle, buf, off, len, position, cb, req_) { - const maxDataLen = self._maxPktLen - PKT_RW_OVERHEAD; + const maxDataLen = self._maxReadLen; const overflow = Math.max(len - maxDataLen, 0); if (overflow) @@ -2394,6 +2498,31 @@ function cleanupRequests(sftp) { } } +function requestLimits(sftp, cb) { + /* + uint32 id + string "limits@openssh.com" + */ + let p = 9; + const buf = Buffer.allocUnsafe(4 + 1 + 4 + 4 + 18); + + writeUInt32BE(buf, buf.length - 4, 0); + buf[4] = REQUEST.EXTENDED; + const reqid = sftp._writeReqid = (sftp._writeReqid + 1) & MAX_REQID; + writeUInt32BE(buf, reqid, 5); + + writeUInt32BE(buf, 18, p); + buf.utf8Write('limits@openssh.com', p += 4, 18); + + sftp._requests[reqid] = { extended: 'limits@openssh.com', cb }; + + const isBuffered = sendOrBuffer(sftp, buf); + if (sftp._debug) { + const which = (isBuffered ? 'Buffered' : 'Sending'); + sftp._debug(`SFTP: Outbound: ${which} limits@openssh.com`); + } +} + const CLIENT_HANDLERS = { [RESPONSE.VERSION]: (sftp, payload) => { if (sftp._version !== -1) @@ -2434,6 +2563,24 @@ const CLIENT_HANDLERS = { sftp._version = version; sftp._extensions = extensions; + + if (extensions['limits@openssh.com'] === '1') { + return requestLimits(sftp, (err, limits) => { + if (!err) { + if (limits.maxPktLen > 0) + sftp._maxOutPktLen = limits.maxPktLen; + if (limits.maxReadLen > 0) + sftp._maxReadLen = limits.maxReadLen; + if (limits.maxWriteLen > 0) + sftp._maxWriteLen = limits.maxWriteLen; + sftp.maxOpenHandles = ( + limits.maxOpenHandles > 0 ? limits.maxOpenHandles : Infinity + ); + } + sftp.emit('ready'); + }); + } + sftp.emit('ready'); }, [RESPONSE.STATUS]: (sftp, payload) => { @@ -2669,6 +2816,32 @@ const CLIENT_HANDLERS = { req.cb(undefined, stats); return; } + case 'limits@openssh.com': { + /* + uint64 max-packet-length + uint64 max-read-length + uint64 max-write-length + uint64 max-open-handles + */ + const limits = { + maxPktLen: bufferParser.readUInt64BE(), + maxReadLen: bufferParser.readUInt64BE(), + maxWriteLen: bufferParser.readUInt64BE(), + maxOpenHandles: bufferParser.readUInt64BE(), + }; + if (limits.maxOpenHandles === undefined) + break; + if (sftp._debug) { + sftp._debug( + 'SFTP: Inbound: Received EXTENDED_REPLY ' + + `(id:${reqID}, ${req.extended})` + ); + } + bufferParser.clear(); + if (typeof req.cb === 'function') + req.cb(undefined, limits); + return; + } default: // Unknown extended request sftp._debug && sftp._debug( diff --git a/test/test-sftp.js b/test/test-sftp.js index b583ad43..50e56f5d 100644 --- a/test/test-sftp.js +++ b/test/test-sftp.js @@ -65,7 +65,7 @@ setup('read', mustCall((client, server) => { })); setup('read (overflow)', mustCall((client, server) => { - const maxChunk = 34000 - 2048; + const maxChunk = client._maxReadLen; const expected = Buffer.alloc(3 * maxChunk, 'Q'); const handle_ = Buffer.from('node.js'); const buf = Buffer.alloc(expected.length, 0); @@ -106,7 +106,7 @@ setup('write', mustCall((client, server) => { })); setup('write (overflow)', mustCall((client, server) => { - const maxChunk = 34000 - 2048; + const maxChunk = client._maxWriteLen; const handle_ = Buffer.from('node.js'); const buf = Buffer.allocUnsafe(3 * maxChunk); let reqs = 0;