@electerm/ssh2 0.8.11 → 1.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +8 -1
- package/install.js +20 -0
- package/lib/Channel.js +236 -450
- package/lib/agent.js +1080 -376
- package/lib/client.js +1698 -1258
- package/lib/http-agents.js +72 -51
- package/lib/index.js +43 -0
- package/lib/protocol/Protocol.js +2077 -0
- package/lib/protocol/SFTP.js +3778 -0
- package/lib/protocol/constants.js +342 -0
- package/lib/protocol/crypto/binding.gyp +14 -0
- package/lib/protocol/crypto/poly1305.js +43 -0
- package/lib/protocol/crypto/src/binding.cc +2003 -0
- package/lib/protocol/crypto.js +1602 -0
- package/lib/protocol/handlers.js +16 -0
- package/lib/protocol/handlers.misc.js +1214 -0
- package/lib/protocol/kex.js +1831 -0
- package/lib/protocol/keyParser.js +1481 -0
- package/lib/protocol/node-fs-compat.js +115 -0
- package/lib/protocol/utils.js +356 -0
- package/lib/protocol/zlib.js +255 -0
- package/lib/server.js +1226 -1019
- package/lib/utils.js +336 -0
- package/package.json +42 -9
- package/lib/SFTPWrapper.js +0 -145
- package/lib/buffer-helpers.js +0 -22
- package/lib/keepalivemgr.js +0 -80
|
@@ -0,0 +1,3778 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const EventEmitter = require('events');
|
|
4
|
+
const fs = require('fs');
|
|
5
|
+
const { constants } = fs;
|
|
6
|
+
const {
|
|
7
|
+
Readable: ReadableStream,
|
|
8
|
+
Writable: WritableStream
|
|
9
|
+
} = require('stream');
|
|
10
|
+
const { inherits, isDate } = require('util');
|
|
11
|
+
|
|
12
|
+
const FastBuffer = Buffer[Symbol.species];
|
|
13
|
+
|
|
14
|
+
const {
|
|
15
|
+
bufferCopy,
|
|
16
|
+
bufferSlice,
|
|
17
|
+
makeBufferParser,
|
|
18
|
+
writeUInt32BE,
|
|
19
|
+
} = require('./utils.js');
|
|
20
|
+
|
|
21
|
+
const ATTR = {
|
|
22
|
+
SIZE: 0x00000001,
|
|
23
|
+
UIDGID: 0x00000002,
|
|
24
|
+
PERMISSIONS: 0x00000004,
|
|
25
|
+
ACMODTIME: 0x00000008,
|
|
26
|
+
EXTENDED: 0x80000000,
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
// Large enough to store all possible attributes
|
|
30
|
+
const ATTRS_BUF = Buffer.alloc(28);
|
|
31
|
+
|
|
32
|
+
const STATUS_CODE = {
|
|
33
|
+
OK: 0,
|
|
34
|
+
EOF: 1,
|
|
35
|
+
NO_SUCH_FILE: 2,
|
|
36
|
+
PERMISSION_DENIED: 3,
|
|
37
|
+
FAILURE: 4,
|
|
38
|
+
BAD_MESSAGE: 5,
|
|
39
|
+
NO_CONNECTION: 6,
|
|
40
|
+
CONNECTION_LOST: 7,
|
|
41
|
+
OP_UNSUPPORTED: 8
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
const VALID_STATUS_CODES = new Map(
|
|
45
|
+
Object.values(STATUS_CODE).map((n) => [n, 1])
|
|
46
|
+
);
|
|
47
|
+
|
|
48
|
+
const STATUS_CODE_STR = {
|
|
49
|
+
[STATUS_CODE.OK]: 'No error',
|
|
50
|
+
[STATUS_CODE.EOF]: 'End of file',
|
|
51
|
+
[STATUS_CODE.NO_SUCH_FILE]: 'No such file or directory',
|
|
52
|
+
[STATUS_CODE.PERMISSION_DENIED]: 'Permission denied',
|
|
53
|
+
[STATUS_CODE.FAILURE]: 'Failure',
|
|
54
|
+
[STATUS_CODE.BAD_MESSAGE]: 'Bad message',
|
|
55
|
+
[STATUS_CODE.NO_CONNECTION]: 'No connection',
|
|
56
|
+
[STATUS_CODE.CONNECTION_LOST]: 'Connection lost',
|
|
57
|
+
[STATUS_CODE.OP_UNSUPPORTED]: 'Operation unsupported',
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
const REQUEST = {
|
|
61
|
+
INIT: 1,
|
|
62
|
+
OPEN: 3,
|
|
63
|
+
CLOSE: 4,
|
|
64
|
+
READ: 5,
|
|
65
|
+
WRITE: 6,
|
|
66
|
+
LSTAT: 7,
|
|
67
|
+
FSTAT: 8,
|
|
68
|
+
SETSTAT: 9,
|
|
69
|
+
FSETSTAT: 10,
|
|
70
|
+
OPENDIR: 11,
|
|
71
|
+
READDIR: 12,
|
|
72
|
+
REMOVE: 13,
|
|
73
|
+
MKDIR: 14,
|
|
74
|
+
RMDIR: 15,
|
|
75
|
+
REALPATH: 16,
|
|
76
|
+
STAT: 17,
|
|
77
|
+
RENAME: 18,
|
|
78
|
+
READLINK: 19,
|
|
79
|
+
SYMLINK: 20,
|
|
80
|
+
EXTENDED: 200
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
const RESPONSE = {
|
|
84
|
+
VERSION: 2,
|
|
85
|
+
STATUS: 101,
|
|
86
|
+
HANDLE: 102,
|
|
87
|
+
DATA: 103,
|
|
88
|
+
NAME: 104,
|
|
89
|
+
ATTRS: 105,
|
|
90
|
+
EXTENDED: 201
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
const OPEN_MODE = {
|
|
94
|
+
READ: 0x00000001,
|
|
95
|
+
WRITE: 0x00000002,
|
|
96
|
+
APPEND: 0x00000004,
|
|
97
|
+
CREAT: 0x00000008,
|
|
98
|
+
TRUNC: 0x00000010,
|
|
99
|
+
EXCL: 0x00000020
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
const PKT_RW_OVERHEAD = 2 * 1024;
|
|
103
|
+
const MAX_REQID = 2 ** 32 - 1;
|
|
104
|
+
const CLIENT_VERSION_BUFFER = Buffer.from([
|
|
105
|
+
0, 0, 0, 5 /* length */,
|
|
106
|
+
REQUEST.INIT,
|
|
107
|
+
0, 0, 0, 3 /* version */
|
|
108
|
+
]);
|
|
109
|
+
const SERVER_VERSION_BUFFER = Buffer.from([
|
|
110
|
+
0, 0, 0, 5 /* length */,
|
|
111
|
+
RESPONSE.VERSION,
|
|
112
|
+
0, 0, 0, 3 /* version */
|
|
113
|
+
]);
|
|
114
|
+
|
|
115
|
+
const RE_OPENSSH = /^SSH-2.0-(?:OpenSSH|dropbear)/;
|
|
116
|
+
const OPENSSH_MAX_PKT_LEN = 256 * 1024;
|
|
117
|
+
|
|
118
|
+
const bufferParser = makeBufferParser();
|
|
119
|
+
|
|
120
|
+
const fakeStderr = {
|
|
121
|
+
readable: false,
|
|
122
|
+
writable: false,
|
|
123
|
+
push: (data) => {},
|
|
124
|
+
once: () => {},
|
|
125
|
+
on: () => {},
|
|
126
|
+
emit: () => {},
|
|
127
|
+
end: () => {},
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
function noop() {}
|
|
131
|
+
|
|
132
|
+
// Emulates enough of `Channel` to be able to be used as a drop-in replacement
|
|
133
|
+
// in order to process incoming data with as little overhead as possible
|
|
134
|
+
class SFTP extends EventEmitter {
|
|
135
|
+
constructor(client, chanInfo, cfg) {
|
|
136
|
+
super();
|
|
137
|
+
|
|
138
|
+
if (typeof cfg !== 'object' || !cfg)
|
|
139
|
+
cfg = {};
|
|
140
|
+
|
|
141
|
+
const remoteIdentRaw = client._protocol._remoteIdentRaw;
|
|
142
|
+
|
|
143
|
+
this.server = !!cfg.server;
|
|
144
|
+
this._debug = (typeof cfg.debug === 'function' ? cfg.debug : undefined);
|
|
145
|
+
this._isOpenSSH = (remoteIdentRaw && RE_OPENSSH.test(remoteIdentRaw));
|
|
146
|
+
|
|
147
|
+
this._version = -1;
|
|
148
|
+
this._extensions = {};
|
|
149
|
+
this._biOpt = cfg.biOpt;
|
|
150
|
+
this._pktLenBytes = 0;
|
|
151
|
+
this._pktLen = 0;
|
|
152
|
+
this._pktPos = 0;
|
|
153
|
+
this._pktType = 0;
|
|
154
|
+
this._pktData = undefined;
|
|
155
|
+
this._writeReqid = -1;
|
|
156
|
+
this._requests = {};
|
|
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;
|
|
165
|
+
|
|
166
|
+
// Channel compatibility
|
|
167
|
+
this._client = client;
|
|
168
|
+
this._protocol = client._protocol;
|
|
169
|
+
this._callbacks = [];
|
|
170
|
+
this._hasX11 = false;
|
|
171
|
+
this._exit = {
|
|
172
|
+
code: undefined,
|
|
173
|
+
signal: undefined,
|
|
174
|
+
dump: undefined,
|
|
175
|
+
desc: undefined,
|
|
176
|
+
};
|
|
177
|
+
this._waitWindow = false; // SSH-level backpressure
|
|
178
|
+
this._chunkcb = undefined;
|
|
179
|
+
this._buffer = [];
|
|
180
|
+
this.type = chanInfo.type;
|
|
181
|
+
this.subtype = undefined;
|
|
182
|
+
this.incoming = chanInfo.incoming;
|
|
183
|
+
this.outgoing = chanInfo.outgoing;
|
|
184
|
+
this.stderr = fakeStderr;
|
|
185
|
+
this.readable = true;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// This handles incoming data to parse
|
|
189
|
+
push(data) {
|
|
190
|
+
if (data === null) {
|
|
191
|
+
cleanupRequests(this);
|
|
192
|
+
if (!this.readable)
|
|
193
|
+
return;
|
|
194
|
+
// No more incoming data from the remote side
|
|
195
|
+
this.readable = false;
|
|
196
|
+
this.emit('end');
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
199
|
+
/*
|
|
200
|
+
uint32 length
|
|
201
|
+
byte type
|
|
202
|
+
byte[length - 1] data payload
|
|
203
|
+
*/
|
|
204
|
+
let p = 0;
|
|
205
|
+
|
|
206
|
+
while (p < data.length) {
|
|
207
|
+
if (this._pktLenBytes < 4) {
|
|
208
|
+
let nb = Math.min(4 - this._pktLenBytes, data.length - p);
|
|
209
|
+
this._pktLenBytes += nb;
|
|
210
|
+
|
|
211
|
+
while (nb--)
|
|
212
|
+
this._pktLen = (this._pktLen << 8) + data[p++];
|
|
213
|
+
|
|
214
|
+
if (this._pktLenBytes < 4)
|
|
215
|
+
return;
|
|
216
|
+
if (this._pktLen === 0)
|
|
217
|
+
return doFatalSFTPError(this, 'Invalid packet length');
|
|
218
|
+
if (this._pktLen > this._maxInPktLen) {
|
|
219
|
+
const max = this._maxInPktLen;
|
|
220
|
+
return doFatalSFTPError(
|
|
221
|
+
this,
|
|
222
|
+
`Packet length ${this._pktLen} exceeds max length of ${max}`
|
|
223
|
+
);
|
|
224
|
+
}
|
|
225
|
+
if (p >= data.length)
|
|
226
|
+
return;
|
|
227
|
+
}
|
|
228
|
+
if (this._pktPos < this._pktLen) {
|
|
229
|
+
const nb = Math.min(this._pktLen - this._pktPos, data.length - p);
|
|
230
|
+
if (p !== 0 || nb !== data.length) {
|
|
231
|
+
if (nb === this._pktLen) {
|
|
232
|
+
this._pkt = new FastBuffer(data.buffer, data.byteOffset + p, nb);
|
|
233
|
+
} else {
|
|
234
|
+
if (!this._pkt)
|
|
235
|
+
this._pkt = Buffer.allocUnsafe(this._pktLen);
|
|
236
|
+
this._pkt.set(
|
|
237
|
+
new Uint8Array(data.buffer, data.byteOffset + p, nb),
|
|
238
|
+
this._pktPos
|
|
239
|
+
);
|
|
240
|
+
}
|
|
241
|
+
} else if (nb === this._pktLen) {
|
|
242
|
+
this._pkt = data;
|
|
243
|
+
} else {
|
|
244
|
+
if (!this._pkt)
|
|
245
|
+
this._pkt = Buffer.allocUnsafe(this._pktLen);
|
|
246
|
+
this._pkt.set(data, this._pktPos);
|
|
247
|
+
}
|
|
248
|
+
p += nb;
|
|
249
|
+
this._pktPos += nb;
|
|
250
|
+
if (this._pktPos < this._pktLen)
|
|
251
|
+
return;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
const type = this._pkt[0];
|
|
255
|
+
const payload = this._pkt;
|
|
256
|
+
|
|
257
|
+
// Prepare for next packet
|
|
258
|
+
this._pktLen = 0;
|
|
259
|
+
this._pktLenBytes = 0;
|
|
260
|
+
this._pkt = undefined;
|
|
261
|
+
this._pktPos = 0;
|
|
262
|
+
|
|
263
|
+
const handler = (this.server
|
|
264
|
+
? SERVER_HANDLERS[type]
|
|
265
|
+
: CLIENT_HANDLERS[type]);
|
|
266
|
+
if (!handler)
|
|
267
|
+
return doFatalSFTPError(this, `Unknown packet type ${type}`);
|
|
268
|
+
|
|
269
|
+
if (this._version === -1) {
|
|
270
|
+
if (this.server) {
|
|
271
|
+
if (type !== REQUEST.INIT)
|
|
272
|
+
return doFatalSFTPError(this, `Expected INIT packet, got ${type}`);
|
|
273
|
+
} else if (type !== RESPONSE.VERSION) {
|
|
274
|
+
return doFatalSFTPError(this, `Expected VERSION packet, got ${type}`);
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
if (handler(this, payload) === false)
|
|
279
|
+
return;
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
end() {
|
|
284
|
+
this.destroy();
|
|
285
|
+
}
|
|
286
|
+
destroy() {
|
|
287
|
+
if (this.outgoing.state === 'open' || this.outgoing.state === 'eof') {
|
|
288
|
+
this.outgoing.state = 'closing';
|
|
289
|
+
this._protocol.channelClose(this.outgoing.id);
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
_init() {
|
|
293
|
+
this._init = noop;
|
|
294
|
+
if (!this.server)
|
|
295
|
+
sendOrBuffer(this, CLIENT_VERSION_BUFFER);
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
// ===========================================================================
|
|
299
|
+
// Client-specific ===========================================================
|
|
300
|
+
// ===========================================================================
|
|
301
|
+
createReadStream(path, options) {
|
|
302
|
+
if (this.server)
|
|
303
|
+
throw new Error('Client-only method called in server mode');
|
|
304
|
+
|
|
305
|
+
return new ReadStream(this, path, options);
|
|
306
|
+
}
|
|
307
|
+
createWriteStream(path, options) {
|
|
308
|
+
if (this.server)
|
|
309
|
+
throw new Error('Client-only method called in server mode');
|
|
310
|
+
|
|
311
|
+
return new WriteStream(this, path, options);
|
|
312
|
+
}
|
|
313
|
+
open(path, flags_, attrs, cb) {
|
|
314
|
+
if (this.server)
|
|
315
|
+
throw new Error('Client-only method called in server mode');
|
|
316
|
+
|
|
317
|
+
if (typeof attrs === 'function') {
|
|
318
|
+
cb = attrs;
|
|
319
|
+
attrs = undefined;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
const flags = (typeof flags_ === 'number' ? flags_ : stringToFlags(flags_));
|
|
323
|
+
if (flags === null)
|
|
324
|
+
throw new Error(`Unknown flags string: ${flags_}`);
|
|
325
|
+
|
|
326
|
+
let attrsFlags = 0;
|
|
327
|
+
let attrsLen = 0;
|
|
328
|
+
if (typeof attrs === 'string' || typeof attrs === 'number')
|
|
329
|
+
attrs = { mode: attrs };
|
|
330
|
+
if (typeof attrs === 'object' && attrs !== null) {
|
|
331
|
+
attrs = attrsToBytes(attrs);
|
|
332
|
+
attrsFlags = attrs.flags;
|
|
333
|
+
attrsLen = attrs.nb;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
/*
|
|
337
|
+
uint32 id
|
|
338
|
+
string filename
|
|
339
|
+
uint32 pflags
|
|
340
|
+
ATTRS attrs
|
|
341
|
+
*/
|
|
342
|
+
const pathLen = Buffer.byteLength(path);
|
|
343
|
+
let p = 9;
|
|
344
|
+
const buf = Buffer.allocUnsafe(4 + 1 + 4 + 4 + pathLen + 4 + 4 + attrsLen);
|
|
345
|
+
|
|
346
|
+
writeUInt32BE(buf, buf.length - 4, 0);
|
|
347
|
+
buf[4] = REQUEST.OPEN;
|
|
348
|
+
const reqid = this._writeReqid = (this._writeReqid + 1) & MAX_REQID;
|
|
349
|
+
writeUInt32BE(buf, reqid, 5);
|
|
350
|
+
|
|
351
|
+
writeUInt32BE(buf, pathLen, p);
|
|
352
|
+
buf.utf8Write(path, p += 4, pathLen);
|
|
353
|
+
writeUInt32BE(buf, flags, p += pathLen);
|
|
354
|
+
writeUInt32BE(buf, attrsFlags, p += 4);
|
|
355
|
+
if (attrsLen) {
|
|
356
|
+
p += 4;
|
|
357
|
+
|
|
358
|
+
if (attrsLen === ATTRS_BUF.length)
|
|
359
|
+
buf.set(ATTRS_BUF, p);
|
|
360
|
+
else
|
|
361
|
+
bufferCopy(ATTRS_BUF, buf, 0, attrsLen, p);
|
|
362
|
+
|
|
363
|
+
p += attrsLen;
|
|
364
|
+
}
|
|
365
|
+
this._requests[reqid] = { cb };
|
|
366
|
+
|
|
367
|
+
const isBuffered = sendOrBuffer(this, buf);
|
|
368
|
+
this._debug && this._debug(
|
|
369
|
+
`SFTP: Outbound: ${isBuffered ? 'Buffered' : 'Sending'} OPEN`
|
|
370
|
+
);
|
|
371
|
+
}
|
|
372
|
+
close(handle, cb) {
|
|
373
|
+
if (this.server)
|
|
374
|
+
throw new Error('Client-only method called in server mode');
|
|
375
|
+
|
|
376
|
+
if (!Buffer.isBuffer(handle))
|
|
377
|
+
throw new Error('handle is not a Buffer');
|
|
378
|
+
|
|
379
|
+
/*
|
|
380
|
+
uint32 id
|
|
381
|
+
string handle
|
|
382
|
+
*/
|
|
383
|
+
const handleLen = handle.length;
|
|
384
|
+
let p = 9;
|
|
385
|
+
const buf = Buffer.allocUnsafe(4 + 1 + 4 + 4 + handleLen);
|
|
386
|
+
|
|
387
|
+
writeUInt32BE(buf, buf.length - 4, 0);
|
|
388
|
+
buf[4] = REQUEST.CLOSE;
|
|
389
|
+
const reqid = this._writeReqid = (this._writeReqid + 1) & MAX_REQID;
|
|
390
|
+
writeUInt32BE(buf, reqid, 5);
|
|
391
|
+
|
|
392
|
+
writeUInt32BE(buf, handleLen, p);
|
|
393
|
+
buf.set(handle, p += 4);
|
|
394
|
+
|
|
395
|
+
this._requests[reqid] = { cb };
|
|
396
|
+
|
|
397
|
+
const isBuffered = sendOrBuffer(this, buf);
|
|
398
|
+
this._debug && this._debug(
|
|
399
|
+
`SFTP: Outbound: ${isBuffered ? 'Buffered' : 'Sending'} CLOSE`
|
|
400
|
+
);
|
|
401
|
+
}
|
|
402
|
+
read(handle, buf, off, len, position, cb) {
|
|
403
|
+
if (this.server)
|
|
404
|
+
throw new Error('Client-only method called in server mode');
|
|
405
|
+
if (!Buffer.isBuffer(handle))
|
|
406
|
+
throw new Error('handle is not a Buffer');
|
|
407
|
+
if (!Buffer.isBuffer(buf))
|
|
408
|
+
throw new Error('buffer is not a Buffer');
|
|
409
|
+
if (off >= buf.length)
|
|
410
|
+
throw new Error('offset is out of bounds');
|
|
411
|
+
if (off + len > buf.length)
|
|
412
|
+
throw new Error('length extends beyond buffer');
|
|
413
|
+
if (position === null)
|
|
414
|
+
throw new Error('null position currently unsupported');
|
|
415
|
+
|
|
416
|
+
read_(this, handle, buf, off, len, position, cb);
|
|
417
|
+
}
|
|
418
|
+
readData(handle, buf, off, len, position, cb) {
|
|
419
|
+
// Backwards compatibility
|
|
420
|
+
this.read(handle, buf, off, len, position, cb);
|
|
421
|
+
}
|
|
422
|
+
write(handle, buf, off, len, position, cb) {
|
|
423
|
+
if (this.server)
|
|
424
|
+
throw new Error('Client-only method called in server mode');
|
|
425
|
+
|
|
426
|
+
if (!Buffer.isBuffer(handle))
|
|
427
|
+
throw new Error('handle is not a Buffer');
|
|
428
|
+
if (!Buffer.isBuffer(buf))
|
|
429
|
+
throw new Error('buffer is not a Buffer');
|
|
430
|
+
if (off > buf.length)
|
|
431
|
+
throw new Error('offset is out of bounds');
|
|
432
|
+
if (off + len > buf.length)
|
|
433
|
+
throw new Error('length extends beyond buffer');
|
|
434
|
+
if (position === null)
|
|
435
|
+
throw new Error('null position currently unsupported');
|
|
436
|
+
|
|
437
|
+
if (!len) {
|
|
438
|
+
cb && process.nextTick(cb, undefined, 0);
|
|
439
|
+
return;
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
const maxDataLen = this._maxWriteLen;
|
|
443
|
+
const overflow = Math.max(len - maxDataLen, 0);
|
|
444
|
+
const origPosition = position;
|
|
445
|
+
|
|
446
|
+
if (overflow)
|
|
447
|
+
len = maxDataLen;
|
|
448
|
+
|
|
449
|
+
/*
|
|
450
|
+
uint32 id
|
|
451
|
+
string handle
|
|
452
|
+
uint64 offset
|
|
453
|
+
string data
|
|
454
|
+
*/
|
|
455
|
+
const handleLen = handle.length;
|
|
456
|
+
let p = 9;
|
|
457
|
+
const out = Buffer.allocUnsafe(4 + 1 + 4 + 4 + handleLen + 8 + 4 + len);
|
|
458
|
+
|
|
459
|
+
writeUInt32BE(out, out.length - 4, 0);
|
|
460
|
+
out[4] = REQUEST.WRITE;
|
|
461
|
+
const reqid = this._writeReqid = (this._writeReqid + 1) & MAX_REQID;
|
|
462
|
+
writeUInt32BE(out, reqid, 5);
|
|
463
|
+
|
|
464
|
+
writeUInt32BE(out, handleLen, p);
|
|
465
|
+
out.set(handle, p += 4);
|
|
466
|
+
p += handleLen;
|
|
467
|
+
for (let i = 7; i >= 0; --i) {
|
|
468
|
+
out[p + i] = position & 0xFF;
|
|
469
|
+
position /= 256;
|
|
470
|
+
}
|
|
471
|
+
writeUInt32BE(out, len, p += 8);
|
|
472
|
+
bufferCopy(buf, out, off, off + len, p += 4);
|
|
473
|
+
|
|
474
|
+
this._requests[reqid] = {
|
|
475
|
+
cb: (err) => {
|
|
476
|
+
if (err) {
|
|
477
|
+
if (typeof cb === 'function')
|
|
478
|
+
cb(err);
|
|
479
|
+
} else if (overflow) {
|
|
480
|
+
this.write(handle,
|
|
481
|
+
buf,
|
|
482
|
+
off + len,
|
|
483
|
+
overflow,
|
|
484
|
+
origPosition + len,
|
|
485
|
+
cb);
|
|
486
|
+
} else if (typeof cb === 'function') {
|
|
487
|
+
cb(undefined, off + len);
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
};
|
|
491
|
+
|
|
492
|
+
const isSent = sendOrBuffer(this, out);
|
|
493
|
+
if (this._debug) {
|
|
494
|
+
const how = (isSent ? 'Sent' : 'Buffered');
|
|
495
|
+
this._debug(`SFTP: Outbound: ${how} WRITE (id:${reqid})`);
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
writeData(handle, buf, off, len, position, cb) {
|
|
499
|
+
// Backwards compatibility
|
|
500
|
+
this.write(handle, buf, off, len, position, cb);
|
|
501
|
+
}
|
|
502
|
+
fastGet(remotePath, localPath, opts, cb) {
|
|
503
|
+
if (this.server)
|
|
504
|
+
throw new Error('Client-only method called in server mode');
|
|
505
|
+
|
|
506
|
+
fastXfer(this, fs, remotePath, localPath, opts, cb);
|
|
507
|
+
}
|
|
508
|
+
fastPut(localPath, remotePath, opts, cb) {
|
|
509
|
+
if (this.server)
|
|
510
|
+
throw new Error('Client-only method called in server mode');
|
|
511
|
+
|
|
512
|
+
fastXfer(fs, this, localPath, remotePath, opts, cb);
|
|
513
|
+
}
|
|
514
|
+
readFile(path, options, callback_) {
|
|
515
|
+
if (this.server)
|
|
516
|
+
throw new Error('Client-only method called in server mode');
|
|
517
|
+
|
|
518
|
+
let callback;
|
|
519
|
+
if (typeof callback_ === 'function') {
|
|
520
|
+
callback = callback_;
|
|
521
|
+
} else if (typeof options === 'function') {
|
|
522
|
+
callback = options;
|
|
523
|
+
options = undefined;
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
if (typeof options === 'string')
|
|
527
|
+
options = { encoding: options, flag: 'r' };
|
|
528
|
+
else if (!options)
|
|
529
|
+
options = { encoding: null, flag: 'r' };
|
|
530
|
+
else if (typeof options !== 'object')
|
|
531
|
+
throw new TypeError('Bad arguments');
|
|
532
|
+
|
|
533
|
+
const encoding = options.encoding;
|
|
534
|
+
if (encoding && !Buffer.isEncoding(encoding))
|
|
535
|
+
throw new Error(`Unknown encoding: ${encoding}`);
|
|
536
|
+
|
|
537
|
+
// First stat the file, so we know the size.
|
|
538
|
+
let size;
|
|
539
|
+
let buffer; // Single buffer with file data
|
|
540
|
+
let buffers; // List for when size is unknown
|
|
541
|
+
let pos = 0;
|
|
542
|
+
let handle;
|
|
543
|
+
|
|
544
|
+
// SFTPv3 does not support using -1 for read position, so we have to track
|
|
545
|
+
// read position manually
|
|
546
|
+
let bytesRead = 0;
|
|
547
|
+
|
|
548
|
+
const flag = options.flag || 'r';
|
|
549
|
+
|
|
550
|
+
const read = () => {
|
|
551
|
+
if (size === 0) {
|
|
552
|
+
buffer = Buffer.allocUnsafe(8192);
|
|
553
|
+
this.read(handle, buffer, 0, 8192, bytesRead, afterRead);
|
|
554
|
+
} else {
|
|
555
|
+
this.read(handle, buffer, pos, size - pos, bytesRead, afterRead);
|
|
556
|
+
}
|
|
557
|
+
};
|
|
558
|
+
|
|
559
|
+
const afterRead = (er, nbytes) => {
|
|
560
|
+
let eof;
|
|
561
|
+
if (er) {
|
|
562
|
+
eof = (er.code === STATUS_CODE.EOF);
|
|
563
|
+
if (!eof) {
|
|
564
|
+
return this.close(handle, () => {
|
|
565
|
+
return callback && callback(er);
|
|
566
|
+
});
|
|
567
|
+
}
|
|
568
|
+
} else {
|
|
569
|
+
eof = false;
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
if (eof || (size === 0 && nbytes === 0))
|
|
573
|
+
return close();
|
|
574
|
+
|
|
575
|
+
bytesRead += nbytes;
|
|
576
|
+
pos += nbytes;
|
|
577
|
+
if (size !== 0) {
|
|
578
|
+
if (pos === size)
|
|
579
|
+
close();
|
|
580
|
+
else
|
|
581
|
+
read();
|
|
582
|
+
} else {
|
|
583
|
+
// Unknown size, just read until we don't get bytes.
|
|
584
|
+
buffers.push(bufferSlice(buffer, 0, nbytes));
|
|
585
|
+
read();
|
|
586
|
+
}
|
|
587
|
+
};
|
|
588
|
+
afterRead._wantEOFError = true;
|
|
589
|
+
|
|
590
|
+
const close = () => {
|
|
591
|
+
this.close(handle, (er) => {
|
|
592
|
+
if (size === 0) {
|
|
593
|
+
// Collect the data into the buffers list.
|
|
594
|
+
buffer = Buffer.concat(buffers, pos);
|
|
595
|
+
} else if (pos < size) {
|
|
596
|
+
buffer = bufferSlice(buffer, 0, pos);
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
if (encoding)
|
|
600
|
+
buffer = buffer.toString(encoding);
|
|
601
|
+
return callback && callback(er, buffer);
|
|
602
|
+
});
|
|
603
|
+
};
|
|
604
|
+
|
|
605
|
+
this.open(path, flag, 0o666, (er, handle_) => {
|
|
606
|
+
if (er)
|
|
607
|
+
return callback && callback(er);
|
|
608
|
+
handle = handle_;
|
|
609
|
+
|
|
610
|
+
const tryStat = (er, st) => {
|
|
611
|
+
if (er) {
|
|
612
|
+
// Try stat() for sftp servers that may not support fstat() for
|
|
613
|
+
// whatever reason
|
|
614
|
+
this.stat(path, (er_, st_) => {
|
|
615
|
+
if (er_) {
|
|
616
|
+
return this.close(handle, () => {
|
|
617
|
+
callback && callback(er);
|
|
618
|
+
});
|
|
619
|
+
}
|
|
620
|
+
tryStat(null, st_);
|
|
621
|
+
});
|
|
622
|
+
return;
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
size = st.size || 0;
|
|
626
|
+
if (size === 0) {
|
|
627
|
+
// The kernel lies about many files.
|
|
628
|
+
// Go ahead and try to read some bytes.
|
|
629
|
+
buffers = [];
|
|
630
|
+
return read();
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
buffer = Buffer.allocUnsafe(size);
|
|
634
|
+
read();
|
|
635
|
+
};
|
|
636
|
+
this.fstat(handle, tryStat);
|
|
637
|
+
});
|
|
638
|
+
}
|
|
639
|
+
writeFile(path, data, options, callback_) {
|
|
640
|
+
if (this.server)
|
|
641
|
+
throw new Error('Client-only method called in server mode');
|
|
642
|
+
|
|
643
|
+
let callback;
|
|
644
|
+
if (typeof callback_ === 'function') {
|
|
645
|
+
callback = callback_;
|
|
646
|
+
} else if (typeof options === 'function') {
|
|
647
|
+
callback = options;
|
|
648
|
+
options = undefined;
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
if (typeof options === 'string')
|
|
652
|
+
options = { encoding: options, mode: 0o666, flag: 'w' };
|
|
653
|
+
else if (!options)
|
|
654
|
+
options = { encoding: 'utf8', mode: 0o666, flag: 'w' };
|
|
655
|
+
else if (typeof options !== 'object')
|
|
656
|
+
throw new TypeError('Bad arguments');
|
|
657
|
+
|
|
658
|
+
if (options.encoding && !Buffer.isEncoding(options.encoding))
|
|
659
|
+
throw new Error(`Unknown encoding: ${options.encoding}`);
|
|
660
|
+
|
|
661
|
+
const flag = options.flag || 'w';
|
|
662
|
+
this.open(path, flag, options.mode, (openErr, handle) => {
|
|
663
|
+
if (openErr) {
|
|
664
|
+
callback && callback(openErr);
|
|
665
|
+
} else {
|
|
666
|
+
const buffer = (Buffer.isBuffer(data)
|
|
667
|
+
? data
|
|
668
|
+
: Buffer.from('' + data, options.encoding || 'utf8'));
|
|
669
|
+
const position = (/a/.test(flag) ? null : 0);
|
|
670
|
+
|
|
671
|
+
// SFTPv3 does not support the notion of 'current position'
|
|
672
|
+
// (null position), so we just attempt to append to the end of the file
|
|
673
|
+
// instead
|
|
674
|
+
if (position === null) {
|
|
675
|
+
const tryStat = (er, st) => {
|
|
676
|
+
if (er) {
|
|
677
|
+
// Try stat() for sftp servers that may not support fstat() for
|
|
678
|
+
// whatever reason
|
|
679
|
+
this.stat(path, (er_, st_) => {
|
|
680
|
+
if (er_) {
|
|
681
|
+
return this.close(handle, () => {
|
|
682
|
+
callback && callback(er);
|
|
683
|
+
});
|
|
684
|
+
}
|
|
685
|
+
tryStat(null, st_);
|
|
686
|
+
});
|
|
687
|
+
return;
|
|
688
|
+
}
|
|
689
|
+
writeAll(this, handle, buffer, 0, buffer.length, st.size, callback);
|
|
690
|
+
};
|
|
691
|
+
this.fstat(handle, tryStat);
|
|
692
|
+
return;
|
|
693
|
+
}
|
|
694
|
+
writeAll(this, handle, buffer, 0, buffer.length, position, callback);
|
|
695
|
+
}
|
|
696
|
+
});
|
|
697
|
+
}
|
|
698
|
+
appendFile(path, data, options, callback_) {
|
|
699
|
+
if (this.server)
|
|
700
|
+
throw new Error('Client-only method called in server mode');
|
|
701
|
+
|
|
702
|
+
let callback;
|
|
703
|
+
if (typeof callback_ === 'function') {
|
|
704
|
+
callback = callback_;
|
|
705
|
+
} else if (typeof options === 'function') {
|
|
706
|
+
callback = options;
|
|
707
|
+
options = undefined;
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
if (typeof options === 'string')
|
|
711
|
+
options = { encoding: options, mode: 0o666, flag: 'a' };
|
|
712
|
+
else if (!options)
|
|
713
|
+
options = { encoding: 'utf8', mode: 0o666, flag: 'a' };
|
|
714
|
+
else if (typeof options !== 'object')
|
|
715
|
+
throw new TypeError('Bad arguments');
|
|
716
|
+
|
|
717
|
+
if (!options.flag)
|
|
718
|
+
options = Object.assign({ flag: 'a' }, options);
|
|
719
|
+
this.writeFile(path, data, options, callback);
|
|
720
|
+
}
|
|
721
|
+
exists(path, cb) {
|
|
722
|
+
if (this.server)
|
|
723
|
+
throw new Error('Client-only method called in server mode');
|
|
724
|
+
|
|
725
|
+
this.stat(path, (err) => {
|
|
726
|
+
cb && cb(err ? false : true);
|
|
727
|
+
});
|
|
728
|
+
}
|
|
729
|
+
unlink(filename, cb) {
|
|
730
|
+
if (this.server)
|
|
731
|
+
throw new Error('Client-only method called in server mode');
|
|
732
|
+
|
|
733
|
+
/*
|
|
734
|
+
uint32 id
|
|
735
|
+
string filename
|
|
736
|
+
*/
|
|
737
|
+
const fnameLen = Buffer.byteLength(filename);
|
|
738
|
+
let p = 9;
|
|
739
|
+
const buf = Buffer.allocUnsafe(4 + 1 + 4 + 4 + fnameLen);
|
|
740
|
+
|
|
741
|
+
writeUInt32BE(buf, buf.length - 4, 0);
|
|
742
|
+
buf[4] = REQUEST.REMOVE;
|
|
743
|
+
const reqid = this._writeReqid = (this._writeReqid + 1) & MAX_REQID;
|
|
744
|
+
writeUInt32BE(buf, reqid, 5);
|
|
745
|
+
|
|
746
|
+
writeUInt32BE(buf, fnameLen, p);
|
|
747
|
+
buf.utf8Write(filename, p += 4, fnameLen);
|
|
748
|
+
|
|
749
|
+
this._requests[reqid] = { cb };
|
|
750
|
+
|
|
751
|
+
const isBuffered = sendOrBuffer(this, buf);
|
|
752
|
+
this._debug && this._debug(
|
|
753
|
+
`SFTP: Outbound: ${isBuffered ? 'Buffered' : 'Sending'} REMOVE`
|
|
754
|
+
);
|
|
755
|
+
}
|
|
756
|
+
rename(oldPath, newPath, cb) {
|
|
757
|
+
if (this.server)
|
|
758
|
+
throw new Error('Client-only method called in server mode');
|
|
759
|
+
|
|
760
|
+
/*
|
|
761
|
+
uint32 id
|
|
762
|
+
string oldpath
|
|
763
|
+
string newpath
|
|
764
|
+
*/
|
|
765
|
+
const oldLen = Buffer.byteLength(oldPath);
|
|
766
|
+
const newLen = Buffer.byteLength(newPath);
|
|
767
|
+
let p = 9;
|
|
768
|
+
const buf = Buffer.allocUnsafe(4 + 1 + 4 + 4 + oldLen + 4 + newLen);
|
|
769
|
+
|
|
770
|
+
writeUInt32BE(buf, buf.length - 4, 0);
|
|
771
|
+
buf[4] = REQUEST.RENAME;
|
|
772
|
+
const reqid = this._writeReqid = (this._writeReqid + 1) & MAX_REQID;
|
|
773
|
+
writeUInt32BE(buf, reqid, 5);
|
|
774
|
+
|
|
775
|
+
writeUInt32BE(buf, oldLen, p);
|
|
776
|
+
buf.utf8Write(oldPath, p += 4, oldLen);
|
|
777
|
+
writeUInt32BE(buf, newLen, p += oldLen);
|
|
778
|
+
buf.utf8Write(newPath, p += 4, newLen);
|
|
779
|
+
|
|
780
|
+
this._requests[reqid] = { cb };
|
|
781
|
+
|
|
782
|
+
const isBuffered = sendOrBuffer(this, buf);
|
|
783
|
+
this._debug && this._debug(
|
|
784
|
+
`SFTP: Outbound: ${isBuffered ? 'Buffered' : 'Sending'} RENAME`
|
|
785
|
+
);
|
|
786
|
+
}
|
|
787
|
+
mkdir(path, attrs, cb) {
|
|
788
|
+
if (this.server)
|
|
789
|
+
throw new Error('Client-only method called in server mode');
|
|
790
|
+
|
|
791
|
+
let flags = 0;
|
|
792
|
+
let attrsLen = 0;
|
|
793
|
+
|
|
794
|
+
if (typeof attrs === 'function') {
|
|
795
|
+
cb = attrs;
|
|
796
|
+
attrs = undefined;
|
|
797
|
+
}
|
|
798
|
+
if (typeof attrs === 'object' && attrs !== null) {
|
|
799
|
+
attrs = attrsToBytes(attrs);
|
|
800
|
+
flags = attrs.flags;
|
|
801
|
+
attrsLen = attrs.nb;
|
|
802
|
+
}
|
|
803
|
+
|
|
804
|
+
/*
|
|
805
|
+
uint32 id
|
|
806
|
+
string path
|
|
807
|
+
ATTRS attrs
|
|
808
|
+
*/
|
|
809
|
+
const pathLen = Buffer.byteLength(path);
|
|
810
|
+
let p = 9;
|
|
811
|
+
const buf = Buffer.allocUnsafe(4 + 1 + 4 + 4 + pathLen + 4 + attrsLen);
|
|
812
|
+
|
|
813
|
+
writeUInt32BE(buf, buf.length - 4, 0);
|
|
814
|
+
buf[4] = REQUEST.MKDIR;
|
|
815
|
+
const reqid = this._writeReqid = (this._writeReqid + 1) & MAX_REQID;
|
|
816
|
+
writeUInt32BE(buf, reqid, 5);
|
|
817
|
+
|
|
818
|
+
writeUInt32BE(buf, pathLen, p);
|
|
819
|
+
buf.utf8Write(path, p += 4, pathLen);
|
|
820
|
+
writeUInt32BE(buf, flags, p += pathLen);
|
|
821
|
+
if (attrsLen) {
|
|
822
|
+
p += 4;
|
|
823
|
+
|
|
824
|
+
if (attrsLen === ATTRS_BUF.length)
|
|
825
|
+
buf.set(ATTRS_BUF, p);
|
|
826
|
+
else
|
|
827
|
+
bufferCopy(ATTRS_BUF, buf, 0, attrsLen, p);
|
|
828
|
+
|
|
829
|
+
p += attrsLen;
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
this._requests[reqid] = { cb };
|
|
833
|
+
|
|
834
|
+
const isBuffered = sendOrBuffer(this, buf);
|
|
835
|
+
this._debug && this._debug(
|
|
836
|
+
`SFTP: Outbound: ${isBuffered ? 'Buffered' : 'Sending'} MKDIR`
|
|
837
|
+
);
|
|
838
|
+
}
|
|
839
|
+
rmdir(path, cb) {
|
|
840
|
+
if (this.server)
|
|
841
|
+
throw new Error('Client-only method called in server mode');
|
|
842
|
+
|
|
843
|
+
/*
|
|
844
|
+
uint32 id
|
|
845
|
+
string path
|
|
846
|
+
*/
|
|
847
|
+
const pathLen = Buffer.byteLength(path);
|
|
848
|
+
let p = 9;
|
|
849
|
+
const buf = Buffer.allocUnsafe(4 + 1 + 4 + 4 + pathLen);
|
|
850
|
+
|
|
851
|
+
writeUInt32BE(buf, buf.length - 4, 0);
|
|
852
|
+
buf[4] = REQUEST.RMDIR;
|
|
853
|
+
const reqid = this._writeReqid = (this._writeReqid + 1) & MAX_REQID;
|
|
854
|
+
writeUInt32BE(buf, reqid, 5);
|
|
855
|
+
|
|
856
|
+
writeUInt32BE(buf, pathLen, p);
|
|
857
|
+
buf.utf8Write(path, p += 4, pathLen);
|
|
858
|
+
|
|
859
|
+
this._requests[reqid] = { cb };
|
|
860
|
+
|
|
861
|
+
const isBuffered = sendOrBuffer(this, buf);
|
|
862
|
+
this._debug && this._debug(
|
|
863
|
+
`SFTP: Outbound: ${isBuffered ? 'Buffered' : 'Sending'} RMDIR`
|
|
864
|
+
);
|
|
865
|
+
}
|
|
866
|
+
readdir(where, opts, cb) {
|
|
867
|
+
if (this.server)
|
|
868
|
+
throw new Error('Client-only method called in server mode');
|
|
869
|
+
|
|
870
|
+
if (typeof opts === 'function') {
|
|
871
|
+
cb = opts;
|
|
872
|
+
opts = {};
|
|
873
|
+
}
|
|
874
|
+
if (typeof opts !== 'object' || opts === null)
|
|
875
|
+
opts = {};
|
|
876
|
+
|
|
877
|
+
const doFilter = (opts && opts.full ? false : true);
|
|
878
|
+
|
|
879
|
+
if (!Buffer.isBuffer(where) && typeof where !== 'string')
|
|
880
|
+
throw new Error('missing directory handle or path');
|
|
881
|
+
|
|
882
|
+
if (typeof where === 'string') {
|
|
883
|
+
const entries = [];
|
|
884
|
+
let e = 0;
|
|
885
|
+
|
|
886
|
+
const reread = (err, handle) => {
|
|
887
|
+
if (err)
|
|
888
|
+
return cb(err);
|
|
889
|
+
|
|
890
|
+
this.readdir(handle, opts, (err, list) => {
|
|
891
|
+
const eof = (err && err.code === STATUS_CODE.EOF);
|
|
892
|
+
|
|
893
|
+
if (err && !eof)
|
|
894
|
+
return this.close(handle, () => cb(err));
|
|
895
|
+
|
|
896
|
+
if (eof) {
|
|
897
|
+
return this.close(handle, (err) => {
|
|
898
|
+
if (err)
|
|
899
|
+
return cb(err);
|
|
900
|
+
cb(undefined, entries);
|
|
901
|
+
});
|
|
902
|
+
}
|
|
903
|
+
|
|
904
|
+
for (let i = 0; i < list.length; ++i, ++e)
|
|
905
|
+
entries[e] = list[i];
|
|
906
|
+
|
|
907
|
+
reread(undefined, handle);
|
|
908
|
+
});
|
|
909
|
+
};
|
|
910
|
+
return this.opendir(where, reread);
|
|
911
|
+
}
|
|
912
|
+
|
|
913
|
+
/*
|
|
914
|
+
uint32 id
|
|
915
|
+
string handle
|
|
916
|
+
*/
|
|
917
|
+
const handleLen = where.length;
|
|
918
|
+
let p = 9;
|
|
919
|
+
const buf = Buffer.allocUnsafe(4 + 1 + 4 + 4 + handleLen);
|
|
920
|
+
|
|
921
|
+
writeUInt32BE(buf, buf.length - 4, 0);
|
|
922
|
+
buf[4] = REQUEST.READDIR;
|
|
923
|
+
const reqid = this._writeReqid = (this._writeReqid + 1) & MAX_REQID;
|
|
924
|
+
writeUInt32BE(buf, reqid, 5);
|
|
925
|
+
|
|
926
|
+
writeUInt32BE(buf, handleLen, p);
|
|
927
|
+
buf.set(where, p += 4);
|
|
928
|
+
|
|
929
|
+
this._requests[reqid] = {
|
|
930
|
+
cb: (doFilter
|
|
931
|
+
? (err, list) => {
|
|
932
|
+
if (typeof cb !== 'function')
|
|
933
|
+
return;
|
|
934
|
+
if (err)
|
|
935
|
+
return cb(err);
|
|
936
|
+
|
|
937
|
+
for (let i = list.length - 1; i >= 0; --i) {
|
|
938
|
+
if (list[i].filename === '.' || list[i].filename === '..')
|
|
939
|
+
list.splice(i, 1);
|
|
940
|
+
}
|
|
941
|
+
|
|
942
|
+
cb(undefined, list);
|
|
943
|
+
}
|
|
944
|
+
: cb)
|
|
945
|
+
};
|
|
946
|
+
|
|
947
|
+
const isBuffered = sendOrBuffer(this, buf);
|
|
948
|
+
this._debug && this._debug(
|
|
949
|
+
`SFTP: Outbound: ${isBuffered ? 'Buffered' : 'Sending'} READDIR`
|
|
950
|
+
);
|
|
951
|
+
}
|
|
952
|
+
fstat(handle, cb) {
|
|
953
|
+
if (this.server)
|
|
954
|
+
throw new Error('Client-only method called in server mode');
|
|
955
|
+
|
|
956
|
+
if (!Buffer.isBuffer(handle))
|
|
957
|
+
throw new Error('handle is not a Buffer');
|
|
958
|
+
|
|
959
|
+
/*
|
|
960
|
+
uint32 id
|
|
961
|
+
string handle
|
|
962
|
+
*/
|
|
963
|
+
const handleLen = handle.length;
|
|
964
|
+
let p = 9;
|
|
965
|
+
const buf = Buffer.allocUnsafe(4 + 1 + 4 + 4 + handleLen);
|
|
966
|
+
|
|
967
|
+
writeUInt32BE(buf, buf.length - 4, 0);
|
|
968
|
+
buf[4] = REQUEST.FSTAT;
|
|
969
|
+
const reqid = this._writeReqid = (this._writeReqid + 1) & MAX_REQID;
|
|
970
|
+
writeUInt32BE(buf, reqid, 5);
|
|
971
|
+
|
|
972
|
+
writeUInt32BE(buf, handleLen, p);
|
|
973
|
+
buf.set(handle, p += 4);
|
|
974
|
+
|
|
975
|
+
this._requests[reqid] = { cb };
|
|
976
|
+
|
|
977
|
+
const isBuffered = sendOrBuffer(this, buf);
|
|
978
|
+
this._debug && this._debug(
|
|
979
|
+
`SFTP: Outbound: ${isBuffered ? 'Buffered' : 'Sending'} FSTAT`
|
|
980
|
+
);
|
|
981
|
+
}
|
|
982
|
+
stat(path, cb) {
|
|
983
|
+
if (this.server)
|
|
984
|
+
throw new Error('Client-only method called in server mode');
|
|
985
|
+
|
|
986
|
+
/*
|
|
987
|
+
uint32 id
|
|
988
|
+
string path
|
|
989
|
+
*/
|
|
990
|
+
const pathLen = Buffer.byteLength(path);
|
|
991
|
+
let p = 9;
|
|
992
|
+
const buf = Buffer.allocUnsafe(4 + 1 + 4 + 4 + pathLen);
|
|
993
|
+
|
|
994
|
+
writeUInt32BE(buf, buf.length - 4, 0);
|
|
995
|
+
buf[4] = REQUEST.STAT;
|
|
996
|
+
const reqid = this._writeReqid = (this._writeReqid + 1) & MAX_REQID;
|
|
997
|
+
writeUInt32BE(buf, reqid, 5);
|
|
998
|
+
|
|
999
|
+
writeUInt32BE(buf, pathLen, p);
|
|
1000
|
+
buf.utf8Write(path, p += 4, pathLen);
|
|
1001
|
+
|
|
1002
|
+
this._requests[reqid] = { cb };
|
|
1003
|
+
|
|
1004
|
+
const isBuffered = sendOrBuffer(this, buf);
|
|
1005
|
+
this._debug && this._debug(
|
|
1006
|
+
`SFTP: Outbound: ${isBuffered ? 'Buffered' : 'Sending'} STAT`
|
|
1007
|
+
);
|
|
1008
|
+
}
|
|
1009
|
+
lstat(path, cb) {
|
|
1010
|
+
if (this.server)
|
|
1011
|
+
throw new Error('Client-only method called in server mode');
|
|
1012
|
+
|
|
1013
|
+
/*
|
|
1014
|
+
uint32 id
|
|
1015
|
+
string path
|
|
1016
|
+
*/
|
|
1017
|
+
const pathLen = Buffer.byteLength(path);
|
|
1018
|
+
let p = 9;
|
|
1019
|
+
const buf = Buffer.allocUnsafe(4 + 1 + 4 + 4 + pathLen);
|
|
1020
|
+
|
|
1021
|
+
writeUInt32BE(buf, buf.length - 4, 0);
|
|
1022
|
+
buf[4] = REQUEST.LSTAT;
|
|
1023
|
+
const reqid = this._writeReqid = (this._writeReqid + 1) & MAX_REQID;
|
|
1024
|
+
writeUInt32BE(buf, reqid, 5);
|
|
1025
|
+
|
|
1026
|
+
writeUInt32BE(buf, pathLen, p);
|
|
1027
|
+
buf.utf8Write(path, p += 4, pathLen);
|
|
1028
|
+
|
|
1029
|
+
this._requests[reqid] = { cb };
|
|
1030
|
+
|
|
1031
|
+
const isBuffered = sendOrBuffer(this, buf);
|
|
1032
|
+
this._debug && this._debug(
|
|
1033
|
+
`SFTP: Outbound: ${isBuffered ? 'Buffered' : 'Sending'} LSTAT`
|
|
1034
|
+
);
|
|
1035
|
+
}
|
|
1036
|
+
opendir(path, cb) {
|
|
1037
|
+
if (this.server)
|
|
1038
|
+
throw new Error('Client-only method called in server mode');
|
|
1039
|
+
|
|
1040
|
+
/*
|
|
1041
|
+
uint32 id
|
|
1042
|
+
string path
|
|
1043
|
+
*/
|
|
1044
|
+
const pathLen = Buffer.byteLength(path);
|
|
1045
|
+
let p = 9;
|
|
1046
|
+
const buf = Buffer.allocUnsafe(4 + 1 + 4 + 4 + pathLen);
|
|
1047
|
+
|
|
1048
|
+
writeUInt32BE(buf, buf.length - 4, 0);
|
|
1049
|
+
buf[4] = REQUEST.OPENDIR;
|
|
1050
|
+
const reqid = this._writeReqid = (this._writeReqid + 1) & MAX_REQID;
|
|
1051
|
+
writeUInt32BE(buf, reqid, 5);
|
|
1052
|
+
|
|
1053
|
+
writeUInt32BE(buf, pathLen, p);
|
|
1054
|
+
buf.utf8Write(path, p += 4, pathLen);
|
|
1055
|
+
|
|
1056
|
+
this._requests[reqid] = { cb };
|
|
1057
|
+
|
|
1058
|
+
const isBuffered = sendOrBuffer(this, buf);
|
|
1059
|
+
this._debug && this._debug(
|
|
1060
|
+
`SFTP: Outbound: ${isBuffered ? 'Buffered' : 'Sending'} OPENDIR`
|
|
1061
|
+
);
|
|
1062
|
+
}
|
|
1063
|
+
setstat(path, attrs, cb) {
|
|
1064
|
+
if (this.server)
|
|
1065
|
+
throw new Error('Client-only method called in server mode');
|
|
1066
|
+
|
|
1067
|
+
let flags = 0;
|
|
1068
|
+
let attrsLen = 0;
|
|
1069
|
+
|
|
1070
|
+
if (typeof attrs === 'object' && attrs !== null) {
|
|
1071
|
+
attrs = attrsToBytes(attrs);
|
|
1072
|
+
flags = attrs.flags;
|
|
1073
|
+
attrsLen = attrs.nb;
|
|
1074
|
+
} else if (typeof attrs === 'function') {
|
|
1075
|
+
cb = attrs;
|
|
1076
|
+
}
|
|
1077
|
+
|
|
1078
|
+
/*
|
|
1079
|
+
uint32 id
|
|
1080
|
+
string path
|
|
1081
|
+
ATTRS attrs
|
|
1082
|
+
*/
|
|
1083
|
+
const pathLen = Buffer.byteLength(path);
|
|
1084
|
+
let p = 9;
|
|
1085
|
+
const buf = Buffer.allocUnsafe(4 + 1 + 4 + 4 + pathLen + 4 + attrsLen);
|
|
1086
|
+
|
|
1087
|
+
writeUInt32BE(buf, buf.length - 4, 0);
|
|
1088
|
+
buf[4] = REQUEST.SETSTAT;
|
|
1089
|
+
const reqid = this._writeReqid = (this._writeReqid + 1) & MAX_REQID;
|
|
1090
|
+
writeUInt32BE(buf, reqid, 5);
|
|
1091
|
+
|
|
1092
|
+
writeUInt32BE(buf, pathLen, p);
|
|
1093
|
+
buf.utf8Write(path, p += 4, pathLen);
|
|
1094
|
+
writeUInt32BE(buf, flags, p += pathLen);
|
|
1095
|
+
if (attrsLen) {
|
|
1096
|
+
p += 4;
|
|
1097
|
+
|
|
1098
|
+
if (attrsLen === ATTRS_BUF.length)
|
|
1099
|
+
buf.set(ATTRS_BUF, p);
|
|
1100
|
+
else
|
|
1101
|
+
bufferCopy(ATTRS_BUF, buf, 0, attrsLen, p);
|
|
1102
|
+
|
|
1103
|
+
p += attrsLen;
|
|
1104
|
+
}
|
|
1105
|
+
|
|
1106
|
+
this._requests[reqid] = { cb };
|
|
1107
|
+
|
|
1108
|
+
const isBuffered = sendOrBuffer(this, buf);
|
|
1109
|
+
this._debug && this._debug(
|
|
1110
|
+
`SFTP: Outbound: ${isBuffered ? 'Buffered' : 'Sending'} SETSTAT`
|
|
1111
|
+
);
|
|
1112
|
+
}
|
|
1113
|
+
fsetstat(handle, attrs, cb) {
|
|
1114
|
+
if (this.server)
|
|
1115
|
+
throw new Error('Client-only method called in server mode');
|
|
1116
|
+
|
|
1117
|
+
if (!Buffer.isBuffer(handle))
|
|
1118
|
+
throw new Error('handle is not a Buffer');
|
|
1119
|
+
|
|
1120
|
+
let flags = 0;
|
|
1121
|
+
let attrsLen = 0;
|
|
1122
|
+
|
|
1123
|
+
if (typeof attrs === 'object' && attrs !== null) {
|
|
1124
|
+
attrs = attrsToBytes(attrs);
|
|
1125
|
+
flags = attrs.flags;
|
|
1126
|
+
attrsLen = attrs.nb;
|
|
1127
|
+
} else if (typeof attrs === 'function') {
|
|
1128
|
+
cb = attrs;
|
|
1129
|
+
}
|
|
1130
|
+
|
|
1131
|
+
/*
|
|
1132
|
+
uint32 id
|
|
1133
|
+
string handle
|
|
1134
|
+
ATTRS attrs
|
|
1135
|
+
*/
|
|
1136
|
+
const handleLen = handle.length;
|
|
1137
|
+
let p = 9;
|
|
1138
|
+
const buf = Buffer.allocUnsafe(4 + 1 + 4 + 4 + handleLen + 4 + attrsLen);
|
|
1139
|
+
|
|
1140
|
+
writeUInt32BE(buf, buf.length - 4, 0);
|
|
1141
|
+
buf[4] = REQUEST.FSETSTAT;
|
|
1142
|
+
const reqid = this._writeReqid = (this._writeReqid + 1) & MAX_REQID;
|
|
1143
|
+
writeUInt32BE(buf, reqid, 5);
|
|
1144
|
+
|
|
1145
|
+
writeUInt32BE(buf, handleLen, p);
|
|
1146
|
+
buf.set(handle, p += 4);
|
|
1147
|
+
writeUInt32BE(buf, flags, p += handleLen);
|
|
1148
|
+
if (attrsLen) {
|
|
1149
|
+
p += 4;
|
|
1150
|
+
|
|
1151
|
+
if (attrsLen === ATTRS_BUF.length)
|
|
1152
|
+
buf.set(ATTRS_BUF, p);
|
|
1153
|
+
else
|
|
1154
|
+
bufferCopy(ATTRS_BUF, buf, 0, attrsLen, p);
|
|
1155
|
+
|
|
1156
|
+
p += attrsLen;
|
|
1157
|
+
}
|
|
1158
|
+
|
|
1159
|
+
this._requests[reqid] = { cb };
|
|
1160
|
+
|
|
1161
|
+
const isBuffered = sendOrBuffer(this, buf);
|
|
1162
|
+
this._debug && this._debug(
|
|
1163
|
+
`SFTP: Outbound: ${isBuffered ? 'Buffered' : 'Sending'} FSETSTAT`
|
|
1164
|
+
);
|
|
1165
|
+
}
|
|
1166
|
+
futimes(handle, atime, mtime, cb) {
|
|
1167
|
+
return this.fsetstat(handle, {
|
|
1168
|
+
atime: toUnixTimestamp(atime),
|
|
1169
|
+
mtime: toUnixTimestamp(mtime)
|
|
1170
|
+
}, cb);
|
|
1171
|
+
}
|
|
1172
|
+
utimes(path, atime, mtime, cb) {
|
|
1173
|
+
return this.setstat(path, {
|
|
1174
|
+
atime: toUnixTimestamp(atime),
|
|
1175
|
+
mtime: toUnixTimestamp(mtime)
|
|
1176
|
+
}, cb);
|
|
1177
|
+
}
|
|
1178
|
+
fchown(handle, uid, gid, cb) {
|
|
1179
|
+
return this.fsetstat(handle, {
|
|
1180
|
+
uid: uid,
|
|
1181
|
+
gid: gid
|
|
1182
|
+
}, cb);
|
|
1183
|
+
}
|
|
1184
|
+
chown(path, uid, gid, cb) {
|
|
1185
|
+
return this.setstat(path, {
|
|
1186
|
+
uid: uid,
|
|
1187
|
+
gid: gid
|
|
1188
|
+
}, cb);
|
|
1189
|
+
}
|
|
1190
|
+
fchmod(handle, mode, cb) {
|
|
1191
|
+
return this.fsetstat(handle, {
|
|
1192
|
+
mode: mode
|
|
1193
|
+
}, cb);
|
|
1194
|
+
}
|
|
1195
|
+
chmod(path, mode, cb) {
|
|
1196
|
+
return this.setstat(path, {
|
|
1197
|
+
mode: mode
|
|
1198
|
+
}, cb);
|
|
1199
|
+
}
|
|
1200
|
+
readlink(path, cb) {
|
|
1201
|
+
if (this.server)
|
|
1202
|
+
throw new Error('Client-only method called in server mode');
|
|
1203
|
+
|
|
1204
|
+
/*
|
|
1205
|
+
uint32 id
|
|
1206
|
+
string path
|
|
1207
|
+
*/
|
|
1208
|
+
const pathLen = Buffer.byteLength(path);
|
|
1209
|
+
let p = 9;
|
|
1210
|
+
const buf = Buffer.allocUnsafe(4 + 1 + 4 + 4 + pathLen);
|
|
1211
|
+
|
|
1212
|
+
writeUInt32BE(buf, buf.length - 4, 0);
|
|
1213
|
+
buf[4] = REQUEST.READLINK;
|
|
1214
|
+
const reqid = this._writeReqid = (this._writeReqid + 1) & MAX_REQID;
|
|
1215
|
+
writeUInt32BE(buf, reqid, 5);
|
|
1216
|
+
|
|
1217
|
+
writeUInt32BE(buf, pathLen, p);
|
|
1218
|
+
buf.utf8Write(path, p += 4, pathLen);
|
|
1219
|
+
|
|
1220
|
+
this._requests[reqid] = {
|
|
1221
|
+
cb: (err, names) => {
|
|
1222
|
+
if (typeof cb !== 'function')
|
|
1223
|
+
return;
|
|
1224
|
+
if (err)
|
|
1225
|
+
return cb(err);
|
|
1226
|
+
if (!names || !names.length)
|
|
1227
|
+
return cb(new Error('Response missing link info'));
|
|
1228
|
+
cb(undefined, names[0].filename);
|
|
1229
|
+
}
|
|
1230
|
+
};
|
|
1231
|
+
|
|
1232
|
+
const isBuffered = sendOrBuffer(this, buf);
|
|
1233
|
+
this._debug && this._debug(
|
|
1234
|
+
`SFTP: Outbound: ${isBuffered ? 'Buffered' : 'Sending'} READLINK`
|
|
1235
|
+
);
|
|
1236
|
+
}
|
|
1237
|
+
symlink(targetPath, linkPath, cb) {
|
|
1238
|
+
if (this.server)
|
|
1239
|
+
throw new Error('Client-only method called in server mode');
|
|
1240
|
+
|
|
1241
|
+
/*
|
|
1242
|
+
uint32 id
|
|
1243
|
+
string linkpath
|
|
1244
|
+
string targetpath
|
|
1245
|
+
*/
|
|
1246
|
+
const linkLen = Buffer.byteLength(linkPath);
|
|
1247
|
+
const targetLen = Buffer.byteLength(targetPath);
|
|
1248
|
+
let p = 9;
|
|
1249
|
+
const buf = Buffer.allocUnsafe(4 + 1 + 4 + 4 + linkLen + 4 + targetLen);
|
|
1250
|
+
|
|
1251
|
+
writeUInt32BE(buf, buf.length - 4, 0);
|
|
1252
|
+
buf[4] = REQUEST.SYMLINK;
|
|
1253
|
+
const reqid = this._writeReqid = (this._writeReqid + 1) & MAX_REQID;
|
|
1254
|
+
writeUInt32BE(buf, reqid, 5);
|
|
1255
|
+
|
|
1256
|
+
if (this._isOpenSSH) {
|
|
1257
|
+
// OpenSSH has linkpath and targetpath positions switched
|
|
1258
|
+
writeUInt32BE(buf, targetLen, p);
|
|
1259
|
+
buf.utf8Write(targetPath, p += 4, targetLen);
|
|
1260
|
+
writeUInt32BE(buf, linkLen, p += targetLen);
|
|
1261
|
+
buf.utf8Write(linkPath, p += 4, linkLen);
|
|
1262
|
+
} else {
|
|
1263
|
+
writeUInt32BE(buf, linkLen, p);
|
|
1264
|
+
buf.utf8Write(linkPath, p += 4, linkLen);
|
|
1265
|
+
writeUInt32BE(buf, targetLen, p += linkLen);
|
|
1266
|
+
buf.utf8Write(targetPath, p += 4, targetLen);
|
|
1267
|
+
}
|
|
1268
|
+
|
|
1269
|
+
this._requests[reqid] = { cb };
|
|
1270
|
+
|
|
1271
|
+
const isBuffered = sendOrBuffer(this, buf);
|
|
1272
|
+
this._debug && this._debug(
|
|
1273
|
+
`SFTP: Outbound: ${isBuffered ? 'Buffered' : 'Sending'} SYMLINK`
|
|
1274
|
+
);
|
|
1275
|
+
}
|
|
1276
|
+
realpath(path, cb) {
|
|
1277
|
+
if (this.server)
|
|
1278
|
+
throw new Error('Client-only method called in server mode');
|
|
1279
|
+
|
|
1280
|
+
/*
|
|
1281
|
+
uint32 id
|
|
1282
|
+
string path
|
|
1283
|
+
*/
|
|
1284
|
+
const pathLen = Buffer.byteLength(path);
|
|
1285
|
+
let p = 9;
|
|
1286
|
+
const buf = Buffer.allocUnsafe(4 + 1 + 4 + 4 + pathLen);
|
|
1287
|
+
|
|
1288
|
+
writeUInt32BE(buf, buf.length - 4, 0);
|
|
1289
|
+
buf[4] = REQUEST.REALPATH;
|
|
1290
|
+
const reqid = this._writeReqid = (this._writeReqid + 1) & MAX_REQID;
|
|
1291
|
+
writeUInt32BE(buf, reqid, 5);
|
|
1292
|
+
|
|
1293
|
+
writeUInt32BE(buf, pathLen, p);
|
|
1294
|
+
buf.utf8Write(path, p += 4, pathLen);
|
|
1295
|
+
|
|
1296
|
+
this._requests[reqid] = {
|
|
1297
|
+
cb: (err, names) => {
|
|
1298
|
+
if (typeof cb !== 'function')
|
|
1299
|
+
return;
|
|
1300
|
+
if (err)
|
|
1301
|
+
return cb(err);
|
|
1302
|
+
if (!names || !names.length)
|
|
1303
|
+
return cb(new Error('Response missing path info'));
|
|
1304
|
+
cb(undefined, names[0].filename);
|
|
1305
|
+
}
|
|
1306
|
+
};
|
|
1307
|
+
|
|
1308
|
+
const isBuffered = sendOrBuffer(this, buf);
|
|
1309
|
+
this._debug && this._debug(
|
|
1310
|
+
`SFTP: Outbound: ${isBuffered ? 'Buffered' : 'Sending'} REALPATH`
|
|
1311
|
+
);
|
|
1312
|
+
}
|
|
1313
|
+
// extended requests
|
|
1314
|
+
ext_openssh_rename(oldPath, newPath, cb) {
|
|
1315
|
+
if (this.server)
|
|
1316
|
+
throw new Error('Client-only method called in server mode');
|
|
1317
|
+
|
|
1318
|
+
const ext = this._extensions['posix-rename@openssh.com'];
|
|
1319
|
+
if (!ext || ext !== '1')
|
|
1320
|
+
throw new Error('Server does not support this extended request');
|
|
1321
|
+
|
|
1322
|
+
/*
|
|
1323
|
+
uint32 id
|
|
1324
|
+
string "posix-rename@openssh.com"
|
|
1325
|
+
string oldpath
|
|
1326
|
+
string newpath
|
|
1327
|
+
*/
|
|
1328
|
+
const oldLen = Buffer.byteLength(oldPath);
|
|
1329
|
+
const newLen = Buffer.byteLength(newPath);
|
|
1330
|
+
let p = 9;
|
|
1331
|
+
const buf =
|
|
1332
|
+
Buffer.allocUnsafe(4 + 1 + 4 + 4 + 24 + 4 + oldLen + 4 + newLen);
|
|
1333
|
+
|
|
1334
|
+
writeUInt32BE(buf, buf.length - 4, 0);
|
|
1335
|
+
buf[4] = REQUEST.EXTENDED;
|
|
1336
|
+
const reqid = this._writeReqid = (this._writeReqid + 1) & MAX_REQID;
|
|
1337
|
+
writeUInt32BE(buf, reqid, 5);
|
|
1338
|
+
|
|
1339
|
+
writeUInt32BE(buf, 24, p);
|
|
1340
|
+
buf.utf8Write('posix-rename@openssh.com', p += 4, 24);
|
|
1341
|
+
writeUInt32BE(buf, oldLen, p += 24);
|
|
1342
|
+
buf.utf8Write(oldPath, p += 4, oldLen);
|
|
1343
|
+
writeUInt32BE(buf, newLen, p += oldLen);
|
|
1344
|
+
buf.utf8Write(newPath, p += 4, newLen);
|
|
1345
|
+
|
|
1346
|
+
this._requests[reqid] = { cb };
|
|
1347
|
+
|
|
1348
|
+
const isBuffered = sendOrBuffer(this, buf);
|
|
1349
|
+
if (this._debug) {
|
|
1350
|
+
const which = (isBuffered ? 'Buffered' : 'Sending');
|
|
1351
|
+
this._debug(`SFTP: Outbound: ${which} posix-rename@openssh.com`);
|
|
1352
|
+
}
|
|
1353
|
+
}
|
|
1354
|
+
ext_openssh_statvfs(path, cb) {
|
|
1355
|
+
if (this.server)
|
|
1356
|
+
throw new Error('Client-only method called in server mode');
|
|
1357
|
+
|
|
1358
|
+
const ext = this._extensions['statvfs@openssh.com'];
|
|
1359
|
+
if (!ext || ext !== '2')
|
|
1360
|
+
throw new Error('Server does not support this extended request');
|
|
1361
|
+
|
|
1362
|
+
/*
|
|
1363
|
+
uint32 id
|
|
1364
|
+
string "statvfs@openssh.com"
|
|
1365
|
+
string path
|
|
1366
|
+
*/
|
|
1367
|
+
const pathLen = Buffer.byteLength(path);
|
|
1368
|
+
let p = 9;
|
|
1369
|
+
const buf = Buffer.allocUnsafe(4 + 1 + 4 + 4 + 19 + 4 + pathLen);
|
|
1370
|
+
|
|
1371
|
+
writeUInt32BE(buf, buf.length - 4, 0);
|
|
1372
|
+
buf[4] = REQUEST.EXTENDED;
|
|
1373
|
+
const reqid = this._writeReqid = (this._writeReqid + 1) & MAX_REQID;
|
|
1374
|
+
writeUInt32BE(buf, reqid, 5);
|
|
1375
|
+
|
|
1376
|
+
writeUInt32BE(buf, 19, p);
|
|
1377
|
+
buf.utf8Write('statvfs@openssh.com', p += 4, 19);
|
|
1378
|
+
writeUInt32BE(buf, pathLen, p += 19);
|
|
1379
|
+
buf.utf8Write(path, p += 4, pathLen);
|
|
1380
|
+
|
|
1381
|
+
this._requests[reqid] = { extended: 'statvfs@openssh.com', cb };
|
|
1382
|
+
|
|
1383
|
+
const isBuffered = sendOrBuffer(this, buf);
|
|
1384
|
+
if (this._debug) {
|
|
1385
|
+
const which = (isBuffered ? 'Buffered' : 'Sending');
|
|
1386
|
+
this._debug(`SFTP: Outbound: ${which} statvfs@openssh.com`);
|
|
1387
|
+
}
|
|
1388
|
+
}
|
|
1389
|
+
ext_openssh_fstatvfs(handle, cb) {
|
|
1390
|
+
if (this.server)
|
|
1391
|
+
throw new Error('Client-only method called in server mode');
|
|
1392
|
+
|
|
1393
|
+
const ext = this._extensions['fstatvfs@openssh.com'];
|
|
1394
|
+
if (!ext || ext !== '2')
|
|
1395
|
+
throw new Error('Server does not support this extended request');
|
|
1396
|
+
if (!Buffer.isBuffer(handle))
|
|
1397
|
+
throw new Error('handle is not a Buffer');
|
|
1398
|
+
|
|
1399
|
+
/*
|
|
1400
|
+
uint32 id
|
|
1401
|
+
string "fstatvfs@openssh.com"
|
|
1402
|
+
string handle
|
|
1403
|
+
*/
|
|
1404
|
+
const handleLen = handle.length;
|
|
1405
|
+
let p = 9;
|
|
1406
|
+
const buf = Buffer.allocUnsafe(4 + 1 + 4 + 4 + 20 + 4 + handleLen);
|
|
1407
|
+
|
|
1408
|
+
writeUInt32BE(buf, buf.length - 4, 0);
|
|
1409
|
+
buf[4] = REQUEST.EXTENDED;
|
|
1410
|
+
const reqid = this._writeReqid = (this._writeReqid + 1) & MAX_REQID;
|
|
1411
|
+
writeUInt32BE(buf, reqid, 5);
|
|
1412
|
+
|
|
1413
|
+
writeUInt32BE(buf, 20, p);
|
|
1414
|
+
buf.utf8Write('fstatvfs@openssh.com', p += 4, 20);
|
|
1415
|
+
writeUInt32BE(buf, handleLen, p += 20);
|
|
1416
|
+
buf.set(handle, p += 4);
|
|
1417
|
+
|
|
1418
|
+
this._requests[reqid] = { extended: 'fstatvfs@openssh.com', cb };
|
|
1419
|
+
|
|
1420
|
+
const isBuffered = sendOrBuffer(this, buf);
|
|
1421
|
+
if (this._debug) {
|
|
1422
|
+
const which = (isBuffered ? 'Buffered' : 'Sending');
|
|
1423
|
+
this._debug(`SFTP: Outbound: ${which} fstatvfs@openssh.com`);
|
|
1424
|
+
}
|
|
1425
|
+
}
|
|
1426
|
+
ext_openssh_hardlink(oldPath, newPath, cb) {
|
|
1427
|
+
if (this.server)
|
|
1428
|
+
throw new Error('Client-only method called in server mode');
|
|
1429
|
+
|
|
1430
|
+
const ext = this._extensions['hardlink@openssh.com'];
|
|
1431
|
+
if (ext !== '1')
|
|
1432
|
+
throw new Error('Server does not support this extended request');
|
|
1433
|
+
|
|
1434
|
+
/*
|
|
1435
|
+
uint32 id
|
|
1436
|
+
string "hardlink@openssh.com"
|
|
1437
|
+
string oldpath
|
|
1438
|
+
string newpath
|
|
1439
|
+
*/
|
|
1440
|
+
const oldLen = Buffer.byteLength(oldPath);
|
|
1441
|
+
const newLen = Buffer.byteLength(newPath);
|
|
1442
|
+
let p = 9;
|
|
1443
|
+
const buf =
|
|
1444
|
+
Buffer.allocUnsafe(4 + 1 + 4 + 4 + 20 + 4 + oldLen + 4 + newLen);
|
|
1445
|
+
|
|
1446
|
+
writeUInt32BE(buf, buf.length - 4, 0);
|
|
1447
|
+
buf[4] = REQUEST.EXTENDED;
|
|
1448
|
+
const reqid = this._writeReqid = (this._writeReqid + 1) & MAX_REQID;
|
|
1449
|
+
writeUInt32BE(buf, reqid, 5);
|
|
1450
|
+
|
|
1451
|
+
writeUInt32BE(buf, 20, p);
|
|
1452
|
+
buf.utf8Write('hardlink@openssh.com', p += 4, 20);
|
|
1453
|
+
writeUInt32BE(buf, oldLen, p += 20);
|
|
1454
|
+
buf.utf8Write(oldPath, p += 4, oldLen);
|
|
1455
|
+
writeUInt32BE(buf, newLen, p += oldLen);
|
|
1456
|
+
buf.utf8Write(newPath, p += 4, newLen);
|
|
1457
|
+
|
|
1458
|
+
this._requests[reqid] = { cb };
|
|
1459
|
+
|
|
1460
|
+
const isBuffered = sendOrBuffer(this, buf);
|
|
1461
|
+
if (this._debug) {
|
|
1462
|
+
const which = (isBuffered ? 'Buffered' : 'Sending');
|
|
1463
|
+
this._debug(`SFTP: Outbound: ${which} hardlink@openssh.com`);
|
|
1464
|
+
}
|
|
1465
|
+
}
|
|
1466
|
+
ext_openssh_fsync(handle, cb) {
|
|
1467
|
+
if (this.server)
|
|
1468
|
+
throw new Error('Client-only method called in server mode');
|
|
1469
|
+
|
|
1470
|
+
const ext = this._extensions['fsync@openssh.com'];
|
|
1471
|
+
if (ext !== '1')
|
|
1472
|
+
throw new Error('Server does not support this extended request');
|
|
1473
|
+
if (!Buffer.isBuffer(handle))
|
|
1474
|
+
throw new Error('handle is not a Buffer');
|
|
1475
|
+
|
|
1476
|
+
/*
|
|
1477
|
+
uint32 id
|
|
1478
|
+
string "fsync@openssh.com"
|
|
1479
|
+
string handle
|
|
1480
|
+
*/
|
|
1481
|
+
const handleLen = handle.length;
|
|
1482
|
+
let p = 9;
|
|
1483
|
+
const buf = Buffer.allocUnsafe(4 + 1 + 4 + 4 + 17 + 4 + handleLen);
|
|
1484
|
+
|
|
1485
|
+
writeUInt32BE(buf, buf.length - 4, 0);
|
|
1486
|
+
buf[4] = REQUEST.EXTENDED;
|
|
1487
|
+
const reqid = this._writeReqid = (this._writeReqid + 1) & MAX_REQID;
|
|
1488
|
+
writeUInt32BE(buf, reqid, 5);
|
|
1489
|
+
|
|
1490
|
+
writeUInt32BE(buf, 17, p);
|
|
1491
|
+
buf.utf8Write('fsync@openssh.com', p += 4, 17);
|
|
1492
|
+
writeUInt32BE(buf, handleLen, p += 17);
|
|
1493
|
+
buf.set(handle, p += 4);
|
|
1494
|
+
|
|
1495
|
+
this._requests[reqid] = { cb };
|
|
1496
|
+
|
|
1497
|
+
const isBuffered = sendOrBuffer(this, buf);
|
|
1498
|
+
this._debug && this._debug(
|
|
1499
|
+
`SFTP: Outbound: ${isBuffered ? 'Buffered' : 'Sending'} fsync@openssh.com`
|
|
1500
|
+
);
|
|
1501
|
+
}
|
|
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
|
+
}
|
|
1599
|
+
// ===========================================================================
|
|
1600
|
+
// Server-specific ===========================================================
|
|
1601
|
+
// ===========================================================================
|
|
1602
|
+
handle(reqid, handle) {
|
|
1603
|
+
if (!this.server)
|
|
1604
|
+
throw new Error('Server-only method called in client mode');
|
|
1605
|
+
|
|
1606
|
+
if (!Buffer.isBuffer(handle))
|
|
1607
|
+
throw new Error('handle is not a Buffer');
|
|
1608
|
+
|
|
1609
|
+
const handleLen = handle.length;
|
|
1610
|
+
|
|
1611
|
+
if (handleLen > 256)
|
|
1612
|
+
throw new Error('handle too large (> 256 bytes)');
|
|
1613
|
+
|
|
1614
|
+
let p = 9;
|
|
1615
|
+
const buf = Buffer.allocUnsafe(4 + 1 + 4 + 4 + handleLen);
|
|
1616
|
+
|
|
1617
|
+
writeUInt32BE(buf, buf.length - 4, 0);
|
|
1618
|
+
buf[4] = RESPONSE.HANDLE;
|
|
1619
|
+
writeUInt32BE(buf, reqid, 5);
|
|
1620
|
+
|
|
1621
|
+
writeUInt32BE(buf, handleLen, p);
|
|
1622
|
+
if (handleLen)
|
|
1623
|
+
buf.set(handle, p += 4);
|
|
1624
|
+
|
|
1625
|
+
const isBuffered = sendOrBuffer(this, buf);
|
|
1626
|
+
this._debug && this._debug(
|
|
1627
|
+
`SFTP: Outbound: ${isBuffered ? 'Buffered' : 'Sending'} HANDLE`
|
|
1628
|
+
);
|
|
1629
|
+
}
|
|
1630
|
+
status(reqid, code, message) {
|
|
1631
|
+
if (!this.server)
|
|
1632
|
+
throw new Error('Server-only method called in client mode');
|
|
1633
|
+
|
|
1634
|
+
if (!VALID_STATUS_CODES.has(code))
|
|
1635
|
+
throw new Error(`Bad status code: ${code}`);
|
|
1636
|
+
|
|
1637
|
+
message || (message = '');
|
|
1638
|
+
|
|
1639
|
+
const msgLen = Buffer.byteLength(message);
|
|
1640
|
+
let p = 9;
|
|
1641
|
+
const buf = Buffer.allocUnsafe(4 + 1 + 4 + 4 + 4 + msgLen + 4);
|
|
1642
|
+
|
|
1643
|
+
writeUInt32BE(buf, buf.length - 4, 0);
|
|
1644
|
+
buf[4] = RESPONSE.STATUS;
|
|
1645
|
+
writeUInt32BE(buf, reqid, 5);
|
|
1646
|
+
|
|
1647
|
+
writeUInt32BE(buf, code, p);
|
|
1648
|
+
|
|
1649
|
+
writeUInt32BE(buf, msgLen, p += 4);
|
|
1650
|
+
p += 4;
|
|
1651
|
+
if (msgLen) {
|
|
1652
|
+
buf.utf8Write(message, p, msgLen);
|
|
1653
|
+
p += msgLen;
|
|
1654
|
+
}
|
|
1655
|
+
|
|
1656
|
+
writeUInt32BE(buf, 0, p); // Empty language tag
|
|
1657
|
+
|
|
1658
|
+
const isBuffered = sendOrBuffer(this, buf);
|
|
1659
|
+
this._debug && this._debug(
|
|
1660
|
+
`SFTP: Outbound: ${isBuffered ? 'Buffered' : 'Sending'} STATUS`
|
|
1661
|
+
);
|
|
1662
|
+
}
|
|
1663
|
+
data(reqid, data, encoding) {
|
|
1664
|
+
if (!this.server)
|
|
1665
|
+
throw new Error('Server-only method called in client mode');
|
|
1666
|
+
|
|
1667
|
+
const isBuffer = Buffer.isBuffer(data);
|
|
1668
|
+
|
|
1669
|
+
if (!isBuffer && typeof data !== 'string')
|
|
1670
|
+
throw new Error('data is not a Buffer or string');
|
|
1671
|
+
|
|
1672
|
+
let isUTF8;
|
|
1673
|
+
if (!isBuffer && !encoding) {
|
|
1674
|
+
encoding = undefined;
|
|
1675
|
+
isUTF8 = true;
|
|
1676
|
+
}
|
|
1677
|
+
|
|
1678
|
+
const dataLen = (
|
|
1679
|
+
isBuffer
|
|
1680
|
+
? data.length
|
|
1681
|
+
: Buffer.byteLength(data, encoding)
|
|
1682
|
+
);
|
|
1683
|
+
let p = 9;
|
|
1684
|
+
const buf = Buffer.allocUnsafe(4 + 1 + 4 + 4 + dataLen);
|
|
1685
|
+
|
|
1686
|
+
writeUInt32BE(buf, buf.length - 4, 0);
|
|
1687
|
+
buf[4] = RESPONSE.DATA;
|
|
1688
|
+
writeUInt32BE(buf, reqid, 5);
|
|
1689
|
+
|
|
1690
|
+
writeUInt32BE(buf, dataLen, p);
|
|
1691
|
+
if (dataLen) {
|
|
1692
|
+
if (isBuffer)
|
|
1693
|
+
buf.set(data, p += 4);
|
|
1694
|
+
else if (isUTF8)
|
|
1695
|
+
buf.utf8Write(data, p += 4, dataLen);
|
|
1696
|
+
else
|
|
1697
|
+
buf.write(data, p += 4, dataLen, encoding);
|
|
1698
|
+
}
|
|
1699
|
+
|
|
1700
|
+
const isBuffered = sendOrBuffer(this, buf);
|
|
1701
|
+
this._debug && this._debug(
|
|
1702
|
+
`SFTP: Outbound: ${isBuffered ? 'Buffered' : 'Sending'} DATA`
|
|
1703
|
+
);
|
|
1704
|
+
}
|
|
1705
|
+
name(reqid, names) {
|
|
1706
|
+
if (!this.server)
|
|
1707
|
+
throw new Error('Server-only method called in client mode');
|
|
1708
|
+
|
|
1709
|
+
if (!Array.isArray(names)) {
|
|
1710
|
+
if (typeof names !== 'object' || names === null)
|
|
1711
|
+
throw new Error('names is not an object or array');
|
|
1712
|
+
names = [ names ];
|
|
1713
|
+
}
|
|
1714
|
+
|
|
1715
|
+
const count = names.length;
|
|
1716
|
+
let namesLen = 0;
|
|
1717
|
+
let nameAttrs;
|
|
1718
|
+
const attrs = [];
|
|
1719
|
+
|
|
1720
|
+
for (let i = 0; i < count; ++i) {
|
|
1721
|
+
const name = names[i];
|
|
1722
|
+
const filename = (
|
|
1723
|
+
!name || !name.filename || typeof name.filename !== 'string'
|
|
1724
|
+
? ''
|
|
1725
|
+
: name.filename
|
|
1726
|
+
);
|
|
1727
|
+
namesLen += 4 + Buffer.byteLength(filename);
|
|
1728
|
+
const longname = (
|
|
1729
|
+
!name || !name.longname || typeof name.longname !== 'string'
|
|
1730
|
+
? ''
|
|
1731
|
+
: name.longname
|
|
1732
|
+
);
|
|
1733
|
+
namesLen += 4 + Buffer.byteLength(longname);
|
|
1734
|
+
|
|
1735
|
+
if (typeof name.attrs === 'object' && name.attrs !== null) {
|
|
1736
|
+
nameAttrs = attrsToBytes(name.attrs);
|
|
1737
|
+
namesLen += 4 + nameAttrs.nb;
|
|
1738
|
+
|
|
1739
|
+
if (nameAttrs.nb) {
|
|
1740
|
+
let bytes;
|
|
1741
|
+
|
|
1742
|
+
if (nameAttrs.nb === ATTRS_BUF.length) {
|
|
1743
|
+
bytes = new Uint8Array(ATTRS_BUF);
|
|
1744
|
+
} else {
|
|
1745
|
+
bytes = new Uint8Array(nameAttrs.nb);
|
|
1746
|
+
bufferCopy(ATTRS_BUF, bytes, 0, nameAttrs.nb, 0);
|
|
1747
|
+
}
|
|
1748
|
+
|
|
1749
|
+
nameAttrs.bytes = bytes;
|
|
1750
|
+
}
|
|
1751
|
+
|
|
1752
|
+
attrs.push(nameAttrs);
|
|
1753
|
+
} else {
|
|
1754
|
+
namesLen += 4;
|
|
1755
|
+
attrs.push(null);
|
|
1756
|
+
}
|
|
1757
|
+
}
|
|
1758
|
+
|
|
1759
|
+
let p = 9;
|
|
1760
|
+
const buf = Buffer.allocUnsafe(4 + 1 + 4 + 4 + namesLen);
|
|
1761
|
+
|
|
1762
|
+
writeUInt32BE(buf, buf.length - 4, 0);
|
|
1763
|
+
buf[4] = RESPONSE.NAME;
|
|
1764
|
+
writeUInt32BE(buf, reqid, 5);
|
|
1765
|
+
|
|
1766
|
+
writeUInt32BE(buf, count, p);
|
|
1767
|
+
|
|
1768
|
+
p += 4;
|
|
1769
|
+
|
|
1770
|
+
for (let i = 0; i < count; ++i) {
|
|
1771
|
+
const name = names[i];
|
|
1772
|
+
|
|
1773
|
+
{
|
|
1774
|
+
const filename = (
|
|
1775
|
+
!name || !name.filename || typeof name.filename !== 'string'
|
|
1776
|
+
? ''
|
|
1777
|
+
: name.filename
|
|
1778
|
+
);
|
|
1779
|
+
const len = Buffer.byteLength(filename);
|
|
1780
|
+
writeUInt32BE(buf, len, p);
|
|
1781
|
+
p += 4;
|
|
1782
|
+
if (len) {
|
|
1783
|
+
buf.utf8Write(filename, p, len);
|
|
1784
|
+
p += len;
|
|
1785
|
+
}
|
|
1786
|
+
}
|
|
1787
|
+
|
|
1788
|
+
{
|
|
1789
|
+
const longname = (
|
|
1790
|
+
!name || !name.longname || typeof name.longname !== 'string'
|
|
1791
|
+
? ''
|
|
1792
|
+
: name.longname
|
|
1793
|
+
);
|
|
1794
|
+
const len = Buffer.byteLength(longname);
|
|
1795
|
+
writeUInt32BE(buf, len, p);
|
|
1796
|
+
p += 4;
|
|
1797
|
+
if (len) {
|
|
1798
|
+
buf.utf8Write(longname, p, len);
|
|
1799
|
+
p += len;
|
|
1800
|
+
}
|
|
1801
|
+
}
|
|
1802
|
+
|
|
1803
|
+
const attr = attrs[i];
|
|
1804
|
+
if (attr) {
|
|
1805
|
+
writeUInt32BE(buf, attr.flags, p);
|
|
1806
|
+
p += 4;
|
|
1807
|
+
if (attr.flags && attr.bytes) {
|
|
1808
|
+
buf.set(attr.bytes, p);
|
|
1809
|
+
p += attr.nb;
|
|
1810
|
+
}
|
|
1811
|
+
} else {
|
|
1812
|
+
writeUInt32BE(buf, 0, p);
|
|
1813
|
+
p += 4;
|
|
1814
|
+
}
|
|
1815
|
+
}
|
|
1816
|
+
|
|
1817
|
+
const isBuffered = sendOrBuffer(this, buf);
|
|
1818
|
+
this._debug && this._debug(
|
|
1819
|
+
`SFTP: Outbound: ${isBuffered ? 'Buffered' : 'Sending'} NAME`
|
|
1820
|
+
);
|
|
1821
|
+
}
|
|
1822
|
+
attrs(reqid, attrs) {
|
|
1823
|
+
if (!this.server)
|
|
1824
|
+
throw new Error('Server-only method called in client mode');
|
|
1825
|
+
|
|
1826
|
+
if (typeof attrs !== 'object' || attrs === null)
|
|
1827
|
+
throw new Error('attrs is not an object');
|
|
1828
|
+
|
|
1829
|
+
attrs = attrsToBytes(attrs);
|
|
1830
|
+
const flags = attrs.flags;
|
|
1831
|
+
const attrsLen = attrs.nb;
|
|
1832
|
+
let p = 9;
|
|
1833
|
+
const buf = Buffer.allocUnsafe(4 + 1 + 4 + 4 + attrsLen);
|
|
1834
|
+
|
|
1835
|
+
writeUInt32BE(buf, buf.length - 4, 0);
|
|
1836
|
+
buf[4] = RESPONSE.ATTRS;
|
|
1837
|
+
writeUInt32BE(buf, reqid, 5);
|
|
1838
|
+
|
|
1839
|
+
writeUInt32BE(buf, flags, p);
|
|
1840
|
+
if (attrsLen) {
|
|
1841
|
+
p += 4;
|
|
1842
|
+
|
|
1843
|
+
if (attrsLen === ATTRS_BUF.length)
|
|
1844
|
+
buf.set(ATTRS_BUF, p);
|
|
1845
|
+
else
|
|
1846
|
+
bufferCopy(ATTRS_BUF, buf, 0, attrsLen, p);
|
|
1847
|
+
|
|
1848
|
+
p += attrsLen;
|
|
1849
|
+
}
|
|
1850
|
+
|
|
1851
|
+
const isBuffered = sendOrBuffer(this, buf);
|
|
1852
|
+
this._debug && this._debug(
|
|
1853
|
+
`SFTP: Outbound: ${isBuffered ? 'Buffered' : 'Sending'} ATTRS`
|
|
1854
|
+
);
|
|
1855
|
+
}
|
|
1856
|
+
}
|
|
1857
|
+
|
|
1858
|
+
function tryCreateBuffer(size) {
|
|
1859
|
+
try {
|
|
1860
|
+
return Buffer.allocUnsafe(size);
|
|
1861
|
+
} catch (ex) {
|
|
1862
|
+
return ex;
|
|
1863
|
+
}
|
|
1864
|
+
}
|
|
1865
|
+
|
|
1866
|
+
function read_(self, handle, buf, off, len, position, cb, req_) {
|
|
1867
|
+
const maxDataLen = self._maxReadLen;
|
|
1868
|
+
const overflow = Math.max(len - maxDataLen, 0);
|
|
1869
|
+
|
|
1870
|
+
if (overflow)
|
|
1871
|
+
len = maxDataLen;
|
|
1872
|
+
|
|
1873
|
+
/*
|
|
1874
|
+
uint32 id
|
|
1875
|
+
string handle
|
|
1876
|
+
uint64 offset
|
|
1877
|
+
uint32 len
|
|
1878
|
+
*/
|
|
1879
|
+
const handleLen = handle.length;
|
|
1880
|
+
let p = 9;
|
|
1881
|
+
let pos = position;
|
|
1882
|
+
const out = Buffer.allocUnsafe(4 + 1 + 4 + 4 + handleLen + 8 + 4);
|
|
1883
|
+
|
|
1884
|
+
writeUInt32BE(out, out.length - 4, 0);
|
|
1885
|
+
out[4] = REQUEST.READ;
|
|
1886
|
+
const reqid = self._writeReqid = (self._writeReqid + 1) & MAX_REQID;
|
|
1887
|
+
writeUInt32BE(out, reqid, 5);
|
|
1888
|
+
|
|
1889
|
+
writeUInt32BE(out, handleLen, p);
|
|
1890
|
+
out.set(handle, p += 4);
|
|
1891
|
+
p += handleLen;
|
|
1892
|
+
for (let i = 7; i >= 0; --i) {
|
|
1893
|
+
out[p + i] = pos & 0xFF;
|
|
1894
|
+
pos /= 256;
|
|
1895
|
+
}
|
|
1896
|
+
writeUInt32BE(out, len, p += 8);
|
|
1897
|
+
|
|
1898
|
+
if (typeof cb !== 'function')
|
|
1899
|
+
cb = noop;
|
|
1900
|
+
|
|
1901
|
+
const req = (req_ || {
|
|
1902
|
+
nb: 0,
|
|
1903
|
+
position,
|
|
1904
|
+
off,
|
|
1905
|
+
origOff: off,
|
|
1906
|
+
len: undefined,
|
|
1907
|
+
overflow: undefined,
|
|
1908
|
+
cb: (err, data, nb) => {
|
|
1909
|
+
const len = req.len;
|
|
1910
|
+
const overflow = req.overflow;
|
|
1911
|
+
|
|
1912
|
+
if (err) {
|
|
1913
|
+
if (cb._wantEOFError || err.code !== STATUS_CODE.EOF)
|
|
1914
|
+
return cb(err);
|
|
1915
|
+
} else if (nb > len) {
|
|
1916
|
+
return cb(new Error('Received more data than requested'));
|
|
1917
|
+
} else if (nb === len && overflow) {
|
|
1918
|
+
req.nb += nb;
|
|
1919
|
+
req.position += nb;
|
|
1920
|
+
req.off += nb;
|
|
1921
|
+
read_(self, handle, buf, req.off, overflow, req.position, cb, req);
|
|
1922
|
+
return;
|
|
1923
|
+
}
|
|
1924
|
+
|
|
1925
|
+
if (req.origOff === 0 && buf.length === req.nb)
|
|
1926
|
+
data = buf;
|
|
1927
|
+
else
|
|
1928
|
+
data = bufferSlice(buf, req.origOff, req.origOff + req.nb);
|
|
1929
|
+
cb(undefined, req.nb + (nb || 0), data, req.position);
|
|
1930
|
+
},
|
|
1931
|
+
buffer: undefined,
|
|
1932
|
+
});
|
|
1933
|
+
|
|
1934
|
+
req.len = len;
|
|
1935
|
+
req.overflow = overflow;
|
|
1936
|
+
|
|
1937
|
+
// TODO: avoid creating multiple buffer slices when we need to re-call read_()
|
|
1938
|
+
// because of overflow
|
|
1939
|
+
req.buffer = bufferSlice(buf, off, off + len);
|
|
1940
|
+
|
|
1941
|
+
self._requests[reqid] = req;
|
|
1942
|
+
|
|
1943
|
+
const isBuffered = sendOrBuffer(self, out);
|
|
1944
|
+
self._debug && self._debug(
|
|
1945
|
+
`SFTP: Outbound: ${isBuffered ? 'Buffered' : 'Sending'} READ`
|
|
1946
|
+
);
|
|
1947
|
+
}
|
|
1948
|
+
|
|
1949
|
+
function fastXfer(src, dst, srcPath, dstPath, opts, cb) {
|
|
1950
|
+
let concurrency = 64;
|
|
1951
|
+
let chunkSize = 32768;
|
|
1952
|
+
let onstep;
|
|
1953
|
+
let mode;
|
|
1954
|
+
let fileSize;
|
|
1955
|
+
|
|
1956
|
+
if (typeof opts === 'function') {
|
|
1957
|
+
cb = opts;
|
|
1958
|
+
} else if (typeof opts === 'object' && opts !== null) {
|
|
1959
|
+
if (typeof opts.concurrency === 'number'
|
|
1960
|
+
&& opts.concurrency > 0
|
|
1961
|
+
&& !isNaN(opts.concurrency)) {
|
|
1962
|
+
concurrency = opts.concurrency;
|
|
1963
|
+
}
|
|
1964
|
+
if (typeof opts.chunkSize === 'number'
|
|
1965
|
+
&& opts.chunkSize > 0
|
|
1966
|
+
&& !isNaN(opts.chunkSize)) {
|
|
1967
|
+
chunkSize = opts.chunkSize;
|
|
1968
|
+
}
|
|
1969
|
+
if (typeof opts.fileSize === 'number'
|
|
1970
|
+
&& opts.fileSize > 0
|
|
1971
|
+
&& !isNaN(opts.fileSize)) {
|
|
1972
|
+
fileSize = opts.fileSize;
|
|
1973
|
+
}
|
|
1974
|
+
if (typeof opts.step === 'function')
|
|
1975
|
+
onstep = opts.step;
|
|
1976
|
+
|
|
1977
|
+
if (typeof opts.mode === 'string' || typeof opts.mode === 'number')
|
|
1978
|
+
mode = modeNum(opts.mode);
|
|
1979
|
+
}
|
|
1980
|
+
|
|
1981
|
+
// Internal state variables
|
|
1982
|
+
let fsize;
|
|
1983
|
+
let pdst = 0;
|
|
1984
|
+
let total = 0;
|
|
1985
|
+
let hadError = false;
|
|
1986
|
+
let srcHandle;
|
|
1987
|
+
let dstHandle;
|
|
1988
|
+
let readbuf;
|
|
1989
|
+
let bufsize = chunkSize * concurrency;
|
|
1990
|
+
|
|
1991
|
+
function onerror(err) {
|
|
1992
|
+
if (hadError)
|
|
1993
|
+
return;
|
|
1994
|
+
|
|
1995
|
+
hadError = true;
|
|
1996
|
+
|
|
1997
|
+
let left = 0;
|
|
1998
|
+
let cbfinal;
|
|
1999
|
+
|
|
2000
|
+
if (srcHandle || dstHandle) {
|
|
2001
|
+
cbfinal = () => {
|
|
2002
|
+
if (--left === 0)
|
|
2003
|
+
cb(err);
|
|
2004
|
+
};
|
|
2005
|
+
if (srcHandle && (src === fs || src.outgoing.state === 'open'))
|
|
2006
|
+
++left;
|
|
2007
|
+
if (dstHandle && (dst === fs || dst.outgoing.state === 'open'))
|
|
2008
|
+
++left;
|
|
2009
|
+
if (srcHandle && (src === fs || src.outgoing.state === 'open'))
|
|
2010
|
+
src.close(srcHandle, cbfinal);
|
|
2011
|
+
if (dstHandle && (dst === fs || dst.outgoing.state === 'open'))
|
|
2012
|
+
dst.close(dstHandle, cbfinal);
|
|
2013
|
+
} else {
|
|
2014
|
+
cb(err);
|
|
2015
|
+
}
|
|
2016
|
+
}
|
|
2017
|
+
|
|
2018
|
+
src.open(srcPath, 'r', (err, sourceHandle) => {
|
|
2019
|
+
if (err)
|
|
2020
|
+
return onerror(err);
|
|
2021
|
+
|
|
2022
|
+
srcHandle = sourceHandle;
|
|
2023
|
+
|
|
2024
|
+
if (fileSize === undefined)
|
|
2025
|
+
src.fstat(srcHandle, tryStat);
|
|
2026
|
+
else
|
|
2027
|
+
tryStat(null, { size: fileSize });
|
|
2028
|
+
|
|
2029
|
+
function tryStat(err, attrs) {
|
|
2030
|
+
if (err) {
|
|
2031
|
+
if (src !== fs) {
|
|
2032
|
+
// Try stat() for sftp servers that may not support fstat() for
|
|
2033
|
+
// whatever reason
|
|
2034
|
+
src.stat(srcPath, (err_, attrs_) => {
|
|
2035
|
+
if (err_)
|
|
2036
|
+
return onerror(err);
|
|
2037
|
+
tryStat(null, attrs_);
|
|
2038
|
+
});
|
|
2039
|
+
return;
|
|
2040
|
+
}
|
|
2041
|
+
return onerror(err);
|
|
2042
|
+
}
|
|
2043
|
+
fsize = attrs.size;
|
|
2044
|
+
|
|
2045
|
+
dst.open(dstPath, 'w', (err, destHandle) => {
|
|
2046
|
+
if (err)
|
|
2047
|
+
return onerror(err);
|
|
2048
|
+
|
|
2049
|
+
dstHandle = destHandle;
|
|
2050
|
+
|
|
2051
|
+
if (fsize <= 0)
|
|
2052
|
+
return onerror();
|
|
2053
|
+
|
|
2054
|
+
// Use less memory where possible
|
|
2055
|
+
while (bufsize > fsize) {
|
|
2056
|
+
if (concurrency === 1) {
|
|
2057
|
+
bufsize = fsize;
|
|
2058
|
+
break;
|
|
2059
|
+
}
|
|
2060
|
+
bufsize -= chunkSize;
|
|
2061
|
+
--concurrency;
|
|
2062
|
+
}
|
|
2063
|
+
|
|
2064
|
+
readbuf = tryCreateBuffer(bufsize);
|
|
2065
|
+
if (readbuf instanceof Error)
|
|
2066
|
+
return onerror(readbuf);
|
|
2067
|
+
|
|
2068
|
+
if (mode !== undefined) {
|
|
2069
|
+
dst.fchmod(dstHandle, mode, function tryAgain(err) {
|
|
2070
|
+
if (err) {
|
|
2071
|
+
// Try chmod() for sftp servers that may not support fchmod()
|
|
2072
|
+
// for whatever reason
|
|
2073
|
+
dst.chmod(dstPath, mode, (err_) => tryAgain());
|
|
2074
|
+
return;
|
|
2075
|
+
}
|
|
2076
|
+
startReads();
|
|
2077
|
+
});
|
|
2078
|
+
} else {
|
|
2079
|
+
startReads();
|
|
2080
|
+
}
|
|
2081
|
+
|
|
2082
|
+
function onread(err, nb, data, dstpos, datapos, origChunkLen) {
|
|
2083
|
+
if (err)
|
|
2084
|
+
return onerror(err);
|
|
2085
|
+
|
|
2086
|
+
datapos = datapos || 0;
|
|
2087
|
+
|
|
2088
|
+
dst.write(dstHandle, readbuf, datapos, nb, dstpos, writeCb);
|
|
2089
|
+
|
|
2090
|
+
function writeCb(err) {
|
|
2091
|
+
if (err)
|
|
2092
|
+
return onerror(err);
|
|
2093
|
+
|
|
2094
|
+
total += nb;
|
|
2095
|
+
onstep && onstep(total, nb, fsize);
|
|
2096
|
+
|
|
2097
|
+
if (nb < origChunkLen)
|
|
2098
|
+
return singleRead(datapos, dstpos + nb, origChunkLen - nb);
|
|
2099
|
+
|
|
2100
|
+
if (total === fsize) {
|
|
2101
|
+
dst.close(dstHandle, (err) => {
|
|
2102
|
+
dstHandle = undefined;
|
|
2103
|
+
if (err)
|
|
2104
|
+
return onerror(err);
|
|
2105
|
+
src.close(srcHandle, (err) => {
|
|
2106
|
+
srcHandle = undefined;
|
|
2107
|
+
if (err)
|
|
2108
|
+
return onerror(err);
|
|
2109
|
+
cb();
|
|
2110
|
+
});
|
|
2111
|
+
});
|
|
2112
|
+
return;
|
|
2113
|
+
}
|
|
2114
|
+
|
|
2115
|
+
if (pdst >= fsize)
|
|
2116
|
+
return;
|
|
2117
|
+
|
|
2118
|
+
const chunk =
|
|
2119
|
+
(pdst + chunkSize > fsize ? fsize - pdst : chunkSize);
|
|
2120
|
+
singleRead(datapos, pdst, chunk);
|
|
2121
|
+
pdst += chunk;
|
|
2122
|
+
}
|
|
2123
|
+
}
|
|
2124
|
+
|
|
2125
|
+
function makeCb(psrc, pdst, chunk) {
|
|
2126
|
+
return (err, nb, data) => {
|
|
2127
|
+
onread(err, nb, data, pdst, psrc, chunk);
|
|
2128
|
+
};
|
|
2129
|
+
}
|
|
2130
|
+
|
|
2131
|
+
function singleRead(psrc, pdst, chunk) {
|
|
2132
|
+
src.read(srcHandle,
|
|
2133
|
+
readbuf,
|
|
2134
|
+
psrc,
|
|
2135
|
+
chunk,
|
|
2136
|
+
pdst,
|
|
2137
|
+
makeCb(psrc, pdst, chunk));
|
|
2138
|
+
}
|
|
2139
|
+
|
|
2140
|
+
function startReads() {
|
|
2141
|
+
let reads = 0;
|
|
2142
|
+
let psrc = 0;
|
|
2143
|
+
while (pdst < fsize && reads < concurrency) {
|
|
2144
|
+
const chunk =
|
|
2145
|
+
(pdst + chunkSize > fsize ? fsize - pdst : chunkSize);
|
|
2146
|
+
singleRead(psrc, pdst, chunk);
|
|
2147
|
+
psrc += chunk;
|
|
2148
|
+
pdst += chunk;
|
|
2149
|
+
++reads;
|
|
2150
|
+
}
|
|
2151
|
+
}
|
|
2152
|
+
});
|
|
2153
|
+
}
|
|
2154
|
+
});
|
|
2155
|
+
}
|
|
2156
|
+
|
|
2157
|
+
function writeAll(sftp, handle, buffer, offset, length, position, callback_) {
|
|
2158
|
+
const callback = (typeof callback_ === 'function' ? callback_ : undefined);
|
|
2159
|
+
|
|
2160
|
+
sftp.write(handle,
|
|
2161
|
+
buffer,
|
|
2162
|
+
offset,
|
|
2163
|
+
length,
|
|
2164
|
+
position,
|
|
2165
|
+
(writeErr, written) => {
|
|
2166
|
+
if (writeErr) {
|
|
2167
|
+
return sftp.close(handle, () => {
|
|
2168
|
+
callback && callback(writeErr);
|
|
2169
|
+
});
|
|
2170
|
+
}
|
|
2171
|
+
if (written === length) {
|
|
2172
|
+
sftp.close(handle, callback);
|
|
2173
|
+
} else {
|
|
2174
|
+
offset += written;
|
|
2175
|
+
length -= written;
|
|
2176
|
+
position += written;
|
|
2177
|
+
writeAll(sftp, handle, buffer, offset, length, position, callback);
|
|
2178
|
+
}
|
|
2179
|
+
});
|
|
2180
|
+
}
|
|
2181
|
+
|
|
2182
|
+
class Stats {
|
|
2183
|
+
constructor(initial) {
|
|
2184
|
+
this.mode = (initial && initial.mode);
|
|
2185
|
+
this.uid = (initial && initial.uid);
|
|
2186
|
+
this.gid = (initial && initial.gid);
|
|
2187
|
+
this.size = (initial && initial.size);
|
|
2188
|
+
this.atime = (initial && initial.atime);
|
|
2189
|
+
this.mtime = (initial && initial.mtime);
|
|
2190
|
+
this.extended = (initial && initial.extended);
|
|
2191
|
+
}
|
|
2192
|
+
isDirectory() {
|
|
2193
|
+
return ((this.mode & constants.S_IFMT) === constants.S_IFDIR);
|
|
2194
|
+
}
|
|
2195
|
+
isFile() {
|
|
2196
|
+
return ((this.mode & constants.S_IFMT) === constants.S_IFREG);
|
|
2197
|
+
}
|
|
2198
|
+
isBlockDevice() {
|
|
2199
|
+
return ((this.mode & constants.S_IFMT) === constants.S_IFBLK);
|
|
2200
|
+
}
|
|
2201
|
+
isCharacterDevice() {
|
|
2202
|
+
return ((this.mode & constants.S_IFMT) === constants.S_IFCHR);
|
|
2203
|
+
}
|
|
2204
|
+
isSymbolicLink() {
|
|
2205
|
+
return ((this.mode & constants.S_IFMT) === constants.S_IFLNK);
|
|
2206
|
+
}
|
|
2207
|
+
isFIFO() {
|
|
2208
|
+
return ((this.mode & constants.S_IFMT) === constants.S_IFIFO);
|
|
2209
|
+
}
|
|
2210
|
+
isSocket() {
|
|
2211
|
+
return ((this.mode & constants.S_IFMT) === constants.S_IFSOCK);
|
|
2212
|
+
}
|
|
2213
|
+
}
|
|
2214
|
+
|
|
2215
|
+
function attrsToBytes(attrs) {
|
|
2216
|
+
let flags = 0;
|
|
2217
|
+
let nb = 0;
|
|
2218
|
+
|
|
2219
|
+
if (typeof attrs === 'object' && attrs !== null) {
|
|
2220
|
+
if (typeof attrs.size === 'number') {
|
|
2221
|
+
flags |= ATTR.SIZE;
|
|
2222
|
+
const val = attrs.size;
|
|
2223
|
+
// Big Endian
|
|
2224
|
+
ATTRS_BUF[nb++] = val / 72057594037927940; // 2**56
|
|
2225
|
+
ATTRS_BUF[nb++] = val / 281474976710656; // 2**48
|
|
2226
|
+
ATTRS_BUF[nb++] = val / 1099511627776; // 2**40
|
|
2227
|
+
ATTRS_BUF[nb++] = val / 4294967296; // 2**32
|
|
2228
|
+
ATTRS_BUF[nb++] = val / 16777216; // 2**24
|
|
2229
|
+
ATTRS_BUF[nb++] = val / 65536; // 2**16
|
|
2230
|
+
ATTRS_BUF[nb++] = val / 256; // 2**8
|
|
2231
|
+
ATTRS_BUF[nb++] = val;
|
|
2232
|
+
}
|
|
2233
|
+
if (typeof attrs.uid === 'number' && typeof attrs.gid === 'number') {
|
|
2234
|
+
flags |= ATTR.UIDGID;
|
|
2235
|
+
const uid = attrs.uid;
|
|
2236
|
+
const gid = attrs.gid;
|
|
2237
|
+
// Big Endian
|
|
2238
|
+
ATTRS_BUF[nb++] = uid >>> 24;
|
|
2239
|
+
ATTRS_BUF[nb++] = uid >>> 16;
|
|
2240
|
+
ATTRS_BUF[nb++] = uid >>> 8;
|
|
2241
|
+
ATTRS_BUF[nb++] = uid;
|
|
2242
|
+
ATTRS_BUF[nb++] = gid >>> 24;
|
|
2243
|
+
ATTRS_BUF[nb++] = gid >>> 16;
|
|
2244
|
+
ATTRS_BUF[nb++] = gid >>> 8;
|
|
2245
|
+
ATTRS_BUF[nb++] = gid;
|
|
2246
|
+
}
|
|
2247
|
+
if (typeof attrs.mode === 'number' || typeof attrs.mode === 'string') {
|
|
2248
|
+
const mode = modeNum(attrs.mode);
|
|
2249
|
+
flags |= ATTR.PERMISSIONS;
|
|
2250
|
+
// Big Endian
|
|
2251
|
+
ATTRS_BUF[nb++] = mode >>> 24;
|
|
2252
|
+
ATTRS_BUF[nb++] = mode >>> 16;
|
|
2253
|
+
ATTRS_BUF[nb++] = mode >>> 8;
|
|
2254
|
+
ATTRS_BUF[nb++] = mode;
|
|
2255
|
+
}
|
|
2256
|
+
if ((typeof attrs.atime === 'number' || isDate(attrs.atime))
|
|
2257
|
+
&& (typeof attrs.mtime === 'number' || isDate(attrs.mtime))) {
|
|
2258
|
+
const atime = toUnixTimestamp(attrs.atime);
|
|
2259
|
+
const mtime = toUnixTimestamp(attrs.mtime);
|
|
2260
|
+
|
|
2261
|
+
flags |= ATTR.ACMODTIME;
|
|
2262
|
+
// Big Endian
|
|
2263
|
+
ATTRS_BUF[nb++] = atime >>> 24;
|
|
2264
|
+
ATTRS_BUF[nb++] = atime >>> 16;
|
|
2265
|
+
ATTRS_BUF[nb++] = atime >>> 8;
|
|
2266
|
+
ATTRS_BUF[nb++] = atime;
|
|
2267
|
+
ATTRS_BUF[nb++] = mtime >>> 24;
|
|
2268
|
+
ATTRS_BUF[nb++] = mtime >>> 16;
|
|
2269
|
+
ATTRS_BUF[nb++] = mtime >>> 8;
|
|
2270
|
+
ATTRS_BUF[nb++] = mtime;
|
|
2271
|
+
}
|
|
2272
|
+
// TODO: extended attributes
|
|
2273
|
+
}
|
|
2274
|
+
|
|
2275
|
+
return { flags, nb };
|
|
2276
|
+
}
|
|
2277
|
+
|
|
2278
|
+
function toUnixTimestamp(time) {
|
|
2279
|
+
// eslint-disable-next-line no-self-compare
|
|
2280
|
+
if (typeof time === 'number' && time === time) // Valid, non-NaN number
|
|
2281
|
+
return time;
|
|
2282
|
+
if (isDate(time))
|
|
2283
|
+
return parseInt(time.getTime() / 1000, 10);
|
|
2284
|
+
throw new Error(`Cannot parse time: ${time}`);
|
|
2285
|
+
}
|
|
2286
|
+
|
|
2287
|
+
function modeNum(mode) {
|
|
2288
|
+
// eslint-disable-next-line no-self-compare
|
|
2289
|
+
if (typeof mode === 'number' && mode === mode) // Valid, non-NaN number
|
|
2290
|
+
return mode;
|
|
2291
|
+
if (typeof mode === 'string')
|
|
2292
|
+
return modeNum(parseInt(mode, 8));
|
|
2293
|
+
throw new Error(`Cannot parse mode: ${mode}`);
|
|
2294
|
+
}
|
|
2295
|
+
|
|
2296
|
+
const stringFlagMap = {
|
|
2297
|
+
'r': OPEN_MODE.READ,
|
|
2298
|
+
'r+': OPEN_MODE.READ | OPEN_MODE.WRITE,
|
|
2299
|
+
'w': OPEN_MODE.TRUNC | OPEN_MODE.CREAT | OPEN_MODE.WRITE,
|
|
2300
|
+
'wx': OPEN_MODE.TRUNC | OPEN_MODE.CREAT | OPEN_MODE.WRITE | OPEN_MODE.EXCL,
|
|
2301
|
+
'xw': OPEN_MODE.TRUNC | OPEN_MODE.CREAT | OPEN_MODE.WRITE | OPEN_MODE.EXCL,
|
|
2302
|
+
'w+': OPEN_MODE.TRUNC | OPEN_MODE.CREAT | OPEN_MODE.READ | OPEN_MODE.WRITE,
|
|
2303
|
+
'wx+': OPEN_MODE.TRUNC | OPEN_MODE.CREAT | OPEN_MODE.READ | OPEN_MODE.WRITE
|
|
2304
|
+
| OPEN_MODE.EXCL,
|
|
2305
|
+
'xw+': OPEN_MODE.TRUNC | OPEN_MODE.CREAT | OPEN_MODE.READ | OPEN_MODE.WRITE
|
|
2306
|
+
| OPEN_MODE.EXCL,
|
|
2307
|
+
'a': OPEN_MODE.APPEND | OPEN_MODE.CREAT | OPEN_MODE.WRITE,
|
|
2308
|
+
'ax': OPEN_MODE.APPEND | OPEN_MODE.CREAT | OPEN_MODE.WRITE | OPEN_MODE.EXCL,
|
|
2309
|
+
'xa': OPEN_MODE.APPEND | OPEN_MODE.CREAT | OPEN_MODE.WRITE | OPEN_MODE.EXCL,
|
|
2310
|
+
'a+': OPEN_MODE.APPEND | OPEN_MODE.CREAT | OPEN_MODE.READ | OPEN_MODE.WRITE,
|
|
2311
|
+
'ax+': OPEN_MODE.APPEND | OPEN_MODE.CREAT | OPEN_MODE.READ | OPEN_MODE.WRITE
|
|
2312
|
+
| OPEN_MODE.EXCL,
|
|
2313
|
+
'xa+': OPEN_MODE.APPEND | OPEN_MODE.CREAT | OPEN_MODE.READ | OPEN_MODE.WRITE
|
|
2314
|
+
| OPEN_MODE.EXCL
|
|
2315
|
+
};
|
|
2316
|
+
|
|
2317
|
+
function stringToFlags(str) {
|
|
2318
|
+
const flags = stringFlagMap[str];
|
|
2319
|
+
return (flags !== undefined ? flags : null);
|
|
2320
|
+
}
|
|
2321
|
+
|
|
2322
|
+
const flagsToString = (() => {
|
|
2323
|
+
const stringFlagMapKeys = Object.keys(stringFlagMap);
|
|
2324
|
+
return (flags) => {
|
|
2325
|
+
for (let i = 0; i < stringFlagMapKeys.length; ++i) {
|
|
2326
|
+
const key = stringFlagMapKeys[i];
|
|
2327
|
+
if (stringFlagMap[key] === flags)
|
|
2328
|
+
return key;
|
|
2329
|
+
}
|
|
2330
|
+
return null;
|
|
2331
|
+
};
|
|
2332
|
+
})();
|
|
2333
|
+
|
|
2334
|
+
function readAttrs(biOpt) {
|
|
2335
|
+
/*
|
|
2336
|
+
uint32 flags
|
|
2337
|
+
uint64 size present only if flag SSH_FILEXFER_ATTR_SIZE
|
|
2338
|
+
uint32 uid present only if flag SSH_FILEXFER_ATTR_UIDGID
|
|
2339
|
+
uint32 gid present only if flag SSH_FILEXFER_ATTR_UIDGID
|
|
2340
|
+
uint32 permissions present only if flag SSH_FILEXFER_ATTR_PERMISSIONS
|
|
2341
|
+
uint32 atime present only if flag SSH_FILEXFER_ACMODTIME
|
|
2342
|
+
uint32 mtime present only if flag SSH_FILEXFER_ACMODTIME
|
|
2343
|
+
uint32 extended_count present only if flag SSH_FILEXFER_ATTR_EXTENDED
|
|
2344
|
+
string extended_type
|
|
2345
|
+
string extended_data
|
|
2346
|
+
... more extended data (extended_type - extended_data pairs),
|
|
2347
|
+
so that number of pairs equals extended_count
|
|
2348
|
+
*/
|
|
2349
|
+
const flags = bufferParser.readUInt32BE();
|
|
2350
|
+
if (flags === undefined)
|
|
2351
|
+
return;
|
|
2352
|
+
|
|
2353
|
+
const attrs = new Stats();
|
|
2354
|
+
if (flags & ATTR.SIZE) {
|
|
2355
|
+
const size = bufferParser.readUInt64BE(biOpt);
|
|
2356
|
+
if (size === undefined)
|
|
2357
|
+
return;
|
|
2358
|
+
attrs.size = size;
|
|
2359
|
+
}
|
|
2360
|
+
|
|
2361
|
+
if (flags & ATTR.UIDGID) {
|
|
2362
|
+
const uid = bufferParser.readUInt32BE();
|
|
2363
|
+
const gid = bufferParser.readUInt32BE();
|
|
2364
|
+
if (gid === undefined)
|
|
2365
|
+
return;
|
|
2366
|
+
attrs.uid = uid;
|
|
2367
|
+
attrs.gid = gid;
|
|
2368
|
+
}
|
|
2369
|
+
|
|
2370
|
+
if (flags & ATTR.PERMISSIONS) {
|
|
2371
|
+
const mode = bufferParser.readUInt32BE();
|
|
2372
|
+
if (mode === undefined)
|
|
2373
|
+
return;
|
|
2374
|
+
attrs.mode = mode;
|
|
2375
|
+
}
|
|
2376
|
+
|
|
2377
|
+
if (flags & ATTR.ACMODTIME) {
|
|
2378
|
+
const atime = bufferParser.readUInt32BE();
|
|
2379
|
+
const mtime = bufferParser.readUInt32BE();
|
|
2380
|
+
if (mtime === undefined)
|
|
2381
|
+
return;
|
|
2382
|
+
attrs.atime = atime;
|
|
2383
|
+
attrs.mtime = mtime;
|
|
2384
|
+
}
|
|
2385
|
+
|
|
2386
|
+
if (flags & ATTR.EXTENDED) {
|
|
2387
|
+
const count = bufferParser.readUInt32BE();
|
|
2388
|
+
if (count === undefined)
|
|
2389
|
+
return;
|
|
2390
|
+
const extended = {};
|
|
2391
|
+
for (let i = 0; i < count; ++i) {
|
|
2392
|
+
const type = bufferParser.readString(true);
|
|
2393
|
+
const data = bufferParser.readString();
|
|
2394
|
+
if (data === undefined)
|
|
2395
|
+
return;
|
|
2396
|
+
extended[type] = data;
|
|
2397
|
+
}
|
|
2398
|
+
attrs.extended = extended;
|
|
2399
|
+
}
|
|
2400
|
+
|
|
2401
|
+
return attrs;
|
|
2402
|
+
}
|
|
2403
|
+
|
|
2404
|
+
function sendOrBuffer(sftp, payload) {
|
|
2405
|
+
const ret = tryWritePayload(sftp, payload);
|
|
2406
|
+
if (ret !== undefined) {
|
|
2407
|
+
sftp._buffer.push(ret);
|
|
2408
|
+
return false;
|
|
2409
|
+
}
|
|
2410
|
+
return true;
|
|
2411
|
+
}
|
|
2412
|
+
|
|
2413
|
+
function tryWritePayload(sftp, payload) {
|
|
2414
|
+
const outgoing = sftp.outgoing;
|
|
2415
|
+
if (outgoing.state !== 'open')
|
|
2416
|
+
return;
|
|
2417
|
+
|
|
2418
|
+
if (outgoing.window === 0) {
|
|
2419
|
+
sftp._waitWindow = true; // XXX: Unnecessary?
|
|
2420
|
+
return payload;
|
|
2421
|
+
}
|
|
2422
|
+
|
|
2423
|
+
let ret;
|
|
2424
|
+
const len = payload.length;
|
|
2425
|
+
let p = 0;
|
|
2426
|
+
|
|
2427
|
+
while (len - p > 0 && outgoing.window > 0) {
|
|
2428
|
+
const actualLen = Math.min(len - p, outgoing.window, outgoing.packetSize);
|
|
2429
|
+
outgoing.window -= actualLen;
|
|
2430
|
+
if (outgoing.window === 0) {
|
|
2431
|
+
sftp._waitWindow = true;
|
|
2432
|
+
sftp._chunkcb = drainBuffer;
|
|
2433
|
+
}
|
|
2434
|
+
|
|
2435
|
+
if (p === 0 && actualLen === len) {
|
|
2436
|
+
sftp._protocol.channelData(sftp.outgoing.id, payload);
|
|
2437
|
+
} else {
|
|
2438
|
+
sftp._protocol.channelData(sftp.outgoing.id,
|
|
2439
|
+
bufferSlice(payload, p, p + actualLen));
|
|
2440
|
+
}
|
|
2441
|
+
|
|
2442
|
+
p += actualLen;
|
|
2443
|
+
}
|
|
2444
|
+
|
|
2445
|
+
if (len - p > 0) {
|
|
2446
|
+
if (p > 0)
|
|
2447
|
+
ret = bufferSlice(payload, p, len);
|
|
2448
|
+
else
|
|
2449
|
+
ret = payload; // XXX: should never get here?
|
|
2450
|
+
}
|
|
2451
|
+
|
|
2452
|
+
return ret;
|
|
2453
|
+
}
|
|
2454
|
+
|
|
2455
|
+
function drainBuffer() {
|
|
2456
|
+
this._chunkcb = undefined;
|
|
2457
|
+
const buffer = this._buffer;
|
|
2458
|
+
let i = 0;
|
|
2459
|
+
while (i < buffer.length) {
|
|
2460
|
+
const payload = buffer[i];
|
|
2461
|
+
const ret = tryWritePayload(this, payload);
|
|
2462
|
+
if (ret !== undefined) {
|
|
2463
|
+
if (ret !== payload)
|
|
2464
|
+
buffer[i] = ret;
|
|
2465
|
+
if (i > 0)
|
|
2466
|
+
this._buffer = buffer.slice(i);
|
|
2467
|
+
return;
|
|
2468
|
+
}
|
|
2469
|
+
++i;
|
|
2470
|
+
}
|
|
2471
|
+
if (i > 0)
|
|
2472
|
+
this._buffer = [];
|
|
2473
|
+
}
|
|
2474
|
+
|
|
2475
|
+
function doFatalSFTPError(sftp, msg, noDebug) {
|
|
2476
|
+
const err = new Error(msg);
|
|
2477
|
+
err.level = 'sftp-protocol';
|
|
2478
|
+
if (!noDebug && sftp._debug)
|
|
2479
|
+
sftp._debug(`SFTP: Inbound: ${msg}`);
|
|
2480
|
+
sftp.emit('error', err);
|
|
2481
|
+
sftp.destroy();
|
|
2482
|
+
cleanupRequests(sftp);
|
|
2483
|
+
return false;
|
|
2484
|
+
}
|
|
2485
|
+
|
|
2486
|
+
function cleanupRequests(sftp) {
|
|
2487
|
+
const keys = Object.keys(sftp._requests);
|
|
2488
|
+
if (keys.length === 0)
|
|
2489
|
+
return;
|
|
2490
|
+
|
|
2491
|
+
const reqs = sftp._requests;
|
|
2492
|
+
sftp._requests = {};
|
|
2493
|
+
const err = new Error('No response from server');
|
|
2494
|
+
for (let i = 0; i < keys.length; ++i) {
|
|
2495
|
+
const req = reqs[keys[i]];
|
|
2496
|
+
if (typeof req.cb === 'function')
|
|
2497
|
+
req.cb(err);
|
|
2498
|
+
}
|
|
2499
|
+
}
|
|
2500
|
+
|
|
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
|
+
|
|
2526
|
+
const CLIENT_HANDLERS = {
|
|
2527
|
+
[RESPONSE.VERSION]: (sftp, payload) => {
|
|
2528
|
+
if (sftp._version !== -1)
|
|
2529
|
+
return doFatalSFTPError(sftp, 'Duplicate VERSION packet');
|
|
2530
|
+
|
|
2531
|
+
const extensions = {};
|
|
2532
|
+
|
|
2533
|
+
/*
|
|
2534
|
+
uint32 version
|
|
2535
|
+
<extension data>
|
|
2536
|
+
*/
|
|
2537
|
+
bufferParser.init(payload, 1);
|
|
2538
|
+
let version = bufferParser.readUInt32BE();
|
|
2539
|
+
while (bufferParser.avail()) {
|
|
2540
|
+
const extName = bufferParser.readString(true);
|
|
2541
|
+
const extData = bufferParser.readString(true);
|
|
2542
|
+
if (extData === undefined) {
|
|
2543
|
+
version = undefined;
|
|
2544
|
+
break;
|
|
2545
|
+
}
|
|
2546
|
+
extensions[extName] = extData;
|
|
2547
|
+
}
|
|
2548
|
+
bufferParser.clear();
|
|
2549
|
+
|
|
2550
|
+
if (version === undefined)
|
|
2551
|
+
return doFatalSFTPError(sftp, 'Malformed VERSION packet');
|
|
2552
|
+
|
|
2553
|
+
if (sftp._debug) {
|
|
2554
|
+
const names = Object.keys(extensions);
|
|
2555
|
+
if (names.length) {
|
|
2556
|
+
sftp._debug(
|
|
2557
|
+
`SFTP: Inbound: Received VERSION (v${version}, exts:${names})`
|
|
2558
|
+
);
|
|
2559
|
+
} else {
|
|
2560
|
+
sftp._debug(`SFTP: Inbound: Received VERSION (v${version})`);
|
|
2561
|
+
}
|
|
2562
|
+
}
|
|
2563
|
+
|
|
2564
|
+
sftp._version = version;
|
|
2565
|
+
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
|
+
|
|
2584
|
+
sftp.emit('ready');
|
|
2585
|
+
},
|
|
2586
|
+
[RESPONSE.STATUS]: (sftp, payload) => {
|
|
2587
|
+
bufferParser.init(payload, 1);
|
|
2588
|
+
const reqID = bufferParser.readUInt32BE();
|
|
2589
|
+
/*
|
|
2590
|
+
uint32 error/status code
|
|
2591
|
+
string error message (ISO-10646 UTF-8)
|
|
2592
|
+
string language tag
|
|
2593
|
+
*/
|
|
2594
|
+
const errorCode = bufferParser.readUInt32BE();
|
|
2595
|
+
const errorMsg = bufferParser.readString(true);
|
|
2596
|
+
const lang = bufferParser.skipString();
|
|
2597
|
+
bufferParser.clear();
|
|
2598
|
+
|
|
2599
|
+
if (lang === undefined) {
|
|
2600
|
+
if (reqID !== undefined)
|
|
2601
|
+
delete sftp._requests[reqID];
|
|
2602
|
+
return doFatalSFTPError(sftp, 'Malformed STATUS packet');
|
|
2603
|
+
}
|
|
2604
|
+
|
|
2605
|
+
if (sftp._debug) {
|
|
2606
|
+
const jsonMsg = JSON.stringify(errorMsg);
|
|
2607
|
+
sftp._debug(
|
|
2608
|
+
`SFTP: Inbound: Received STATUS (id:${reqID}, ${errorCode}, ${jsonMsg})`
|
|
2609
|
+
);
|
|
2610
|
+
}
|
|
2611
|
+
const req = sftp._requests[reqID];
|
|
2612
|
+
delete sftp._requests[reqID];
|
|
2613
|
+
if (req && typeof req.cb === 'function') {
|
|
2614
|
+
if (errorCode === STATUS_CODE.OK) {
|
|
2615
|
+
req.cb();
|
|
2616
|
+
return;
|
|
2617
|
+
}
|
|
2618
|
+
const err = new Error(errorMsg
|
|
2619
|
+
|| STATUS_CODE_STR[errorCode]
|
|
2620
|
+
|| 'Unknown status');
|
|
2621
|
+
err.code = errorCode;
|
|
2622
|
+
req.cb(err);
|
|
2623
|
+
}
|
|
2624
|
+
},
|
|
2625
|
+
[RESPONSE.HANDLE]: (sftp, payload) => {
|
|
2626
|
+
bufferParser.init(payload, 1);
|
|
2627
|
+
const reqID = bufferParser.readUInt32BE();
|
|
2628
|
+
/*
|
|
2629
|
+
string handle
|
|
2630
|
+
*/
|
|
2631
|
+
const handle = bufferParser.readString();
|
|
2632
|
+
bufferParser.clear();
|
|
2633
|
+
|
|
2634
|
+
if (handle === undefined) {
|
|
2635
|
+
if (reqID !== undefined)
|
|
2636
|
+
delete sftp._requests[reqID];
|
|
2637
|
+
return doFatalSFTPError(sftp, 'Malformed HANDLE packet');
|
|
2638
|
+
}
|
|
2639
|
+
|
|
2640
|
+
sftp._debug && sftp._debug(`SFTP: Inbound: Received HANDLE (id:${reqID})`);
|
|
2641
|
+
|
|
2642
|
+
const req = sftp._requests[reqID];
|
|
2643
|
+
delete sftp._requests[reqID];
|
|
2644
|
+
if (req && typeof req.cb === 'function')
|
|
2645
|
+
req.cb(undefined, handle);
|
|
2646
|
+
},
|
|
2647
|
+
[RESPONSE.DATA]: (sftp, payload) => {
|
|
2648
|
+
bufferParser.init(payload, 1);
|
|
2649
|
+
const reqID = bufferParser.readUInt32BE();
|
|
2650
|
+
let req;
|
|
2651
|
+
if (reqID !== undefined) {
|
|
2652
|
+
req = sftp._requests[reqID];
|
|
2653
|
+
delete sftp._requests[reqID];
|
|
2654
|
+
}
|
|
2655
|
+
/*
|
|
2656
|
+
string data
|
|
2657
|
+
*/
|
|
2658
|
+
if (req && typeof req.cb === 'function') {
|
|
2659
|
+
if (req.buffer) {
|
|
2660
|
+
// We have already pre-allocated space to store the data
|
|
2661
|
+
|
|
2662
|
+
const nb = bufferParser.readString(req.buffer);
|
|
2663
|
+
bufferParser.clear();
|
|
2664
|
+
|
|
2665
|
+
if (nb !== undefined) {
|
|
2666
|
+
sftp._debug && sftp._debug(
|
|
2667
|
+
`SFTP: Inbound: Received DATA (id:${reqID}, ${nb})`
|
|
2668
|
+
);
|
|
2669
|
+
req.cb(undefined, req.buffer, nb);
|
|
2670
|
+
return;
|
|
2671
|
+
}
|
|
2672
|
+
} else {
|
|
2673
|
+
const data = bufferParser.readString();
|
|
2674
|
+
bufferParser.clear();
|
|
2675
|
+
|
|
2676
|
+
if (data !== undefined) {
|
|
2677
|
+
sftp._debug && sftp._debug(
|
|
2678
|
+
`SFTP: Inbound: Received DATA (id:${reqID}, ${data.length})`
|
|
2679
|
+
);
|
|
2680
|
+
req.cb(undefined, data);
|
|
2681
|
+
return;
|
|
2682
|
+
}
|
|
2683
|
+
}
|
|
2684
|
+
} else {
|
|
2685
|
+
const nb = bufferParser.skipString();
|
|
2686
|
+
bufferParser.clear();
|
|
2687
|
+
if (nb !== undefined) {
|
|
2688
|
+
sftp._debug && sftp._debug(
|
|
2689
|
+
`SFTP: Inbound: Received DATA (id:${reqID}, ${nb})`
|
|
2690
|
+
);
|
|
2691
|
+
return;
|
|
2692
|
+
}
|
|
2693
|
+
}
|
|
2694
|
+
|
|
2695
|
+
return doFatalSFTPError(sftp, 'Malformed DATA packet');
|
|
2696
|
+
},
|
|
2697
|
+
[RESPONSE.NAME]: (sftp, payload) => {
|
|
2698
|
+
bufferParser.init(payload, 1);
|
|
2699
|
+
const reqID = bufferParser.readUInt32BE();
|
|
2700
|
+
let req;
|
|
2701
|
+
if (reqID !== undefined) {
|
|
2702
|
+
req = sftp._requests[reqID];
|
|
2703
|
+
delete sftp._requests[reqID];
|
|
2704
|
+
}
|
|
2705
|
+
/*
|
|
2706
|
+
uint32 count
|
|
2707
|
+
repeats count times:
|
|
2708
|
+
string filename
|
|
2709
|
+
string longname
|
|
2710
|
+
ATTRS attrs
|
|
2711
|
+
*/
|
|
2712
|
+
const count = bufferParser.readUInt32BE();
|
|
2713
|
+
if (count !== undefined) {
|
|
2714
|
+
let names = [];
|
|
2715
|
+
for (let i = 0; i < count; ++i) {
|
|
2716
|
+
// We are going to assume UTF-8 for filenames despite the SFTPv3
|
|
2717
|
+
// spec not specifying an encoding because the specs for newer
|
|
2718
|
+
// versions of the protocol all explicitly specify UTF-8 for
|
|
2719
|
+
// filenames
|
|
2720
|
+
const filename = bufferParser.readString(true);
|
|
2721
|
+
|
|
2722
|
+
// `longname` only exists in SFTPv3 and since it typically will
|
|
2723
|
+
// contain the filename, we assume it is also UTF-8
|
|
2724
|
+
const longname = bufferParser.readString(true);
|
|
2725
|
+
|
|
2726
|
+
const attrs = readAttrs(sftp._biOpt);
|
|
2727
|
+
if (attrs === undefined) {
|
|
2728
|
+
names = undefined;
|
|
2729
|
+
break;
|
|
2730
|
+
}
|
|
2731
|
+
names.push({ filename, longname, attrs });
|
|
2732
|
+
}
|
|
2733
|
+
if (names !== undefined) {
|
|
2734
|
+
sftp._debug && sftp._debug(
|
|
2735
|
+
`SFTP: Inbound: Received NAME (id:${reqID}, ${names.length})`
|
|
2736
|
+
);
|
|
2737
|
+
bufferParser.clear();
|
|
2738
|
+
if (req && typeof req.cb === 'function')
|
|
2739
|
+
req.cb(undefined, names);
|
|
2740
|
+
return;
|
|
2741
|
+
}
|
|
2742
|
+
}
|
|
2743
|
+
|
|
2744
|
+
bufferParser.clear();
|
|
2745
|
+
return doFatalSFTPError(sftp, 'Malformed NAME packet');
|
|
2746
|
+
},
|
|
2747
|
+
[RESPONSE.ATTRS]: (sftp, payload) => {
|
|
2748
|
+
bufferParser.init(payload, 1);
|
|
2749
|
+
const reqID = bufferParser.readUInt32BE();
|
|
2750
|
+
let req;
|
|
2751
|
+
if (reqID !== undefined) {
|
|
2752
|
+
req = sftp._requests[reqID];
|
|
2753
|
+
delete sftp._requests[reqID];
|
|
2754
|
+
}
|
|
2755
|
+
/*
|
|
2756
|
+
ATTRS attrs
|
|
2757
|
+
*/
|
|
2758
|
+
const attrs = readAttrs(sftp._biOpt);
|
|
2759
|
+
bufferParser.clear();
|
|
2760
|
+
if (attrs !== undefined) {
|
|
2761
|
+
sftp._debug && sftp._debug(`SFTP: Inbound: Received ATTRS (id:${reqID})`);
|
|
2762
|
+
if (req && typeof req.cb === 'function')
|
|
2763
|
+
req.cb(undefined, attrs);
|
|
2764
|
+
return;
|
|
2765
|
+
}
|
|
2766
|
+
|
|
2767
|
+
return doFatalSFTPError(sftp, 'Malformed ATTRS packet');
|
|
2768
|
+
},
|
|
2769
|
+
[RESPONSE.EXTENDED]: (sftp, payload) => {
|
|
2770
|
+
bufferParser.init(payload, 1);
|
|
2771
|
+
const reqID = bufferParser.readUInt32BE();
|
|
2772
|
+
if (reqID !== undefined) {
|
|
2773
|
+
const req = sftp._requests[reqID];
|
|
2774
|
+
if (req) {
|
|
2775
|
+
delete sftp._requests[reqID];
|
|
2776
|
+
switch (req.extended) {
|
|
2777
|
+
case 'statvfs@openssh.com':
|
|
2778
|
+
case 'fstatvfs@openssh.com': {
|
|
2779
|
+
/*
|
|
2780
|
+
uint64 f_bsize // file system block size
|
|
2781
|
+
uint64 f_frsize // fundamental fs block size
|
|
2782
|
+
uint64 f_blocks // number of blocks (unit f_frsize)
|
|
2783
|
+
uint64 f_bfree // free blocks in file system
|
|
2784
|
+
uint64 f_bavail // free blocks for non-root
|
|
2785
|
+
uint64 f_files // total file inodes
|
|
2786
|
+
uint64 f_ffree // free file inodes
|
|
2787
|
+
uint64 f_favail // free file inodes for to non-root
|
|
2788
|
+
uint64 f_fsid // file system id
|
|
2789
|
+
uint64 f_flag // bit mask of f_flag values
|
|
2790
|
+
uint64 f_namemax // maximum filename length
|
|
2791
|
+
*/
|
|
2792
|
+
const biOpt = sftp._biOpt;
|
|
2793
|
+
const stats = {
|
|
2794
|
+
f_bsize: bufferParser.readUInt64BE(biOpt),
|
|
2795
|
+
f_frsize: bufferParser.readUInt64BE(biOpt),
|
|
2796
|
+
f_blocks: bufferParser.readUInt64BE(biOpt),
|
|
2797
|
+
f_bfree: bufferParser.readUInt64BE(biOpt),
|
|
2798
|
+
f_bavail: bufferParser.readUInt64BE(biOpt),
|
|
2799
|
+
f_files: bufferParser.readUInt64BE(biOpt),
|
|
2800
|
+
f_ffree: bufferParser.readUInt64BE(biOpt),
|
|
2801
|
+
f_favail: bufferParser.readUInt64BE(biOpt),
|
|
2802
|
+
f_sid: bufferParser.readUInt64BE(biOpt),
|
|
2803
|
+
f_flag: bufferParser.readUInt64BE(biOpt),
|
|
2804
|
+
f_namemax: bufferParser.readUInt64BE(biOpt),
|
|
2805
|
+
};
|
|
2806
|
+
if (stats.f_namemax === undefined)
|
|
2807
|
+
break;
|
|
2808
|
+
if (sftp._debug) {
|
|
2809
|
+
sftp._debug(
|
|
2810
|
+
'SFTP: Inbound: Received EXTENDED_REPLY '
|
|
2811
|
+
+ `(id:${reqID}, ${req.extended})`
|
|
2812
|
+
);
|
|
2813
|
+
}
|
|
2814
|
+
bufferParser.clear();
|
|
2815
|
+
if (typeof req.cb === 'function')
|
|
2816
|
+
req.cb(undefined, stats);
|
|
2817
|
+
return;
|
|
2818
|
+
}
|
|
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
|
+
}
|
|
2845
|
+
default:
|
|
2846
|
+
// Unknown extended request
|
|
2847
|
+
sftp._debug && sftp._debug(
|
|
2848
|
+
`SFTP: Inbound: Received EXTENDED_REPLY (id:${reqID}, ???)`
|
|
2849
|
+
);
|
|
2850
|
+
bufferParser.clear();
|
|
2851
|
+
if (typeof req.cb === 'function')
|
|
2852
|
+
req.cb();
|
|
2853
|
+
return;
|
|
2854
|
+
}
|
|
2855
|
+
} else {
|
|
2856
|
+
sftp._debug && sftp._debug(
|
|
2857
|
+
`SFTP: Inbound: Received EXTENDED_REPLY (id:${reqID}, ???)`
|
|
2858
|
+
);
|
|
2859
|
+
bufferParser.clear();
|
|
2860
|
+
return;
|
|
2861
|
+
}
|
|
2862
|
+
}
|
|
2863
|
+
|
|
2864
|
+
bufferParser.clear();
|
|
2865
|
+
return doFatalSFTPError(sftp, 'Malformed EXTENDED_REPLY packet');
|
|
2866
|
+
},
|
|
2867
|
+
};
|
|
2868
|
+
const SERVER_HANDLERS = {
|
|
2869
|
+
[REQUEST.INIT]: (sftp, payload) => {
|
|
2870
|
+
if (sftp._version !== -1)
|
|
2871
|
+
return doFatalSFTPError(sftp, 'Duplicate INIT packet');
|
|
2872
|
+
|
|
2873
|
+
const extensions = {};
|
|
2874
|
+
|
|
2875
|
+
/*
|
|
2876
|
+
uint32 version
|
|
2877
|
+
<extension data>
|
|
2878
|
+
*/
|
|
2879
|
+
bufferParser.init(payload, 1);
|
|
2880
|
+
let version = bufferParser.readUInt32BE();
|
|
2881
|
+
while (bufferParser.avail()) {
|
|
2882
|
+
const extName = bufferParser.readString(true);
|
|
2883
|
+
const extData = bufferParser.readString(true);
|
|
2884
|
+
if (extData === undefined) {
|
|
2885
|
+
version = undefined;
|
|
2886
|
+
break;
|
|
2887
|
+
}
|
|
2888
|
+
extensions[extName] = extData;
|
|
2889
|
+
}
|
|
2890
|
+
bufferParser.clear();
|
|
2891
|
+
|
|
2892
|
+
if (version === undefined)
|
|
2893
|
+
return doFatalSFTPError(sftp, 'Malformed INIT packet');
|
|
2894
|
+
|
|
2895
|
+
if (sftp._debug) {
|
|
2896
|
+
const names = Object.keys(extensions);
|
|
2897
|
+
if (names.length) {
|
|
2898
|
+
sftp._debug(
|
|
2899
|
+
`SFTP: Inbound: Received INIT (v${version}, exts:${names})`
|
|
2900
|
+
);
|
|
2901
|
+
} else {
|
|
2902
|
+
sftp._debug(`SFTP: Inbound: Received INIT (v${version})`);
|
|
2903
|
+
}
|
|
2904
|
+
}
|
|
2905
|
+
|
|
2906
|
+
sendOrBuffer(sftp, SERVER_VERSION_BUFFER);
|
|
2907
|
+
|
|
2908
|
+
sftp._version = version;
|
|
2909
|
+
sftp._extensions = extensions;
|
|
2910
|
+
sftp.emit('ready');
|
|
2911
|
+
},
|
|
2912
|
+
[REQUEST.OPEN]: (sftp, payload) => {
|
|
2913
|
+
bufferParser.init(payload, 1);
|
|
2914
|
+
const reqID = bufferParser.readUInt32BE();
|
|
2915
|
+
/*
|
|
2916
|
+
string filename
|
|
2917
|
+
uint32 pflags
|
|
2918
|
+
ATTRS attrs
|
|
2919
|
+
*/
|
|
2920
|
+
const filename = bufferParser.readString(true);
|
|
2921
|
+
const pflags = bufferParser.readUInt32BE();
|
|
2922
|
+
const attrs = readAttrs(sftp._biOpt);
|
|
2923
|
+
bufferParser.clear();
|
|
2924
|
+
|
|
2925
|
+
if (attrs === undefined)
|
|
2926
|
+
return doFatalSFTPError(sftp, 'Malformed OPEN packet');
|
|
2927
|
+
|
|
2928
|
+
sftp._debug && sftp._debug(`SFTP: Inbound: Received OPEN (id:${reqID})`);
|
|
2929
|
+
|
|
2930
|
+
if (!sftp.emit('OPEN', reqID, filename, pflags, attrs)) {
|
|
2931
|
+
// Automatically reject request if no handler for request type
|
|
2932
|
+
sftp.status(reqID, STATUS_CODE.OP_UNSUPPORTED);
|
|
2933
|
+
}
|
|
2934
|
+
},
|
|
2935
|
+
[REQUEST.CLOSE]: (sftp, payload) => {
|
|
2936
|
+
bufferParser.init(payload, 1);
|
|
2937
|
+
const reqID = bufferParser.readUInt32BE();
|
|
2938
|
+
/*
|
|
2939
|
+
string handle
|
|
2940
|
+
*/
|
|
2941
|
+
const handle = bufferParser.readString();
|
|
2942
|
+
bufferParser.clear();
|
|
2943
|
+
|
|
2944
|
+
if (handle === undefined || handle.length > 256)
|
|
2945
|
+
return doFatalSFTPError(sftp, 'Malformed CLOSE packet');
|
|
2946
|
+
|
|
2947
|
+
sftp._debug && sftp._debug(`SFTP: Inbound: Received CLOSE (id:${reqID})`);
|
|
2948
|
+
|
|
2949
|
+
if (!sftp.emit('CLOSE', reqID, handle)) {
|
|
2950
|
+
// Automatically reject request if no handler for request type
|
|
2951
|
+
sftp.status(reqID, STATUS_CODE.OP_UNSUPPORTED);
|
|
2952
|
+
}
|
|
2953
|
+
},
|
|
2954
|
+
[REQUEST.READ]: (sftp, payload) => {
|
|
2955
|
+
bufferParser.init(payload, 1);
|
|
2956
|
+
const reqID = bufferParser.readUInt32BE();
|
|
2957
|
+
/*
|
|
2958
|
+
string handle
|
|
2959
|
+
uint64 offset
|
|
2960
|
+
uint32 len
|
|
2961
|
+
*/
|
|
2962
|
+
const handle = bufferParser.readString();
|
|
2963
|
+
const offset = bufferParser.readUInt64BE(sftp._biOpt);
|
|
2964
|
+
const len = bufferParser.readUInt32BE();
|
|
2965
|
+
bufferParser.clear();
|
|
2966
|
+
|
|
2967
|
+
if (len === undefined || handle.length > 256)
|
|
2968
|
+
return doFatalSFTPError(sftp, 'Malformed READ packet');
|
|
2969
|
+
|
|
2970
|
+
sftp._debug && sftp._debug(`SFTP: Inbound: Received READ (id:${reqID})`);
|
|
2971
|
+
|
|
2972
|
+
if (!sftp.emit('READ', reqID, handle, offset, len)) {
|
|
2973
|
+
// Automatically reject request if no handler for request type
|
|
2974
|
+
sftp.status(reqID, STATUS_CODE.OP_UNSUPPORTED);
|
|
2975
|
+
}
|
|
2976
|
+
},
|
|
2977
|
+
[REQUEST.WRITE]: (sftp, payload) => {
|
|
2978
|
+
bufferParser.init(payload, 1);
|
|
2979
|
+
const reqID = bufferParser.readUInt32BE();
|
|
2980
|
+
/*
|
|
2981
|
+
string handle
|
|
2982
|
+
uint64 offset
|
|
2983
|
+
string data
|
|
2984
|
+
*/
|
|
2985
|
+
const handle = bufferParser.readString();
|
|
2986
|
+
const offset = bufferParser.readUInt64BE(sftp._biOpt);
|
|
2987
|
+
const data = bufferParser.readString();
|
|
2988
|
+
bufferParser.clear();
|
|
2989
|
+
|
|
2990
|
+
if (data === undefined || handle.length > 256)
|
|
2991
|
+
return doFatalSFTPError(sftp, 'Malformed WRITE packet');
|
|
2992
|
+
|
|
2993
|
+
sftp._debug && sftp._debug(`SFTP: Inbound: Received WRITE (id:${reqID})`);
|
|
2994
|
+
|
|
2995
|
+
if (!sftp.emit('WRITE', reqID, handle, offset, data)) {
|
|
2996
|
+
// Automatically reject request if no handler for request type
|
|
2997
|
+
sftp.status(reqID, STATUS_CODE.OP_UNSUPPORTED);
|
|
2998
|
+
}
|
|
2999
|
+
},
|
|
3000
|
+
[REQUEST.LSTAT]: (sftp, payload) => {
|
|
3001
|
+
bufferParser.init(payload, 1);
|
|
3002
|
+
const reqID = bufferParser.readUInt32BE();
|
|
3003
|
+
/*
|
|
3004
|
+
string path
|
|
3005
|
+
*/
|
|
3006
|
+
const path = bufferParser.readString(true);
|
|
3007
|
+
bufferParser.clear();
|
|
3008
|
+
|
|
3009
|
+
if (path === undefined)
|
|
3010
|
+
return doFatalSFTPError(sftp, 'Malformed LSTAT packet');
|
|
3011
|
+
|
|
3012
|
+
sftp._debug && sftp._debug(`SFTP: Inbound: Received LSTAT (id:${reqID})`);
|
|
3013
|
+
|
|
3014
|
+
if (!sftp.emit('LSTAT', reqID, path)) {
|
|
3015
|
+
// Automatically reject request if no handler for request type
|
|
3016
|
+
sftp.status(reqID, STATUS_CODE.OP_UNSUPPORTED);
|
|
3017
|
+
}
|
|
3018
|
+
},
|
|
3019
|
+
[REQUEST.FSTAT]: (sftp, payload) => {
|
|
3020
|
+
bufferParser.init(payload, 1);
|
|
3021
|
+
const reqID = bufferParser.readUInt32BE();
|
|
3022
|
+
/*
|
|
3023
|
+
string handle
|
|
3024
|
+
*/
|
|
3025
|
+
const handle = bufferParser.readString();
|
|
3026
|
+
bufferParser.clear();
|
|
3027
|
+
|
|
3028
|
+
if (handle === undefined || handle.length > 256)
|
|
3029
|
+
return doFatalSFTPError(sftp, 'Malformed FSTAT packet');
|
|
3030
|
+
|
|
3031
|
+
sftp._debug && sftp._debug(`SFTP: Inbound: Received FSTAT (id:${reqID})`);
|
|
3032
|
+
|
|
3033
|
+
if (!sftp.emit('FSTAT', reqID, handle)) {
|
|
3034
|
+
// Automatically reject request if no handler for request type
|
|
3035
|
+
sftp.status(reqID, STATUS_CODE.OP_UNSUPPORTED);
|
|
3036
|
+
}
|
|
3037
|
+
},
|
|
3038
|
+
[REQUEST.SETSTAT]: (sftp, payload) => {
|
|
3039
|
+
bufferParser.init(payload, 1);
|
|
3040
|
+
const reqID = bufferParser.readUInt32BE();
|
|
3041
|
+
/*
|
|
3042
|
+
string path
|
|
3043
|
+
ATTRS attrs
|
|
3044
|
+
*/
|
|
3045
|
+
const path = bufferParser.readString(true);
|
|
3046
|
+
const attrs = readAttrs(sftp._biOpt);
|
|
3047
|
+
bufferParser.clear();
|
|
3048
|
+
|
|
3049
|
+
if (attrs === undefined)
|
|
3050
|
+
return doFatalSFTPError(sftp, 'Malformed SETSTAT packet');
|
|
3051
|
+
|
|
3052
|
+
sftp._debug && sftp._debug(`SFTP: Inbound: Received SETSTAT (id:${reqID})`);
|
|
3053
|
+
|
|
3054
|
+
if (!sftp.emit('SETSTAT', reqID, path, attrs)) {
|
|
3055
|
+
// Automatically reject request if no handler for request type
|
|
3056
|
+
sftp.status(reqID, STATUS_CODE.OP_UNSUPPORTED);
|
|
3057
|
+
}
|
|
3058
|
+
},
|
|
3059
|
+
[REQUEST.FSETSTAT]: (sftp, payload) => {
|
|
3060
|
+
bufferParser.init(payload, 1);
|
|
3061
|
+
const reqID = bufferParser.readUInt32BE();
|
|
3062
|
+
/*
|
|
3063
|
+
string handle
|
|
3064
|
+
ATTRS attrs
|
|
3065
|
+
*/
|
|
3066
|
+
const handle = bufferParser.readString();
|
|
3067
|
+
const attrs = readAttrs(sftp._biOpt);
|
|
3068
|
+
bufferParser.clear();
|
|
3069
|
+
|
|
3070
|
+
if (attrs === undefined || handle.length > 256)
|
|
3071
|
+
return doFatalSFTPError(sftp, 'Malformed FSETSTAT packet');
|
|
3072
|
+
|
|
3073
|
+
sftp._debug && sftp._debug(
|
|
3074
|
+
`SFTP: Inbound: Received FSETSTAT (id:${reqID})`
|
|
3075
|
+
);
|
|
3076
|
+
|
|
3077
|
+
if (!sftp.emit('FSETSTAT', reqID, handle, attrs)) {
|
|
3078
|
+
// Automatically reject request if no handler for request type
|
|
3079
|
+
sftp.status(reqID, STATUS_CODE.OP_UNSUPPORTED);
|
|
3080
|
+
}
|
|
3081
|
+
},
|
|
3082
|
+
[REQUEST.OPENDIR]: (sftp, payload) => {
|
|
3083
|
+
bufferParser.init(payload, 1);
|
|
3084
|
+
const reqID = bufferParser.readUInt32BE();
|
|
3085
|
+
/*
|
|
3086
|
+
string path
|
|
3087
|
+
*/
|
|
3088
|
+
const path = bufferParser.readString(true);
|
|
3089
|
+
bufferParser.clear();
|
|
3090
|
+
|
|
3091
|
+
if (path === undefined)
|
|
3092
|
+
return doFatalSFTPError(sftp, 'Malformed OPENDIR packet');
|
|
3093
|
+
|
|
3094
|
+
sftp._debug && sftp._debug(`SFTP: Inbound: Received OPENDIR (id:${reqID})`);
|
|
3095
|
+
|
|
3096
|
+
if (!sftp.emit('OPENDIR', reqID, path)) {
|
|
3097
|
+
// Automatically reject request if no handler for request type
|
|
3098
|
+
sftp.status(reqID, STATUS_CODE.OP_UNSUPPORTED);
|
|
3099
|
+
}
|
|
3100
|
+
},
|
|
3101
|
+
[REQUEST.READDIR]: (sftp, payload) => {
|
|
3102
|
+
bufferParser.init(payload, 1);
|
|
3103
|
+
const reqID = bufferParser.readUInt32BE();
|
|
3104
|
+
/*
|
|
3105
|
+
string handle
|
|
3106
|
+
*/
|
|
3107
|
+
const handle = bufferParser.readString();
|
|
3108
|
+
bufferParser.clear();
|
|
3109
|
+
|
|
3110
|
+
if (handle === undefined || handle.length > 256)
|
|
3111
|
+
return doFatalSFTPError(sftp, 'Malformed READDIR packet');
|
|
3112
|
+
|
|
3113
|
+
sftp._debug && sftp._debug(`SFTP: Inbound: Received READDIR (id:${reqID})`);
|
|
3114
|
+
|
|
3115
|
+
if (!sftp.emit('READDIR', reqID, handle)) {
|
|
3116
|
+
// Automatically reject request if no handler for request type
|
|
3117
|
+
sftp.status(reqID, STATUS_CODE.OP_UNSUPPORTED);
|
|
3118
|
+
}
|
|
3119
|
+
},
|
|
3120
|
+
[REQUEST.REMOVE]: (sftp, payload) => {
|
|
3121
|
+
bufferParser.init(payload, 1);
|
|
3122
|
+
const reqID = bufferParser.readUInt32BE();
|
|
3123
|
+
/*
|
|
3124
|
+
string path
|
|
3125
|
+
*/
|
|
3126
|
+
const path = bufferParser.readString(true);
|
|
3127
|
+
bufferParser.clear();
|
|
3128
|
+
|
|
3129
|
+
if (path === undefined)
|
|
3130
|
+
return doFatalSFTPError(sftp, 'Malformed REMOVE packet');
|
|
3131
|
+
|
|
3132
|
+
sftp._debug && sftp._debug(`SFTP: Inbound: Received REMOVE (id:${reqID})`);
|
|
3133
|
+
|
|
3134
|
+
if (!sftp.emit('REMOVE', reqID, path)) {
|
|
3135
|
+
// Automatically reject request if no handler for request type
|
|
3136
|
+
sftp.status(reqID, STATUS_CODE.OP_UNSUPPORTED);
|
|
3137
|
+
}
|
|
3138
|
+
},
|
|
3139
|
+
[REQUEST.MKDIR]: (sftp, payload) => {
|
|
3140
|
+
bufferParser.init(payload, 1);
|
|
3141
|
+
const reqID = bufferParser.readUInt32BE();
|
|
3142
|
+
/*
|
|
3143
|
+
string path
|
|
3144
|
+
ATTRS attrs
|
|
3145
|
+
*/
|
|
3146
|
+
const path = bufferParser.readString(true);
|
|
3147
|
+
const attrs = readAttrs(sftp._biOpt);
|
|
3148
|
+
bufferParser.clear();
|
|
3149
|
+
|
|
3150
|
+
if (attrs === undefined)
|
|
3151
|
+
return doFatalSFTPError(sftp, 'Malformed MKDIR packet');
|
|
3152
|
+
|
|
3153
|
+
sftp._debug && sftp._debug(`SFTP: Inbound: Received MKDIR (id:${reqID})`);
|
|
3154
|
+
|
|
3155
|
+
if (!sftp.emit('MKDIR', reqID, path, attrs)) {
|
|
3156
|
+
// Automatically reject request if no handler for request type
|
|
3157
|
+
sftp.status(reqID, STATUS_CODE.OP_UNSUPPORTED);
|
|
3158
|
+
}
|
|
3159
|
+
},
|
|
3160
|
+
[REQUEST.RMDIR]: (sftp, payload) => {
|
|
3161
|
+
bufferParser.init(payload, 1);
|
|
3162
|
+
const reqID = bufferParser.readUInt32BE();
|
|
3163
|
+
/*
|
|
3164
|
+
string path
|
|
3165
|
+
*/
|
|
3166
|
+
const path = bufferParser.readString(true);
|
|
3167
|
+
bufferParser.clear();
|
|
3168
|
+
|
|
3169
|
+
if (path === undefined)
|
|
3170
|
+
return doFatalSFTPError(sftp, 'Malformed RMDIR packet');
|
|
3171
|
+
|
|
3172
|
+
sftp._debug && sftp._debug(`SFTP: Inbound: Received RMDIR (id:${reqID})`);
|
|
3173
|
+
|
|
3174
|
+
if (!sftp.emit('RMDIR', reqID, path)) {
|
|
3175
|
+
// Automatically reject request if no handler for request type
|
|
3176
|
+
sftp.status(reqID, STATUS_CODE.OP_UNSUPPORTED);
|
|
3177
|
+
}
|
|
3178
|
+
},
|
|
3179
|
+
[REQUEST.REALPATH]: (sftp, payload) => {
|
|
3180
|
+
bufferParser.init(payload, 1);
|
|
3181
|
+
const reqID = bufferParser.readUInt32BE();
|
|
3182
|
+
/*
|
|
3183
|
+
string path
|
|
3184
|
+
*/
|
|
3185
|
+
const path = bufferParser.readString(true);
|
|
3186
|
+
bufferParser.clear();
|
|
3187
|
+
|
|
3188
|
+
if (path === undefined)
|
|
3189
|
+
return doFatalSFTPError(sftp, 'Malformed REALPATH packet');
|
|
3190
|
+
|
|
3191
|
+
sftp._debug && sftp._debug(
|
|
3192
|
+
`SFTP: Inbound: Received REALPATH (id:${reqID})`
|
|
3193
|
+
);
|
|
3194
|
+
|
|
3195
|
+
if (!sftp.emit('REALPATH', reqID, path)) {
|
|
3196
|
+
// Automatically reject request if no handler for request type
|
|
3197
|
+
sftp.status(reqID, STATUS_CODE.OP_UNSUPPORTED);
|
|
3198
|
+
}
|
|
3199
|
+
},
|
|
3200
|
+
[REQUEST.STAT]: (sftp, payload) => {
|
|
3201
|
+
bufferParser.init(payload, 1);
|
|
3202
|
+
const reqID = bufferParser.readUInt32BE();
|
|
3203
|
+
/*
|
|
3204
|
+
string path
|
|
3205
|
+
*/
|
|
3206
|
+
const path = bufferParser.readString(true);
|
|
3207
|
+
bufferParser.clear();
|
|
3208
|
+
|
|
3209
|
+
if (path === undefined)
|
|
3210
|
+
return doFatalSFTPError(sftp, 'Malformed STAT packet');
|
|
3211
|
+
|
|
3212
|
+
sftp._debug && sftp._debug(`SFTP: Inbound: Received STAT (id:${reqID})`);
|
|
3213
|
+
|
|
3214
|
+
if (!sftp.emit('STAT', reqID, path)) {
|
|
3215
|
+
// Automatically reject request if no handler for request type
|
|
3216
|
+
sftp.status(reqID, STATUS_CODE.OP_UNSUPPORTED);
|
|
3217
|
+
}
|
|
3218
|
+
},
|
|
3219
|
+
[REQUEST.RENAME]: (sftp, payload) => {
|
|
3220
|
+
bufferParser.init(payload, 1);
|
|
3221
|
+
const reqID = bufferParser.readUInt32BE();
|
|
3222
|
+
/*
|
|
3223
|
+
string oldpath
|
|
3224
|
+
string newpath
|
|
3225
|
+
*/
|
|
3226
|
+
const oldPath = bufferParser.readString(true);
|
|
3227
|
+
const newPath = bufferParser.readString(true);
|
|
3228
|
+
bufferParser.clear();
|
|
3229
|
+
|
|
3230
|
+
if (newPath === undefined)
|
|
3231
|
+
return doFatalSFTPError(sftp, 'Malformed RENAME packet');
|
|
3232
|
+
|
|
3233
|
+
sftp._debug && sftp._debug(`SFTP: Inbound: Received RENAME (id:${reqID})`);
|
|
3234
|
+
|
|
3235
|
+
if (!sftp.emit('RENAME', reqID, oldPath, newPath)) {
|
|
3236
|
+
// Automatically reject request if no handler for request type
|
|
3237
|
+
sftp.status(reqID, STATUS_CODE.OP_UNSUPPORTED);
|
|
3238
|
+
}
|
|
3239
|
+
},
|
|
3240
|
+
[REQUEST.READLINK]: (sftp, payload) => {
|
|
3241
|
+
bufferParser.init(payload, 1);
|
|
3242
|
+
const reqID = bufferParser.readUInt32BE();
|
|
3243
|
+
/*
|
|
3244
|
+
string path
|
|
3245
|
+
*/
|
|
3246
|
+
const path = bufferParser.readString(true);
|
|
3247
|
+
bufferParser.clear();
|
|
3248
|
+
|
|
3249
|
+
if (path === undefined)
|
|
3250
|
+
return doFatalSFTPError(sftp, 'Malformed READLINK packet');
|
|
3251
|
+
|
|
3252
|
+
sftp._debug && sftp._debug(
|
|
3253
|
+
`SFTP: Inbound: Received READLINK (id:${reqID})`
|
|
3254
|
+
);
|
|
3255
|
+
|
|
3256
|
+
if (!sftp.emit('READLINK', reqID, path)) {
|
|
3257
|
+
// Automatically reject request if no handler for request type
|
|
3258
|
+
sftp.status(reqID, STATUS_CODE.OP_UNSUPPORTED);
|
|
3259
|
+
}
|
|
3260
|
+
},
|
|
3261
|
+
[REQUEST.SYMLINK]: (sftp, payload) => {
|
|
3262
|
+
bufferParser.init(payload, 1);
|
|
3263
|
+
const reqID = bufferParser.readUInt32BE();
|
|
3264
|
+
/*
|
|
3265
|
+
string linkpath
|
|
3266
|
+
string targetpath
|
|
3267
|
+
*/
|
|
3268
|
+
const linkPath = bufferParser.readString(true);
|
|
3269
|
+
const targetPath = bufferParser.readString(true);
|
|
3270
|
+
bufferParser.clear();
|
|
3271
|
+
|
|
3272
|
+
if (targetPath === undefined)
|
|
3273
|
+
return doFatalSFTPError(sftp, 'Malformed SYMLINK packet');
|
|
3274
|
+
|
|
3275
|
+
sftp._debug && sftp._debug(`SFTP: Inbound: Received SYMLINK (id:${reqID})`);
|
|
3276
|
+
|
|
3277
|
+
let handled;
|
|
3278
|
+
if (sftp._isOpenSSH) {
|
|
3279
|
+
// OpenSSH has linkpath and targetpath positions switched
|
|
3280
|
+
handled = sftp.emit('SYMLINK', reqID, targetPath, linkPath);
|
|
3281
|
+
} else {
|
|
3282
|
+
handled = sftp.emit('SYMLINK', reqID, linkPath, targetPath);
|
|
3283
|
+
}
|
|
3284
|
+
if (!handled) {
|
|
3285
|
+
// Automatically reject request if no handler for request type
|
|
3286
|
+
sftp.status(reqID, STATUS_CODE.OP_UNSUPPORTED);
|
|
3287
|
+
}
|
|
3288
|
+
},
|
|
3289
|
+
[REQUEST.EXTENDED]: (sftp, payload) => {
|
|
3290
|
+
bufferParser.init(payload, 1);
|
|
3291
|
+
const reqID = bufferParser.readUInt32BE();
|
|
3292
|
+
/*
|
|
3293
|
+
string extended-request
|
|
3294
|
+
... any request-specific data ...
|
|
3295
|
+
*/
|
|
3296
|
+
const extName = bufferParser.readString(true);
|
|
3297
|
+
if (extName === undefined) {
|
|
3298
|
+
bufferParser.clear();
|
|
3299
|
+
return doFatalSFTPError(sftp, 'Malformed EXTENDED packet');
|
|
3300
|
+
}
|
|
3301
|
+
|
|
3302
|
+
let extData;
|
|
3303
|
+
if (bufferParser.avail())
|
|
3304
|
+
extData = bufferParser.readRaw();
|
|
3305
|
+
bufferParser.clear();
|
|
3306
|
+
|
|
3307
|
+
sftp._debug && sftp._debug(
|
|
3308
|
+
`SFTP: Inbound: Received EXTENDED (id:${reqID})`
|
|
3309
|
+
);
|
|
3310
|
+
|
|
3311
|
+
if (!sftp.emit('EXTENDED', reqID, extName, extData)) {
|
|
3312
|
+
// Automatically reject request if no handler for request type
|
|
3313
|
+
sftp.status(reqID, STATUS_CODE.OP_UNSUPPORTED);
|
|
3314
|
+
}
|
|
3315
|
+
},
|
|
3316
|
+
};
|
|
3317
|
+
|
|
3318
|
+
// =============================================================================
|
|
3319
|
+
// ReadStream/WriteStream-related ==============================================
|
|
3320
|
+
// =============================================================================
|
|
3321
|
+
const {
|
|
3322
|
+
ERR_INVALID_ARG_TYPE,
|
|
3323
|
+
ERR_OUT_OF_RANGE,
|
|
3324
|
+
validateNumber
|
|
3325
|
+
} = require('./node-fs-compat');
|
|
3326
|
+
|
|
3327
|
+
const kMinPoolSpace = 128;
|
|
3328
|
+
|
|
3329
|
+
let pool;
|
|
3330
|
+
// It can happen that we expect to read a large chunk of data, and reserve
|
|
3331
|
+
// a large chunk of the pool accordingly, but the read() call only filled
|
|
3332
|
+
// a portion of it. If a concurrently executing read() then uses the same pool,
|
|
3333
|
+
// the "reserved" portion cannot be used, so we allow it to be re-used as a
|
|
3334
|
+
// new pool later.
|
|
3335
|
+
const poolFragments = [];
|
|
3336
|
+
|
|
3337
|
+
function allocNewPool(poolSize) {
|
|
3338
|
+
if (poolFragments.length > 0)
|
|
3339
|
+
pool = poolFragments.pop();
|
|
3340
|
+
else
|
|
3341
|
+
pool = Buffer.allocUnsafe(poolSize);
|
|
3342
|
+
pool.used = 0;
|
|
3343
|
+
}
|
|
3344
|
+
|
|
3345
|
+
// Check the `this.start` and `this.end` of stream.
|
|
3346
|
+
function checkPosition(pos, name) {
|
|
3347
|
+
if (!Number.isSafeInteger(pos)) {
|
|
3348
|
+
validateNumber(pos, name);
|
|
3349
|
+
if (!Number.isInteger(pos))
|
|
3350
|
+
throw new ERR_OUT_OF_RANGE(name, 'an integer', pos);
|
|
3351
|
+
throw new ERR_OUT_OF_RANGE(name, '>= 0 and <= 2 ** 53 - 1', pos);
|
|
3352
|
+
}
|
|
3353
|
+
if (pos < 0)
|
|
3354
|
+
throw new ERR_OUT_OF_RANGE(name, '>= 0 and <= 2 ** 53 - 1', pos);
|
|
3355
|
+
}
|
|
3356
|
+
|
|
3357
|
+
function roundUpToMultipleOf8(n) {
|
|
3358
|
+
return (n + 7) & ~7; // Align to 8 byte boundary.
|
|
3359
|
+
}
|
|
3360
|
+
|
|
3361
|
+
function ReadStream(sftp, path, options) {
|
|
3362
|
+
if (options === undefined)
|
|
3363
|
+
options = {};
|
|
3364
|
+
else if (typeof options === 'string')
|
|
3365
|
+
options = { encoding: options };
|
|
3366
|
+
else if (options === null || typeof options !== 'object')
|
|
3367
|
+
throw new TypeError('"options" argument must be a string or an object');
|
|
3368
|
+
else
|
|
3369
|
+
options = Object.create(options);
|
|
3370
|
+
|
|
3371
|
+
// A little bit bigger buffer and water marks by default
|
|
3372
|
+
if (options.highWaterMark === undefined)
|
|
3373
|
+
options.highWaterMark = 64 * 1024;
|
|
3374
|
+
|
|
3375
|
+
// For backwards compat do not emit close on destroy.
|
|
3376
|
+
options.emitClose = false;
|
|
3377
|
+
options.autoDestroy = false; // Node 14 major change.
|
|
3378
|
+
|
|
3379
|
+
ReadableStream.call(this, options);
|
|
3380
|
+
|
|
3381
|
+
this.path = path;
|
|
3382
|
+
this.flags = options.flags === undefined ? 'r' : options.flags;
|
|
3383
|
+
this.mode = options.mode === undefined ? 0o666 : options.mode;
|
|
3384
|
+
|
|
3385
|
+
this.start = options.start;
|
|
3386
|
+
this.end = options.end;
|
|
3387
|
+
this.autoClose = options.autoClose === undefined ? true : options.autoClose;
|
|
3388
|
+
this.pos = 0;
|
|
3389
|
+
this.bytesRead = 0;
|
|
3390
|
+
this.closed = false;
|
|
3391
|
+
|
|
3392
|
+
this.handle = options.handle === undefined ? null : options.handle;
|
|
3393
|
+
this.sftp = sftp;
|
|
3394
|
+
this._opening = false;
|
|
3395
|
+
|
|
3396
|
+
if (this.start !== undefined) {
|
|
3397
|
+
checkPosition(this.start, 'start');
|
|
3398
|
+
|
|
3399
|
+
this.pos = this.start;
|
|
3400
|
+
}
|
|
3401
|
+
|
|
3402
|
+
if (this.end === undefined) {
|
|
3403
|
+
this.end = Infinity;
|
|
3404
|
+
} else if (this.end !== Infinity) {
|
|
3405
|
+
checkPosition(this.end, 'end');
|
|
3406
|
+
|
|
3407
|
+
if (this.start !== undefined && this.start > this.end) {
|
|
3408
|
+
throw new ERR_OUT_OF_RANGE(
|
|
3409
|
+
'start',
|
|
3410
|
+
`<= "end" (here: ${this.end})`,
|
|
3411
|
+
this.start
|
|
3412
|
+
);
|
|
3413
|
+
}
|
|
3414
|
+
}
|
|
3415
|
+
|
|
3416
|
+
this.on('end', function() {
|
|
3417
|
+
if (this.autoClose)
|
|
3418
|
+
this.destroy();
|
|
3419
|
+
});
|
|
3420
|
+
|
|
3421
|
+
if (!Buffer.isBuffer(this.handle))
|
|
3422
|
+
this.open();
|
|
3423
|
+
}
|
|
3424
|
+
inherits(ReadStream, ReadableStream);
|
|
3425
|
+
|
|
3426
|
+
ReadStream.prototype.open = function() {
|
|
3427
|
+
if (this._opening)
|
|
3428
|
+
return;
|
|
3429
|
+
|
|
3430
|
+
this._opening = true;
|
|
3431
|
+
|
|
3432
|
+
this.sftp.open(this.path, this.flags, this.mode, (er, handle) => {
|
|
3433
|
+
this._opening = false;
|
|
3434
|
+
|
|
3435
|
+
if (er) {
|
|
3436
|
+
this.emit('error', er);
|
|
3437
|
+
if (this.autoClose)
|
|
3438
|
+
this.destroy();
|
|
3439
|
+
return;
|
|
3440
|
+
}
|
|
3441
|
+
|
|
3442
|
+
this.handle = handle;
|
|
3443
|
+
this.emit('open', handle);
|
|
3444
|
+
this.emit('ready');
|
|
3445
|
+
// Start the flow of data.
|
|
3446
|
+
this.read();
|
|
3447
|
+
});
|
|
3448
|
+
};
|
|
3449
|
+
|
|
3450
|
+
ReadStream.prototype._read = function(n) {
|
|
3451
|
+
if (!Buffer.isBuffer(this.handle))
|
|
3452
|
+
return this.once('open', () => this._read(n));
|
|
3453
|
+
|
|
3454
|
+
// XXX: safe to remove this?
|
|
3455
|
+
if (this.destroyed)
|
|
3456
|
+
return;
|
|
3457
|
+
|
|
3458
|
+
if (!pool || pool.length - pool.used < kMinPoolSpace) {
|
|
3459
|
+
// Discard the old pool.
|
|
3460
|
+
allocNewPool(this.readableHighWaterMark
|
|
3461
|
+
|| this._readableState.highWaterMark);
|
|
3462
|
+
}
|
|
3463
|
+
|
|
3464
|
+
// Grab another reference to the pool in the case that while we're
|
|
3465
|
+
// in the thread pool another read() finishes up the pool, and
|
|
3466
|
+
// allocates a new one.
|
|
3467
|
+
const thisPool = pool;
|
|
3468
|
+
let toRead = Math.min(pool.length - pool.used, n);
|
|
3469
|
+
const start = pool.used;
|
|
3470
|
+
|
|
3471
|
+
if (this.end !== undefined)
|
|
3472
|
+
toRead = Math.min(this.end - this.pos + 1, toRead);
|
|
3473
|
+
|
|
3474
|
+
// Already read everything we were supposed to read!
|
|
3475
|
+
// treat as EOF.
|
|
3476
|
+
if (toRead <= 0)
|
|
3477
|
+
return this.push(null);
|
|
3478
|
+
|
|
3479
|
+
// the actual read.
|
|
3480
|
+
this.sftp.read(this.handle,
|
|
3481
|
+
pool,
|
|
3482
|
+
pool.used,
|
|
3483
|
+
toRead,
|
|
3484
|
+
this.pos,
|
|
3485
|
+
(er, bytesRead) => {
|
|
3486
|
+
if (er) {
|
|
3487
|
+
this.emit('error', er);
|
|
3488
|
+
if (this.autoClose)
|
|
3489
|
+
this.destroy();
|
|
3490
|
+
return;
|
|
3491
|
+
}
|
|
3492
|
+
let b = null;
|
|
3493
|
+
|
|
3494
|
+
// Now that we know how much data we have actually read, re-wind the
|
|
3495
|
+
// 'used' field if we can, and otherwise allow the remainder of our
|
|
3496
|
+
// reservation to be used as a new pool later.
|
|
3497
|
+
if (start + toRead === thisPool.used && thisPool === pool) {
|
|
3498
|
+
thisPool.used = roundUpToMultipleOf8(thisPool.used + bytesRead - toRead);
|
|
3499
|
+
} else {
|
|
3500
|
+
// Round down to the next lowest multiple of 8 to ensure the new pool
|
|
3501
|
+
// fragment start and end positions are aligned to an 8 byte boundary.
|
|
3502
|
+
const alignedEnd = (start + toRead) & ~7;
|
|
3503
|
+
const alignedStart = roundUpToMultipleOf8(start + bytesRead);
|
|
3504
|
+
if (alignedEnd - alignedStart >= kMinPoolSpace)
|
|
3505
|
+
poolFragments.push(thisPool.slice(alignedStart, alignedEnd));
|
|
3506
|
+
}
|
|
3507
|
+
|
|
3508
|
+
if (bytesRead > 0) {
|
|
3509
|
+
this.bytesRead += bytesRead;
|
|
3510
|
+
b = thisPool.slice(start, start + bytesRead);
|
|
3511
|
+
}
|
|
3512
|
+
|
|
3513
|
+
// Move the pool positions, and internal position for reading.
|
|
3514
|
+
this.pos += bytesRead;
|
|
3515
|
+
|
|
3516
|
+
this.push(b);
|
|
3517
|
+
});
|
|
3518
|
+
|
|
3519
|
+
pool.used = roundUpToMultipleOf8(pool.used + toRead);
|
|
3520
|
+
};
|
|
3521
|
+
|
|
3522
|
+
ReadStream.prototype._destroy = function(err, cb) {
|
|
3523
|
+
if (this._opening && !Buffer.isBuffer(this.handle)) {
|
|
3524
|
+
this.once('open', closeStream.bind(null, this, cb, err));
|
|
3525
|
+
return;
|
|
3526
|
+
}
|
|
3527
|
+
|
|
3528
|
+
closeStream(this, cb, err);
|
|
3529
|
+
this.handle = null;
|
|
3530
|
+
this._opening = false;
|
|
3531
|
+
};
|
|
3532
|
+
|
|
3533
|
+
function closeStream(stream, cb, err) {
|
|
3534
|
+
if (!stream.handle)
|
|
3535
|
+
return onclose();
|
|
3536
|
+
|
|
3537
|
+
stream.sftp.close(stream.handle, onclose);
|
|
3538
|
+
|
|
3539
|
+
function onclose(er) {
|
|
3540
|
+
er = er || err;
|
|
3541
|
+
cb(er);
|
|
3542
|
+
stream.closed = true;
|
|
3543
|
+
if (!er)
|
|
3544
|
+
stream.emit('close');
|
|
3545
|
+
}
|
|
3546
|
+
}
|
|
3547
|
+
|
|
3548
|
+
ReadStream.prototype.close = function(cb) {
|
|
3549
|
+
this.destroy(null, cb);
|
|
3550
|
+
};
|
|
3551
|
+
|
|
3552
|
+
Object.defineProperty(ReadStream.prototype, 'pending', {
|
|
3553
|
+
get() {
|
|
3554
|
+
return this.handle === null;
|
|
3555
|
+
},
|
|
3556
|
+
configurable: true
|
|
3557
|
+
});
|
|
3558
|
+
|
|
3559
|
+
// TODO: add `concurrency` setting to allow more than one in-flight WRITE
|
|
3560
|
+
// request to server to improve throughput
|
|
3561
|
+
function WriteStream(sftp, path, options) {
|
|
3562
|
+
if (options === undefined)
|
|
3563
|
+
options = {};
|
|
3564
|
+
else if (typeof options === 'string')
|
|
3565
|
+
options = { encoding: options };
|
|
3566
|
+
else if (options === null || typeof options !== 'object')
|
|
3567
|
+
throw new TypeError('"options" argument must be a string or an object');
|
|
3568
|
+
else
|
|
3569
|
+
options = Object.create(options);
|
|
3570
|
+
|
|
3571
|
+
// For backwards compat do not emit close on destroy.
|
|
3572
|
+
options.emitClose = false;
|
|
3573
|
+
options.autoDestroy = false; // Node 14 major change.
|
|
3574
|
+
|
|
3575
|
+
WritableStream.call(this, options);
|
|
3576
|
+
|
|
3577
|
+
this.path = path;
|
|
3578
|
+
this.flags = options.flags === undefined ? 'w' : options.flags;
|
|
3579
|
+
this.mode = options.mode === undefined ? 0o666 : options.mode;
|
|
3580
|
+
|
|
3581
|
+
this.start = options.start;
|
|
3582
|
+
this.autoClose = options.autoClose === undefined ? true : options.autoClose;
|
|
3583
|
+
this.pos = 0;
|
|
3584
|
+
this.bytesWritten = 0;
|
|
3585
|
+
this.closed = false;
|
|
3586
|
+
|
|
3587
|
+
this.handle = options.handle === undefined ? null : options.handle;
|
|
3588
|
+
this.sftp = sftp;
|
|
3589
|
+
this._opening = false;
|
|
3590
|
+
|
|
3591
|
+
if (this.start !== undefined) {
|
|
3592
|
+
checkPosition(this.start, 'start');
|
|
3593
|
+
|
|
3594
|
+
this.pos = this.start;
|
|
3595
|
+
}
|
|
3596
|
+
|
|
3597
|
+
if (options.encoding)
|
|
3598
|
+
this.setDefaultEncoding(options.encoding);
|
|
3599
|
+
|
|
3600
|
+
// Node v6.x only
|
|
3601
|
+
this.on('finish', function() {
|
|
3602
|
+
if (this._writableState.finalCalled)
|
|
3603
|
+
return;
|
|
3604
|
+
if (this.autoClose)
|
|
3605
|
+
this.destroy();
|
|
3606
|
+
});
|
|
3607
|
+
|
|
3608
|
+
if (!Buffer.isBuffer(this.handle))
|
|
3609
|
+
this.open();
|
|
3610
|
+
}
|
|
3611
|
+
inherits(WriteStream, WritableStream);
|
|
3612
|
+
|
|
3613
|
+
WriteStream.prototype._final = function(cb) {
|
|
3614
|
+
if (this.autoClose)
|
|
3615
|
+
this.destroy();
|
|
3616
|
+
cb();
|
|
3617
|
+
};
|
|
3618
|
+
|
|
3619
|
+
WriteStream.prototype.open = function() {
|
|
3620
|
+
if (this._opening)
|
|
3621
|
+
return;
|
|
3622
|
+
|
|
3623
|
+
this._opening = true;
|
|
3624
|
+
|
|
3625
|
+
this.sftp.open(this.path, this.flags, this.mode, (er, handle) => {
|
|
3626
|
+
this._opening = false;
|
|
3627
|
+
|
|
3628
|
+
if (er) {
|
|
3629
|
+
this.emit('error', er);
|
|
3630
|
+
if (this.autoClose)
|
|
3631
|
+
this.destroy();
|
|
3632
|
+
return;
|
|
3633
|
+
}
|
|
3634
|
+
|
|
3635
|
+
this.handle = handle;
|
|
3636
|
+
|
|
3637
|
+
const tryAgain = (err) => {
|
|
3638
|
+
if (err) {
|
|
3639
|
+
// Try chmod() for sftp servers that may not support fchmod() for
|
|
3640
|
+
// whatever reason
|
|
3641
|
+
this.sftp.chmod(this.path, this.mode, (err_) => tryAgain());
|
|
3642
|
+
return;
|
|
3643
|
+
}
|
|
3644
|
+
|
|
3645
|
+
// SFTPv3 requires absolute offsets, no matter the open flag used
|
|
3646
|
+
if (this.flags[0] === 'a') {
|
|
3647
|
+
const tryStat = (err, st) => {
|
|
3648
|
+
if (err) {
|
|
3649
|
+
// Try stat() for sftp servers that may not support fstat() for
|
|
3650
|
+
// whatever reason
|
|
3651
|
+
this.sftp.stat(this.path, (err_, st_) => {
|
|
3652
|
+
if (err_) {
|
|
3653
|
+
this.destroy();
|
|
3654
|
+
this.emit('error', err);
|
|
3655
|
+
return;
|
|
3656
|
+
}
|
|
3657
|
+
tryStat(null, st_);
|
|
3658
|
+
});
|
|
3659
|
+
return;
|
|
3660
|
+
}
|
|
3661
|
+
|
|
3662
|
+
this.pos = st.size;
|
|
3663
|
+
this.emit('open', handle);
|
|
3664
|
+
this.emit('ready');
|
|
3665
|
+
};
|
|
3666
|
+
|
|
3667
|
+
this.sftp.fstat(handle, tryStat);
|
|
3668
|
+
return;
|
|
3669
|
+
}
|
|
3670
|
+
|
|
3671
|
+
this.emit('open', handle);
|
|
3672
|
+
this.emit('ready');
|
|
3673
|
+
};
|
|
3674
|
+
|
|
3675
|
+
this.sftp.fchmod(handle, this.mode, tryAgain);
|
|
3676
|
+
});
|
|
3677
|
+
};
|
|
3678
|
+
|
|
3679
|
+
WriteStream.prototype._write = function(data, encoding, cb) {
|
|
3680
|
+
if (!Buffer.isBuffer(data)) {
|
|
3681
|
+
const err = new ERR_INVALID_ARG_TYPE('data', 'Buffer', data);
|
|
3682
|
+
return this.emit('error', err);
|
|
3683
|
+
}
|
|
3684
|
+
|
|
3685
|
+
if (!Buffer.isBuffer(this.handle)) {
|
|
3686
|
+
return this.once('open', function() {
|
|
3687
|
+
this._write(data, encoding, cb);
|
|
3688
|
+
});
|
|
3689
|
+
}
|
|
3690
|
+
|
|
3691
|
+
this.sftp.write(this.handle,
|
|
3692
|
+
data,
|
|
3693
|
+
0,
|
|
3694
|
+
data.length,
|
|
3695
|
+
this.pos,
|
|
3696
|
+
(er, bytes) => {
|
|
3697
|
+
if (er) {
|
|
3698
|
+
if (this.autoClose)
|
|
3699
|
+
this.destroy();
|
|
3700
|
+
return cb(er);
|
|
3701
|
+
}
|
|
3702
|
+
this.bytesWritten += bytes;
|
|
3703
|
+
cb();
|
|
3704
|
+
});
|
|
3705
|
+
|
|
3706
|
+
this.pos += data.length;
|
|
3707
|
+
};
|
|
3708
|
+
|
|
3709
|
+
WriteStream.prototype._writev = function(data, cb) {
|
|
3710
|
+
if (!Buffer.isBuffer(this.handle)) {
|
|
3711
|
+
return this.once('open', function() {
|
|
3712
|
+
this._writev(data, cb);
|
|
3713
|
+
});
|
|
3714
|
+
}
|
|
3715
|
+
|
|
3716
|
+
const sftp = this.sftp;
|
|
3717
|
+
const handle = this.handle;
|
|
3718
|
+
let writesLeft = data.length;
|
|
3719
|
+
|
|
3720
|
+
const onwrite = (er, bytes) => {
|
|
3721
|
+
if (er) {
|
|
3722
|
+
this.destroy();
|
|
3723
|
+
return cb(er);
|
|
3724
|
+
}
|
|
3725
|
+
this.bytesWritten += bytes;
|
|
3726
|
+
if (--writesLeft === 0)
|
|
3727
|
+
cb();
|
|
3728
|
+
};
|
|
3729
|
+
|
|
3730
|
+
// TODO: try to combine chunks to reduce number of requests to the server?
|
|
3731
|
+
for (let i = 0; i < data.length; ++i) {
|
|
3732
|
+
const chunk = data[i].chunk;
|
|
3733
|
+
|
|
3734
|
+
sftp.write(handle, chunk, 0, chunk.length, this.pos, onwrite);
|
|
3735
|
+
this.pos += chunk.length;
|
|
3736
|
+
}
|
|
3737
|
+
};
|
|
3738
|
+
|
|
3739
|
+
if (typeof WritableStream.prototype.destroy !== 'function')
|
|
3740
|
+
WriteStream.prototype.destroy = ReadStream.prototype.destroy;
|
|
3741
|
+
|
|
3742
|
+
WriteStream.prototype._destroy = ReadStream.prototype._destroy;
|
|
3743
|
+
WriteStream.prototype.close = function(cb) {
|
|
3744
|
+
if (cb) {
|
|
3745
|
+
if (this.closed) {
|
|
3746
|
+
process.nextTick(cb);
|
|
3747
|
+
return;
|
|
3748
|
+
}
|
|
3749
|
+
this.on('close', cb);
|
|
3750
|
+
}
|
|
3751
|
+
|
|
3752
|
+
// If we are not autoClosing, we should call
|
|
3753
|
+
// destroy on 'finish'.
|
|
3754
|
+
if (!this.autoClose)
|
|
3755
|
+
this.on('finish', this.destroy.bind(this));
|
|
3756
|
+
|
|
3757
|
+
this.end();
|
|
3758
|
+
};
|
|
3759
|
+
|
|
3760
|
+
// There is no shutdown() for files.
|
|
3761
|
+
WriteStream.prototype.destroySoon = WriteStream.prototype.end;
|
|
3762
|
+
|
|
3763
|
+
Object.defineProperty(WriteStream.prototype, 'pending', {
|
|
3764
|
+
get() {
|
|
3765
|
+
return this.handle === null;
|
|
3766
|
+
},
|
|
3767
|
+
configurable: true
|
|
3768
|
+
});
|
|
3769
|
+
// =============================================================================
|
|
3770
|
+
|
|
3771
|
+
module.exports = {
|
|
3772
|
+
flagsToString,
|
|
3773
|
+
OPEN_MODE,
|
|
3774
|
+
SFTP,
|
|
3775
|
+
Stats,
|
|
3776
|
+
STATUS_CODE,
|
|
3777
|
+
stringToFlags,
|
|
3778
|
+
};
|