Skip to content

Commit 4606d0e

Browse files
committed
SFTP: increase max packet length, add missing OpenSSH extensions
1 parent a28b3ac commit 4606d0e

File tree

3 files changed

+183
-10
lines changed

3 files changed

+183
-10
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
SSH2 client and server modules written in pure JavaScript for [node.js](http://nodejs.org/).
44

5-
Development/testing is done against OpenSSH (8.0 currently).
5+
Development/testing is done against OpenSSH (8.7 currently).
66

77
Changes (breaking or otherwise) in v1.0.0 can be found [here](https://github.com/mscdex/ssh2/issues/935).
88

lib/protocol/SFTP.js

Lines changed: 180 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -154,7 +154,14 @@ class SFTP extends EventEmitter {
154154
this._pktData = undefined;
155155
this._writeReqid = -1;
156156
this._requests = {};
157-
this._maxPktLen = (this._isOpenSSH ? OPENSSH_MAX_PKT_LEN : 34000);
157+
this._maxInPktLen = OPENSSH_MAX_PKT_LEN;
158+
this._maxOutPktLen = 34000;
159+
this._maxReadLen =
160+
(this._isOpenSSH ? OPENSSH_MAX_PKT_LEN : 34000) - PKT_RW_OVERHEAD;
161+
this._maxWriteLen =
162+
(this._isOpenSSH ? OPENSSH_MAX_PKT_LEN : 34000) - PKT_RW_OVERHEAD;
163+
164+
this.maxOpenHandles = undefined;
158165

159166
// Channel compatibility
160167
this._client = client;
@@ -208,8 +215,8 @@ class SFTP extends EventEmitter {
208215
return;
209216
if (this._pktLen === 0)
210217
return doFatalSFTPError(this, 'Invalid packet length');
211-
if (this._pktLen > this._maxPktLen) {
212-
const max = this._maxPktLen;
218+
if (this._pktLen > this._maxInPktLen) {
219+
const max = this._maxInPktLen;
213220
return doFatalSFTPError(
214221
this,
215222
`Packet length ${this._pktLen} exceeds max length of ${max}`
@@ -432,7 +439,7 @@ class SFTP extends EventEmitter {
432439
return;
433440
}
434441

435-
const maxDataLen = this._maxPktLen - PKT_RW_OVERHEAD;
442+
const maxDataLen = this._maxWriteLen;
436443
const overflow = Math.max(len - maxDataLen, 0);
437444
const origPosition = position;
438445

@@ -1421,7 +1428,7 @@ class SFTP extends EventEmitter {
14211428
throw new Error('Client-only method called in server mode');
14221429

14231430
const ext = this._extensions['hardlink@openssh.com'];
1424-
if (!ext || ext.indexOf('1') === -1)
1431+
if (ext !== '1')
14251432
throw new Error('Server does not support this extended request');
14261433

14271434
/*
@@ -1461,7 +1468,7 @@ class SFTP extends EventEmitter {
14611468
throw new Error('Client-only method called in server mode');
14621469

14631470
const ext = this._extensions['fsync@openssh.com'];
1464-
if (!ext || ext.indexOf('1') === -1)
1471+
if (ext !== '1')
14651472
throw new Error('Server does not support this extended request');
14661473
if (!Buffer.isBuffer(handle))
14671474
throw new Error('handle is not a Buffer');
@@ -1492,6 +1499,103 @@ class SFTP extends EventEmitter {
14921499
`SFTP: Outbound: ${isBuffered ? 'Buffered' : 'Sending'} fsync@openssh.com`
14931500
);
14941501
}
1502+
ext_openssh_lsetstat(path, attrs, cb) {
1503+
if (this.server)
1504+
throw new Error('Client-only method called in server mode');
1505+
1506+
const ext = this._extensions['lsetstat@openssh.com'];
1507+
if (ext !== '1')
1508+
throw new Error('Server does not support this extended request');
1509+
1510+
let flags = 0;
1511+
let attrsLen = 0;
1512+
1513+
if (typeof attrs === 'object' && attrs !== null) {
1514+
attrs = attrsToBytes(attrs);
1515+
flags = attrs.flags;
1516+
attrsLen = attrs.nb;
1517+
} else if (typeof attrs === 'function') {
1518+
cb = attrs;
1519+
}
1520+
1521+
/*
1522+
uint32 id
1523+
string "lsetstat@openssh.com"
1524+
string path
1525+
ATTRS attrs
1526+
*/
1527+
const pathLen = Buffer.byteLength(path);
1528+
let p = 9;
1529+
const buf =
1530+
Buffer.allocUnsafe(4 + 1 + 4 + 4 + 20 + 4 + pathLen + 4 + attrsLen);
1531+
1532+
writeUInt32BE(buf, buf.length - 4, 0);
1533+
buf[4] = REQUEST.EXTENDED;
1534+
const reqid = this._writeReqid = (this._writeReqid + 1) & MAX_REQID;
1535+
writeUInt32BE(buf, reqid, 5);
1536+
1537+
writeUInt32BE(buf, 20, p);
1538+
buf.utf8Write('lsetstat@openssh.com', p += 4, 20);
1539+
1540+
writeUInt32BE(buf, pathLen, p += 20);
1541+
buf.utf8Write(path, p += 4, pathLen);
1542+
1543+
writeUInt32BE(buf, flags, p += pathLen);
1544+
if (attrsLen) {
1545+
p += 4;
1546+
1547+
if (attrsLen === ATTRS_BUF.length)
1548+
buf.set(ATTRS_BUF, p);
1549+
else
1550+
bufferCopy(ATTRS_BUF, buf, 0, attrsLen, p);
1551+
1552+
p += attrsLen;
1553+
}
1554+
1555+
this._requests[reqid] = { cb };
1556+
1557+
const isBuffered = sendOrBuffer(this, buf);
1558+
if (this._debug) {
1559+
const status = (isBuffered ? 'Buffered' : 'Sending');
1560+
this._debug(`SFTP: Outbound: ${status} lsetstat@openssh.com`);
1561+
}
1562+
}
1563+
ext_openssh_expandPath(path, cb) {
1564+
if (this.server)
1565+
throw new Error('Client-only method called in server mode');
1566+
1567+
const ext = this._extensions['expand-path@openssh.com'];
1568+
if (ext !== '1')
1569+
throw new Error('Server does not support this extended request');
1570+
1571+
/*
1572+
uint32 id
1573+
string "expand-path@openssh.com"
1574+
string path
1575+
*/
1576+
const pathLen = Buffer.byteLength(path);
1577+
let p = 9;
1578+
const buf = Buffer.allocUnsafe(4 + 1 + 4 + 4 + 23 + 4 + pathLen);
1579+
1580+
writeUInt32BE(buf, buf.length - 4, 0);
1581+
buf[4] = REQUEST.EXTENDED;
1582+
const reqid = this._writeReqid = (this._writeReqid + 1) & MAX_REQID;
1583+
writeUInt32BE(buf, reqid, 5);
1584+
1585+
writeUInt32BE(buf, 23, p);
1586+
buf.utf8Write('expand-path@openssh.com', p += 4, 23);
1587+
1588+
writeUInt32BE(buf, pathLen, p += 20);
1589+
buf.utf8Write(path, p += 4, pathLen);
1590+
1591+
this._requests[reqid] = { cb };
1592+
1593+
const isBuffered = sendOrBuffer(this, buf);
1594+
if (this._debug) {
1595+
const status = (isBuffered ? 'Buffered' : 'Sending');
1596+
this._debug(`SFTP: Outbound: ${status} expand-path@openssh.com`);
1597+
}
1598+
}
14951599
// ===========================================================================
14961600
// Server-specific ===========================================================
14971601
// ===========================================================================
@@ -1760,7 +1864,7 @@ function tryCreateBuffer(size) {
17601864
}
17611865

17621866
function read_(self, handle, buf, off, len, position, cb, req_) {
1763-
const maxDataLen = self._maxPktLen - PKT_RW_OVERHEAD;
1867+
const maxDataLen = self._maxReadLen;
17641868
const overflow = Math.max(len - maxDataLen, 0);
17651869

17661870
if (overflow)
@@ -2394,6 +2498,31 @@ function cleanupRequests(sftp) {
23942498
}
23952499
}
23962500

2501+
function requestLimits(sftp, cb) {
2502+
/*
2503+
uint32 id
2504+
string "limits@openssh.com"
2505+
*/
2506+
let p = 9;
2507+
const buf = Buffer.allocUnsafe(4 + 1 + 4 + 4 + 18);
2508+
2509+
writeUInt32BE(buf, buf.length - 4, 0);
2510+
buf[4] = REQUEST.EXTENDED;
2511+
const reqid = sftp._writeReqid = (sftp._writeReqid + 1) & MAX_REQID;
2512+
writeUInt32BE(buf, reqid, 5);
2513+
2514+
writeUInt32BE(buf, 18, p);
2515+
buf.utf8Write('limits@openssh.com', p += 4, 18);
2516+
2517+
sftp._requests[reqid] = { extended: 'limits@openssh.com', cb };
2518+
2519+
const isBuffered = sendOrBuffer(sftp, buf);
2520+
if (sftp._debug) {
2521+
const which = (isBuffered ? 'Buffered' : 'Sending');
2522+
sftp._debug(`SFTP: Outbound: ${which} limits@openssh.com`);
2523+
}
2524+
}
2525+
23972526
const CLIENT_HANDLERS = {
23982527
[RESPONSE.VERSION]: (sftp, payload) => {
23992528
if (sftp._version !== -1)
@@ -2434,6 +2563,24 @@ const CLIENT_HANDLERS = {
24342563

24352564
sftp._version = version;
24362565
sftp._extensions = extensions;
2566+
2567+
if (extensions['limits@openssh.com'] === '1') {
2568+
return requestLimits(sftp, (err, limits) => {
2569+
if (!err) {
2570+
if (limits.maxPktLen > 0)
2571+
sftp._maxOutPktLen = limits.maxPktLen;
2572+
if (limits.maxReadLen > 0)
2573+
sftp._maxReadLen = limits.maxReadLen;
2574+
if (limits.maxWriteLen > 0)
2575+
sftp._maxWriteLen = limits.maxWriteLen;
2576+
sftp.maxOpenHandles = (
2577+
limits.maxOpenHandles > 0 ? limits.maxOpenHandles : Infinity
2578+
);
2579+
}
2580+
sftp.emit('ready');
2581+
});
2582+
}
2583+
24372584
sftp.emit('ready');
24382585
},
24392586
[RESPONSE.STATUS]: (sftp, payload) => {
@@ -2669,6 +2816,32 @@ const CLIENT_HANDLERS = {
26692816
req.cb(undefined, stats);
26702817
return;
26712818
}
2819+
case 'limits@openssh.com': {
2820+
/*
2821+
uint64 max-packet-length
2822+
uint64 max-read-length
2823+
uint64 max-write-length
2824+
uint64 max-open-handles
2825+
*/
2826+
const limits = {
2827+
maxPktLen: bufferParser.readUInt64BE(),
2828+
maxReadLen: bufferParser.readUInt64BE(),
2829+
maxWriteLen: bufferParser.readUInt64BE(),
2830+
maxOpenHandles: bufferParser.readUInt64BE(),
2831+
};
2832+
if (limits.maxOpenHandles === undefined)
2833+
break;
2834+
if (sftp._debug) {
2835+
sftp._debug(
2836+
'SFTP: Inbound: Received EXTENDED_REPLY '
2837+
+ `(id:${reqID}, ${req.extended})`
2838+
);
2839+
}
2840+
bufferParser.clear();
2841+
if (typeof req.cb === 'function')
2842+
req.cb(undefined, limits);
2843+
return;
2844+
}
26722845
default:
26732846
// Unknown extended request
26742847
sftp._debug && sftp._debug(

test/test-sftp.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@ setup('read', mustCall((client, server) => {
6565
}));
6666

6767
setup('read (overflow)', mustCall((client, server) => {
68-
const maxChunk = 34000 - 2048;
68+
const maxChunk = client._maxReadLen;
6969
const expected = Buffer.alloc(3 * maxChunk, 'Q');
7070
const handle_ = Buffer.from('node.js');
7171
const buf = Buffer.alloc(expected.length, 0);
@@ -106,7 +106,7 @@ setup('write', mustCall((client, server) => {
106106
}));
107107

108108
setup('write (overflow)', mustCall((client, server) => {
109-
const maxChunk = 34000 - 2048;
109+
const maxChunk = client._maxWriteLen;
110110
const handle_ = Buffer.from('node.js');
111111
const buf = Buffer.allocUnsafe(3 * maxChunk);
112112
let reqs = 0;

0 commit comments

Comments
 (0)