Skip to content

Commit

Permalink
SFTP: increase max packet length, add missing OpenSSH extensions
Browse files Browse the repository at this point in the history
  • Loading branch information
mscdex committed Aug 23, 2021
1 parent a28b3ac commit 4606d0e
Show file tree
Hide file tree
Showing 3 changed files with 183 additions and 10 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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).

Expand Down
187 changes: 180 additions & 7 deletions lib/protocol/SFTP.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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}`
Expand Down Expand Up @@ -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;

Expand Down Expand Up @@ -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');

/*
Expand Down Expand Up @@ -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');
Expand Down Expand Up @@ -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 ===========================================================
// ===========================================================================
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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) => {
Expand Down Expand Up @@ -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(
Expand Down
4 changes: 2 additions & 2 deletions test/test-sftp.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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;
Expand Down

0 comments on commit 4606d0e

Please sign in to comment.