SFTP.js•114 kB
'use strict';
const EventEmitter = require('events');
const fs = require('fs');
const { constants } = fs;
const {
Readable: ReadableStream,
Writable: WritableStream
} = require('stream');
const { inherits, types: { isDate } } = require('util');
const FastBuffer = Buffer[Symbol.species];
const {
bufferCopy,
bufferSlice,
makeBufferParser,
writeUInt32BE,
} = require('./utils.js');
const ATTR = {
SIZE: 0x00000001,
UIDGID: 0x00000002,
PERMISSIONS: 0x00000004,
ACMODTIME: 0x00000008,
EXTENDED: 0x80000000,
};
// Large enough to store all possible attributes
const ATTRS_BUF = Buffer.alloc(28);
const STATUS_CODE = {
OK: 0,
EOF: 1,
NO_SUCH_FILE: 2,
PERMISSION_DENIED: 3,
FAILURE: 4,
BAD_MESSAGE: 5,
NO_CONNECTION: 6,
CONNECTION_LOST: 7,
OP_UNSUPPORTED: 8
};
const VALID_STATUS_CODES = new Map(
Object.values(STATUS_CODE).map((n) => [n, 1])
);
const STATUS_CODE_STR = {
[STATUS_CODE.OK]: 'No error',
[STATUS_CODE.EOF]: 'End of file',
[STATUS_CODE.NO_SUCH_FILE]: 'No such file or directory',
[STATUS_CODE.PERMISSION_DENIED]: 'Permission denied',
[STATUS_CODE.FAILURE]: 'Failure',
[STATUS_CODE.BAD_MESSAGE]: 'Bad message',
[STATUS_CODE.NO_CONNECTION]: 'No connection',
[STATUS_CODE.CONNECTION_LOST]: 'Connection lost',
[STATUS_CODE.OP_UNSUPPORTED]: 'Operation unsupported',
};
const REQUEST = {
INIT: 1,
OPEN: 3,
CLOSE: 4,
READ: 5,
WRITE: 6,
LSTAT: 7,
FSTAT: 8,
SETSTAT: 9,
FSETSTAT: 10,
OPENDIR: 11,
READDIR: 12,
REMOVE: 13,
MKDIR: 14,
RMDIR: 15,
REALPATH: 16,
STAT: 17,
RENAME: 18,
READLINK: 19,
SYMLINK: 20,
EXTENDED: 200
};
const RESPONSE = {
VERSION: 2,
STATUS: 101,
HANDLE: 102,
DATA: 103,
NAME: 104,
ATTRS: 105,
EXTENDED: 201
};
const OPEN_MODE = {
READ: 0x00000001,
WRITE: 0x00000002,
APPEND: 0x00000004,
CREAT: 0x00000008,
TRUNC: 0x00000010,
EXCL: 0x00000020
};
const PKT_RW_OVERHEAD = 2 * 1024;
const MAX_REQID = 2 ** 32 - 1;
const CLIENT_VERSION_BUFFER = Buffer.from([
0, 0, 0, 5 /* length */,
REQUEST.INIT,
0, 0, 0, 3 /* version */
]);
const SERVER_VERSION_BUFFER = Buffer.from([
0, 0, 0, 5 /* length */,
RESPONSE.VERSION,
0, 0, 0, 3 /* version */
]);
const RE_OPENSSH = /^SSH-2.0-(?:OpenSSH|dropbear)/;
const OPENSSH_MAX_PKT_LEN = 256 * 1024;
const bufferParser = makeBufferParser();
const fakeStderr = {
readable: false,
writable: false,
push: (data) => {},
once: () => {},
on: () => {},
emit: () => {},
end: () => {},
};
function noop() {}
// Emulates enough of `Channel` to be able to be used as a drop-in replacement
// in order to process incoming data with as little overhead as possible
class SFTP extends EventEmitter {
constructor(client, chanInfo, cfg) {
super();
if (typeof cfg !== 'object' || !cfg)
cfg = {};
const remoteIdentRaw = client._protocol._remoteIdentRaw;
this.server = !!cfg.server;
this._debug = (typeof cfg.debug === 'function' ? cfg.debug : undefined);
this._isOpenSSH = (remoteIdentRaw && RE_OPENSSH.test(remoteIdentRaw));
this._version = -1;
this._extensions = {};
this._biOpt = cfg.biOpt;
this._pktLenBytes = 0;
this._pktLen = 0;
this._pktPos = 0;
this._pktType = 0;
this._pktData = undefined;
this._writeReqid = -1;
this._requests = {};
this._maxInPktLen = OPENSSH_MAX_PKT_LEN;
this._maxOutPktLen = 34000;
this._maxReadLen =
(this._isOpenSSH ? OPENSSH_MAX_PKT_LEN : 34000) - PKT_RW_OVERHEAD;
this._maxWriteLen =
(this._isOpenSSH ? OPENSSH_MAX_PKT_LEN : 34000) - PKT_RW_OVERHEAD;
this.maxOpenHandles = undefined;
// Channel compatibility
this._client = client;
this._protocol = client._protocol;
this._callbacks = [];
this._hasX11 = false;
this._exit = {
code: undefined,
signal: undefined,
dump: undefined,
desc: undefined,
};
this._waitWindow = false; // SSH-level backpressure
this._chunkcb = undefined;
this._buffer = [];
this.type = chanInfo.type;
this.subtype = undefined;
this.incoming = chanInfo.incoming;
this.outgoing = chanInfo.outgoing;
this.stderr = fakeStderr;
this.readable = true;
}
// This handles incoming data to parse
push(data) {
if (data === null) {
cleanupRequests(this);
if (!this.readable)
return;
// No more incoming data from the remote side
this.readable = false;
this.emit('end');
return;
}
/*
uint32 length
byte type
byte[length - 1] data payload
*/
let p = 0;
while (p < data.length) {
if (this._pktLenBytes < 4) {
let nb = Math.min(4 - this._pktLenBytes, data.length - p);
this._pktLenBytes += nb;
while (nb--)
this._pktLen = (this._pktLen << 8) + data[p++];
if (this._pktLenBytes < 4)
return;
if (this._pktLen === 0)
return doFatalSFTPError(this, 'Invalid packet length');
if (this._pktLen > this._maxInPktLen) {
const max = this._maxInPktLen;
return doFatalSFTPError(
this,
`Packet length ${this._pktLen} exceeds max length of ${max}`
);
}
if (p >= data.length)
return;
}
if (this._pktPos < this._pktLen) {
const nb = Math.min(this._pktLen - this._pktPos, data.length - p);
if (p !== 0 || nb !== data.length) {
if (nb === this._pktLen) {
this._pkt = new FastBuffer(data.buffer, data.byteOffset + p, nb);
} else {
if (!this._pkt)
this._pkt = Buffer.allocUnsafe(this._pktLen);
this._pkt.set(
new Uint8Array(data.buffer, data.byteOffset + p, nb),
this._pktPos
);
}
} else if (nb === this._pktLen) {
this._pkt = data;
} else {
if (!this._pkt)
this._pkt = Buffer.allocUnsafe(this._pktLen);
this._pkt.set(data, this._pktPos);
}
p += nb;
this._pktPos += nb;
if (this._pktPos < this._pktLen)
return;
}
const type = this._pkt[0];
const payload = this._pkt;
// Prepare for next packet
this._pktLen = 0;
this._pktLenBytes = 0;
this._pkt = undefined;
this._pktPos = 0;
const handler = (this.server
? SERVER_HANDLERS[type]
: CLIENT_HANDLERS[type]);
if (!handler)
return doFatalSFTPError(this, `Unknown packet type ${type}`);
if (this._version === -1) {
if (this.server) {
if (type !== REQUEST.INIT)
return doFatalSFTPError(this, `Expected INIT packet, got ${type}`);
} else if (type !== RESPONSE.VERSION) {
return doFatalSFTPError(this, `Expected VERSION packet, got ${type}`);
}
}
if (handler(this, payload) === false)
return;
}
}
end() {
this.destroy();
}
destroy() {
if (this.outgoing.state === 'open' || this.outgoing.state === 'eof') {
this.outgoing.state = 'closing';
this._protocol.channelClose(this.outgoing.id);
}
}
_init() {
this._init = noop;
if (!this.server)
sendOrBuffer(this, CLIENT_VERSION_BUFFER);
}
// ===========================================================================
// Client-specific ===========================================================
// ===========================================================================
createReadStream(path, options) {
if (this.server)
throw new Error('Client-only method called in server mode');
return new ReadStream(this, path, options);
}
createWriteStream(path, options) {
if (this.server)
throw new Error('Client-only method called in server mode');
return new WriteStream(this, path, options);
}
open(path, flags_, attrs, cb) {
if (this.server)
throw new Error('Client-only method called in server mode');
if (typeof attrs === 'function') {
cb = attrs;
attrs = undefined;
}
const flags = (typeof flags_ === 'number' ? flags_ : stringToFlags(flags_));
if (flags === null)
throw new Error(`Unknown flags string: ${flags_}`);
let attrsFlags = 0;
let attrsLen = 0;
if (typeof attrs === 'string' || typeof attrs === 'number')
attrs = { mode: attrs };
if (typeof attrs === 'object' && attrs !== null) {
attrs = attrsToBytes(attrs);
attrsFlags = attrs.flags;
attrsLen = attrs.nb;
}
/*
uint32 id
string filename
uint32 pflags
ATTRS attrs
*/
const pathLen = Buffer.byteLength(path);
let p = 9;
const buf = Buffer.allocUnsafe(4 + 1 + 4 + 4 + pathLen + 4 + 4 + attrsLen);
writeUInt32BE(buf, buf.length - 4, 0);
buf[4] = REQUEST.OPEN;
const reqid = this._writeReqid = (this._writeReqid + 1) & MAX_REQID;
writeUInt32BE(buf, reqid, 5);
writeUInt32BE(buf, pathLen, p);
buf.utf8Write(path, p += 4, pathLen);
writeUInt32BE(buf, flags, p += pathLen);
writeUInt32BE(buf, attrsFlags, p += 4);
if (attrsLen) {
p += 4;
if (attrsLen === ATTRS_BUF.length)
buf.set(ATTRS_BUF, p);
else
bufferCopy(ATTRS_BUF, buf, 0, attrsLen, p);
p += attrsLen;
}
this._requests[reqid] = { cb };
const isBuffered = sendOrBuffer(this, buf);
this._debug && this._debug(
`SFTP: Outbound: ${isBuffered ? 'Buffered' : 'Sending'} OPEN`
);
}
close(handle, cb) {
if (this.server)
throw new Error('Client-only method called in server mode');
if (!Buffer.isBuffer(handle))
throw new Error('handle is not a Buffer');
/*
uint32 id
string handle
*/
const handleLen = handle.length;
let p = 9;
const buf = Buffer.allocUnsafe(4 + 1 + 4 + 4 + handleLen);
writeUInt32BE(buf, buf.length - 4, 0);
buf[4] = REQUEST.CLOSE;
const reqid = this._writeReqid = (this._writeReqid + 1) & MAX_REQID;
writeUInt32BE(buf, reqid, 5);
writeUInt32BE(buf, handleLen, p);
buf.set(handle, p += 4);
this._requests[reqid] = { cb };
const isBuffered = sendOrBuffer(this, buf);
this._debug && this._debug(
`SFTP: Outbound: ${isBuffered ? 'Buffered' : 'Sending'} CLOSE`
);
}
read(handle, buf, off, len, position, cb) {
if (this.server)
throw new Error('Client-only method called in server mode');
if (!Buffer.isBuffer(handle))
throw new Error('handle is not a Buffer');
if (!Buffer.isBuffer(buf))
throw new Error('buffer is not a Buffer');
if (off >= buf.length)
throw new Error('offset is out of bounds');
if (off + len > buf.length)
throw new Error('length extends beyond buffer');
if (position === null)
throw new Error('null position currently unsupported');
read_(this, handle, buf, off, len, position, cb);
}
readData(handle, buf, off, len, position, cb) {
// Backwards compatibility
this.read(handle, buf, off, len, position, cb);
}
write(handle, buf, off, len, position, cb) {
if (this.server)
throw new Error('Client-only method called in server mode');
if (!Buffer.isBuffer(handle))
throw new Error('handle is not a Buffer');
if (!Buffer.isBuffer(buf))
throw new Error('buffer is not a Buffer');
if (off > buf.length)
throw new Error('offset is out of bounds');
if (off + len > buf.length)
throw new Error('length extends beyond buffer');
if (position === null)
throw new Error('null position currently unsupported');
if (!len) {
cb && process.nextTick(cb, undefined, 0);
return;
}
const maxDataLen = this._maxWriteLen;
const overflow = Math.max(len - maxDataLen, 0);
const origPosition = position;
if (overflow)
len = maxDataLen;
/*
uint32 id
string handle
uint64 offset
string data
*/
const handleLen = handle.length;
let p = 9;
const out = Buffer.allocUnsafe(4 + 1 + 4 + 4 + handleLen + 8 + 4 + len);
writeUInt32BE(out, out.length - 4, 0);
out[4] = REQUEST.WRITE;
const reqid = this._writeReqid = (this._writeReqid + 1) & MAX_REQID;
writeUInt32BE(out, reqid, 5);
writeUInt32BE(out, handleLen, p);
out.set(handle, p += 4);
p += handleLen;
for (let i = 7; i >= 0; --i) {
out[p + i] = position & 0xFF;
position /= 256;
}
writeUInt32BE(out, len, p += 8);
bufferCopy(buf, out, off, off + len, p += 4);
this._requests[reqid] = {
cb: (err) => {
if (err) {
if (typeof cb === 'function')
cb(err);
} else if (overflow) {
this.write(handle,
buf,
off + len,
overflow,
origPosition + len,
cb);
} else if (typeof cb === 'function') {
cb(undefined, off + len);
}
}
};
const isSent = sendOrBuffer(this, out);
if (this._debug) {
const how = (isSent ? 'Sent' : 'Buffered');
this._debug(`SFTP: Outbound: ${how} WRITE (id:${reqid})`);
}
}
writeData(handle, buf, off, len, position, cb) {
// Backwards compatibility
this.write(handle, buf, off, len, position, cb);
}
fastGet(remotePath, localPath, opts, cb) {
if (this.server)
throw new Error('Client-only method called in server mode');
fastXfer(this, fs, remotePath, localPath, opts, cb);
}
fastPut(localPath, remotePath, opts, cb) {
if (this.server)
throw new Error('Client-only method called in server mode');
fastXfer(fs, this, localPath, remotePath, opts, cb);
}
readFile(path, options, callback_) {
if (this.server)
throw new Error('Client-only method called in server mode');
let callback;
if (typeof callback_ === 'function') {
callback = callback_;
} else if (typeof options === 'function') {
callback = options;
options = undefined;
}
if (typeof options === 'string')
options = { encoding: options, flag: 'r' };
else if (!options)
options = { encoding: null, flag: 'r' };
else if (typeof options !== 'object')
throw new TypeError('Bad arguments');
const encoding = options.encoding;
if (encoding && !Buffer.isEncoding(encoding))
throw new Error(`Unknown encoding: ${encoding}`);
// First stat the file, so we know the size.
let size;
let buffer; // Single buffer with file data
let buffers; // List for when size is unknown
let pos = 0;
let handle;
// SFTPv3 does not support using -1 for read position, so we have to track
// read position manually
let bytesRead = 0;
const flag = options.flag || 'r';
const read = () => {
if (size === 0) {
buffer = Buffer.allocUnsafe(8192);
this.read(handle, buffer, 0, 8192, bytesRead, afterRead);
} else {
this.read(handle, buffer, pos, size - pos, bytesRead, afterRead);
}
};
const afterRead = (er, nbytes) => {
let eof;
if (er) {
eof = (er.code === STATUS_CODE.EOF);
if (!eof) {
return this.close(handle, () => {
return callback && callback(er);
});
}
} else {
eof = false;
}
if (eof || (size === 0 && nbytes === 0))
return close();
bytesRead += nbytes;
pos += nbytes;
if (size !== 0) {
if (pos === size)
close();
else
read();
} else {
// Unknown size, just read until we don't get bytes.
buffers.push(bufferSlice(buffer, 0, nbytes));
read();
}
};
afterRead._wantEOFError = true;
const close = () => {
this.close(handle, (er) => {
if (size === 0) {
// Collect the data into the buffers list.
buffer = Buffer.concat(buffers, pos);
} else if (pos < size) {
buffer = bufferSlice(buffer, 0, pos);
}
if (encoding)
buffer = buffer.toString(encoding);
return callback && callback(er, buffer);
});
};
this.open(path, flag, 0o666, (er, handle_) => {
if (er)
return callback && callback(er);
handle = handle_;
const tryStat = (er, st) => {
if (er) {
// Try stat() for sftp servers that may not support fstat() for
// whatever reason
this.stat(path, (er_, st_) => {
if (er_) {
return this.close(handle, () => {
callback && callback(er);
});
}
tryStat(null, st_);
});
return;
}
size = st.size || 0;
if (size === 0) {
// The kernel lies about many files.
// Go ahead and try to read some bytes.
buffers = [];
return read();
}
buffer = Buffer.allocUnsafe(size);
read();
};
this.fstat(handle, tryStat);
});
}
writeFile(path, data, options, callback_) {
if (this.server)
throw new Error('Client-only method called in server mode');
let callback;
if (typeof callback_ === 'function') {
callback = callback_;
} else if (typeof options === 'function') {
callback = options;
options = undefined;
}
if (typeof options === 'string')
options = { encoding: options, mode: 0o666, flag: 'w' };
else if (!options)
options = { encoding: 'utf8', mode: 0o666, flag: 'w' };
else if (typeof options !== 'object')
throw new TypeError('Bad arguments');
if (options.encoding && !Buffer.isEncoding(options.encoding))
throw new Error(`Unknown encoding: ${options.encoding}`);
const flag = options.flag || 'w';
this.open(path, flag, options.mode, (openErr, handle) => {
if (openErr) {
callback && callback(openErr);
} else {
const buffer = (Buffer.isBuffer(data)
? data
: Buffer.from('' + data, options.encoding || 'utf8'));
const position = (/a/.test(flag) ? null : 0);
// SFTPv3 does not support the notion of 'current position'
// (null position), so we just attempt to append to the end of the file
// instead
if (position === null) {
const tryStat = (er, st) => {
if (er) {
// Try stat() for sftp servers that may not support fstat() for
// whatever reason
this.stat(path, (er_, st_) => {
if (er_) {
return this.close(handle, () => {
callback && callback(er);
});
}
tryStat(null, st_);
});
return;
}
writeAll(this, handle, buffer, 0, buffer.length, st.size, callback);
};
this.fstat(handle, tryStat);
return;
}
writeAll(this, handle, buffer, 0, buffer.length, position, callback);
}
});
}
appendFile(path, data, options, callback_) {
if (this.server)
throw new Error('Client-only method called in server mode');
let callback;
if (typeof callback_ === 'function') {
callback = callback_;
} else if (typeof options === 'function') {
callback = options;
options = undefined;
}
if (typeof options === 'string')
options = { encoding: options, mode: 0o666, flag: 'a' };
else if (!options)
options = { encoding: 'utf8', mode: 0o666, flag: 'a' };
else if (typeof options !== 'object')
throw new TypeError('Bad arguments');
if (!options.flag)
options = Object.assign({ flag: 'a' }, options);
this.writeFile(path, data, options, callback);
}
exists(path, cb) {
if (this.server)
throw new Error('Client-only method called in server mode');
this.stat(path, (err) => {
cb && cb(err ? false : true);
});
}
unlink(filename, cb) {
if (this.server)
throw new Error('Client-only method called in server mode');
/*
uint32 id
string filename
*/
const fnameLen = Buffer.byteLength(filename);
let p = 9;
const buf = Buffer.allocUnsafe(4 + 1 + 4 + 4 + fnameLen);
writeUInt32BE(buf, buf.length - 4, 0);
buf[4] = REQUEST.REMOVE;
const reqid = this._writeReqid = (this._writeReqid + 1) & MAX_REQID;
writeUInt32BE(buf, reqid, 5);
writeUInt32BE(buf, fnameLen, p);
buf.utf8Write(filename, p += 4, fnameLen);
this._requests[reqid] = { cb };
const isBuffered = sendOrBuffer(this, buf);
this._debug && this._debug(
`SFTP: Outbound: ${isBuffered ? 'Buffered' : 'Sending'} REMOVE`
);
}
rename(oldPath, newPath, cb) {
if (this.server)
throw new Error('Client-only method called in server mode');
/*
uint32 id
string oldpath
string newpath
*/
const oldLen = Buffer.byteLength(oldPath);
const newLen = Buffer.byteLength(newPath);
let p = 9;
const buf = Buffer.allocUnsafe(4 + 1 + 4 + 4 + oldLen + 4 + newLen);
writeUInt32BE(buf, buf.length - 4, 0);
buf[4] = REQUEST.RENAME;
const reqid = this._writeReqid = (this._writeReqid + 1) & MAX_REQID;
writeUInt32BE(buf, reqid, 5);
writeUInt32BE(buf, oldLen, p);
buf.utf8Write(oldPath, p += 4, oldLen);
writeUInt32BE(buf, newLen, p += oldLen);
buf.utf8Write(newPath, p += 4, newLen);
this._requests[reqid] = { cb };
const isBuffered = sendOrBuffer(this, buf);
this._debug && this._debug(
`SFTP: Outbound: ${isBuffered ? 'Buffered' : 'Sending'} RENAME`
);
}
mkdir(path, attrs, cb) {
if (this.server)
throw new Error('Client-only method called in server mode');
let flags = 0;
let attrsLen = 0;
if (typeof attrs === 'function') {
cb = attrs;
attrs = undefined;
}
if (typeof attrs === 'object' && attrs !== null) {
attrs = attrsToBytes(attrs);
flags = attrs.flags;
attrsLen = attrs.nb;
}
/*
uint32 id
string path
ATTRS attrs
*/
const pathLen = Buffer.byteLength(path);
let p = 9;
const buf = Buffer.allocUnsafe(4 + 1 + 4 + 4 + pathLen + 4 + attrsLen);
writeUInt32BE(buf, buf.length - 4, 0);
buf[4] = REQUEST.MKDIR;
const reqid = this._writeReqid = (this._writeReqid + 1) & MAX_REQID;
writeUInt32BE(buf, reqid, 5);
writeUInt32BE(buf, pathLen, p);
buf.utf8Write(path, p += 4, pathLen);
writeUInt32BE(buf, flags, p += pathLen);
if (attrsLen) {
p += 4;
if (attrsLen === ATTRS_BUF.length)
buf.set(ATTRS_BUF, p);
else
bufferCopy(ATTRS_BUF, buf, 0, attrsLen, p);
p += attrsLen;
}
this._requests[reqid] = { cb };
const isBuffered = sendOrBuffer(this, buf);
this._debug && this._debug(
`SFTP: Outbound: ${isBuffered ? 'Buffered' : 'Sending'} MKDIR`
);
}
rmdir(path, cb) {
if (this.server)
throw new Error('Client-only method called in server mode');
/*
uint32 id
string path
*/
const pathLen = Buffer.byteLength(path);
let p = 9;
const buf = Buffer.allocUnsafe(4 + 1 + 4 + 4 + pathLen);
writeUInt32BE(buf, buf.length - 4, 0);
buf[4] = REQUEST.RMDIR;
const reqid = this._writeReqid = (this._writeReqid + 1) & MAX_REQID;
writeUInt32BE(buf, reqid, 5);
writeUInt32BE(buf, pathLen, p);
buf.utf8Write(path, p += 4, pathLen);
this._requests[reqid] = { cb };
const isBuffered = sendOrBuffer(this, buf);
this._debug && this._debug(
`SFTP: Outbound: ${isBuffered ? 'Buffered' : 'Sending'} RMDIR`
);
}
readdir(where, opts, cb) {
if (this.server)
throw new Error('Client-only method called in server mode');
if (typeof opts === 'function') {
cb = opts;
opts = {};
}
if (typeof opts !== 'object' || opts === null)
opts = {};
const doFilter = (opts && opts.full ? false : true);
if (!Buffer.isBuffer(where) && typeof where !== 'string')
throw new Error('missing directory handle or path');
if (typeof where === 'string') {
const entries = [];
let e = 0;
const reread = (err, handle) => {
if (err)
return cb(err);
this.readdir(handle, opts, (err, list) => {
const eof = (err && err.code === STATUS_CODE.EOF);
if (err && !eof)
return this.close(handle, () => cb(err));
if (eof) {
return this.close(handle, (err) => {
if (err)
return cb(err);
cb(undefined, entries);
});
}
for (let i = 0; i < list.length; ++i, ++e)
entries[e] = list[i];
reread(undefined, handle);
});
};
return this.opendir(where, reread);
}
/*
uint32 id
string handle
*/
const handleLen = where.length;
let p = 9;
const buf = Buffer.allocUnsafe(4 + 1 + 4 + 4 + handleLen);
writeUInt32BE(buf, buf.length - 4, 0);
buf[4] = REQUEST.READDIR;
const reqid = this._writeReqid = (this._writeReqid + 1) & MAX_REQID;
writeUInt32BE(buf, reqid, 5);
writeUInt32BE(buf, handleLen, p);
buf.set(where, p += 4);
this._requests[reqid] = {
cb: (doFilter
? (err, list) => {
if (typeof cb !== 'function')
return;
if (err)
return cb(err);
for (let i = list.length - 1; i >= 0; --i) {
if (list[i].filename === '.' || list[i].filename === '..')
list.splice(i, 1);
}
cb(undefined, list);
}
: cb)
};
const isBuffered = sendOrBuffer(this, buf);
this._debug && this._debug(
`SFTP: Outbound: ${isBuffered ? 'Buffered' : 'Sending'} READDIR`
);
}
fstat(handle, cb) {
if (this.server)
throw new Error('Client-only method called in server mode');
if (!Buffer.isBuffer(handle))
throw new Error('handle is not a Buffer');
/*
uint32 id
string handle
*/
const handleLen = handle.length;
let p = 9;
const buf = Buffer.allocUnsafe(4 + 1 + 4 + 4 + handleLen);
writeUInt32BE(buf, buf.length - 4, 0);
buf[4] = REQUEST.FSTAT;
const reqid = this._writeReqid = (this._writeReqid + 1) & MAX_REQID;
writeUInt32BE(buf, reqid, 5);
writeUInt32BE(buf, handleLen, p);
buf.set(handle, p += 4);
this._requests[reqid] = { cb };
const isBuffered = sendOrBuffer(this, buf);
this._debug && this._debug(
`SFTP: Outbound: ${isBuffered ? 'Buffered' : 'Sending'} FSTAT`
);
}
stat(path, cb) {
if (this.server)
throw new Error('Client-only method called in server mode');
/*
uint32 id
string path
*/
const pathLen = Buffer.byteLength(path);
let p = 9;
const buf = Buffer.allocUnsafe(4 + 1 + 4 + 4 + pathLen);
writeUInt32BE(buf, buf.length - 4, 0);
buf[4] = REQUEST.STAT;
const reqid = this._writeReqid = (this._writeReqid + 1) & MAX_REQID;
writeUInt32BE(buf, reqid, 5);
writeUInt32BE(buf, pathLen, p);
buf.utf8Write(path, p += 4, pathLen);
this._requests[reqid] = { cb };
const isBuffered = sendOrBuffer(this, buf);
this._debug && this._debug(
`SFTP: Outbound: ${isBuffered ? 'Buffered' : 'Sending'} STAT`
);
}
lstat(path, cb) {
if (this.server)
throw new Error('Client-only method called in server mode');
/*
uint32 id
string path
*/
const pathLen = Buffer.byteLength(path);
let p = 9;
const buf = Buffer.allocUnsafe(4 + 1 + 4 + 4 + pathLen);
writeUInt32BE(buf, buf.length - 4, 0);
buf[4] = REQUEST.LSTAT;
const reqid = this._writeReqid = (this._writeReqid + 1) & MAX_REQID;
writeUInt32BE(buf, reqid, 5);
writeUInt32BE(buf, pathLen, p);
buf.utf8Write(path, p += 4, pathLen);
this._requests[reqid] = { cb };
const isBuffered = sendOrBuffer(this, buf);
this._debug && this._debug(
`SFTP: Outbound: ${isBuffered ? 'Buffered' : 'Sending'} LSTAT`
);
}
opendir(path, cb) {
if (this.server)
throw new Error('Client-only method called in server mode');
/*
uint32 id
string path
*/
const pathLen = Buffer.byteLength(path);
let p = 9;
const buf = Buffer.allocUnsafe(4 + 1 + 4 + 4 + pathLen);
writeUInt32BE(buf, buf.length - 4, 0);
buf[4] = REQUEST.OPENDIR;
const reqid = this._writeReqid = (this._writeReqid + 1) & MAX_REQID;
writeUInt32BE(buf, reqid, 5);
writeUInt32BE(buf, pathLen, p);
buf.utf8Write(path, p += 4, pathLen);
this._requests[reqid] = { cb };
const isBuffered = sendOrBuffer(this, buf);
this._debug && this._debug(
`SFTP: Outbound: ${isBuffered ? 'Buffered' : 'Sending'} OPENDIR`
);
}
setstat(path, attrs, cb) {
if (this.server)
throw new Error('Client-only method called in server mode');
let flags = 0;
let attrsLen = 0;
if (typeof attrs === 'object' && attrs !== null) {
attrs = attrsToBytes(attrs);
flags = attrs.flags;
attrsLen = attrs.nb;
} else if (typeof attrs === 'function') {
cb = attrs;
}
/*
uint32 id
string path
ATTRS attrs
*/
const pathLen = Buffer.byteLength(path);
let p = 9;
const buf = Buffer.allocUnsafe(4 + 1 + 4 + 4 + pathLen + 4 + attrsLen);
writeUInt32BE(buf, buf.length - 4, 0);
buf[4] = REQUEST.SETSTAT;
const reqid = this._writeReqid = (this._writeReqid + 1) & MAX_REQID;
writeUInt32BE(buf, reqid, 5);
writeUInt32BE(buf, pathLen, p);
buf.utf8Write(path, p += 4, pathLen);
writeUInt32BE(buf, flags, p += pathLen);
if (attrsLen) {
p += 4;
if (attrsLen === ATTRS_BUF.length)
buf.set(ATTRS_BUF, p);
else
bufferCopy(ATTRS_BUF, buf, 0, attrsLen, p);
p += attrsLen;
}
this._requests[reqid] = { cb };
const isBuffered = sendOrBuffer(this, buf);
this._debug && this._debug(
`SFTP: Outbound: ${isBuffered ? 'Buffered' : 'Sending'} SETSTAT`
);
}
fsetstat(handle, attrs, cb) {
if (this.server)
throw new Error('Client-only method called in server mode');
if (!Buffer.isBuffer(handle))
throw new Error('handle is not a Buffer');
let flags = 0;
let attrsLen = 0;
if (typeof attrs === 'object' && attrs !== null) {
attrs = attrsToBytes(attrs);
flags = attrs.flags;
attrsLen = attrs.nb;
} else if (typeof attrs === 'function') {
cb = attrs;
}
/*
uint32 id
string handle
ATTRS attrs
*/
const handleLen = handle.length;
let p = 9;
const buf = Buffer.allocUnsafe(4 + 1 + 4 + 4 + handleLen + 4 + attrsLen);
writeUInt32BE(buf, buf.length - 4, 0);
buf[4] = REQUEST.FSETSTAT;
const reqid = this._writeReqid = (this._writeReqid + 1) & MAX_REQID;
writeUInt32BE(buf, reqid, 5);
writeUInt32BE(buf, handleLen, p);
buf.set(handle, p += 4);
writeUInt32BE(buf, flags, p += handleLen);
if (attrsLen) {
p += 4;
if (attrsLen === ATTRS_BUF.length)
buf.set(ATTRS_BUF, p);
else
bufferCopy(ATTRS_BUF, buf, 0, attrsLen, p);
p += attrsLen;
}
this._requests[reqid] = { cb };
const isBuffered = sendOrBuffer(this, buf);
this._debug && this._debug(
`SFTP: Outbound: ${isBuffered ? 'Buffered' : 'Sending'} FSETSTAT`
);
}
futimes(handle, atime, mtime, cb) {
return this.fsetstat(handle, {
atime: toUnixTimestamp(atime),
mtime: toUnixTimestamp(mtime)
}, cb);
}
utimes(path, atime, mtime, cb) {
return this.setstat(path, {
atime: toUnixTimestamp(atime),
mtime: toUnixTimestamp(mtime)
}, cb);
}
fchown(handle, uid, gid, cb) {
return this.fsetstat(handle, {
uid: uid,
gid: gid
}, cb);
}
chown(path, uid, gid, cb) {
return this.setstat(path, {
uid: uid,
gid: gid
}, cb);
}
fchmod(handle, mode, cb) {
return this.fsetstat(handle, {
mode: mode
}, cb);
}
chmod(path, mode, cb) {
return this.setstat(path, {
mode: mode
}, cb);
}
readlink(path, cb) {
if (this.server)
throw new Error('Client-only method called in server mode');
/*
uint32 id
string path
*/
const pathLen = Buffer.byteLength(path);
let p = 9;
const buf = Buffer.allocUnsafe(4 + 1 + 4 + 4 + pathLen);
writeUInt32BE(buf, buf.length - 4, 0);
buf[4] = REQUEST.READLINK;
const reqid = this._writeReqid = (this._writeReqid + 1) & MAX_REQID;
writeUInt32BE(buf, reqid, 5);
writeUInt32BE(buf, pathLen, p);
buf.utf8Write(path, p += 4, pathLen);
this._requests[reqid] = {
cb: (err, names) => {
if (typeof cb !== 'function')
return;
if (err)
return cb(err);
if (!names || !names.length)
return cb(new Error('Response missing link info'));
cb(undefined, names[0].filename);
}
};
const isBuffered = sendOrBuffer(this, buf);
this._debug && this._debug(
`SFTP: Outbound: ${isBuffered ? 'Buffered' : 'Sending'} READLINK`
);
}
symlink(targetPath, linkPath, cb) {
if (this.server)
throw new Error('Client-only method called in server mode');
/*
uint32 id
string linkpath
string targetpath
*/
const linkLen = Buffer.byteLength(linkPath);
const targetLen = Buffer.byteLength(targetPath);
let p = 9;
const buf = Buffer.allocUnsafe(4 + 1 + 4 + 4 + linkLen + 4 + targetLen);
writeUInt32BE(buf, buf.length - 4, 0);
buf[4] = REQUEST.SYMLINK;
const reqid = this._writeReqid = (this._writeReqid + 1) & MAX_REQID;
writeUInt32BE(buf, reqid, 5);
if (this._isOpenSSH) {
// OpenSSH has linkpath and targetpath positions switched
writeUInt32BE(buf, targetLen, p);
buf.utf8Write(targetPath, p += 4, targetLen);
writeUInt32BE(buf, linkLen, p += targetLen);
buf.utf8Write(linkPath, p += 4, linkLen);
} else {
writeUInt32BE(buf, linkLen, p);
buf.utf8Write(linkPath, p += 4, linkLen);
writeUInt32BE(buf, targetLen, p += linkLen);
buf.utf8Write(targetPath, p += 4, targetLen);
}
this._requests[reqid] = { cb };
const isBuffered = sendOrBuffer(this, buf);
this._debug && this._debug(
`SFTP: Outbound: ${isBuffered ? 'Buffered' : 'Sending'} SYMLINK`
);
}
realpath(path, cb) {
if (this.server)
throw new Error('Client-only method called in server mode');
/*
uint32 id
string path
*/
const pathLen = Buffer.byteLength(path);
let p = 9;
const buf = Buffer.allocUnsafe(4 + 1 + 4 + 4 + pathLen);
writeUInt32BE(buf, buf.length - 4, 0);
buf[4] = REQUEST.REALPATH;
const reqid = this._writeReqid = (this._writeReqid + 1) & MAX_REQID;
writeUInt32BE(buf, reqid, 5);
writeUInt32BE(buf, pathLen, p);
buf.utf8Write(path, p += 4, pathLen);
this._requests[reqid] = {
cb: (err, names) => {
if (typeof cb !== 'function')
return;
if (err)
return cb(err);
if (!names || !names.length)
return cb(new Error('Response missing path info'));
cb(undefined, names[0].filename);
}
};
const isBuffered = sendOrBuffer(this, buf);
this._debug && this._debug(
`SFTP: Outbound: ${isBuffered ? 'Buffered' : 'Sending'} REALPATH`
);
}
// extended requests
ext_openssh_rename(oldPath, newPath, cb) {
if (this.server)
throw new Error('Client-only method called in server mode');
const ext = this._extensions['posix-rename@openssh.com'];
if (!ext || ext !== '1')
throw new Error('Server does not support this extended request');
/*
uint32 id
string "posix-rename@openssh.com"
string oldpath
string newpath
*/
const oldLen = Buffer.byteLength(oldPath);
const newLen = Buffer.byteLength(newPath);
let p = 9;
const buf =
Buffer.allocUnsafe(4 + 1 + 4 + 4 + 24 + 4 + oldLen + 4 + newLen);
writeUInt32BE(buf, buf.length - 4, 0);
buf[4] = REQUEST.EXTENDED;
const reqid = this._writeReqid = (this._writeReqid + 1) & MAX_REQID;
writeUInt32BE(buf, reqid, 5);
writeUInt32BE(buf, 24, p);
buf.utf8Write('posix-rename@openssh.com', p += 4, 24);
writeUInt32BE(buf, oldLen, p += 24);
buf.utf8Write(oldPath, p += 4, oldLen);
writeUInt32BE(buf, newLen, p += oldLen);
buf.utf8Write(newPath, p += 4, newLen);
this._requests[reqid] = { cb };
const isBuffered = sendOrBuffer(this, buf);
if (this._debug) {
const which = (isBuffered ? 'Buffered' : 'Sending');
this._debug(`SFTP: Outbound: ${which} posix-rename@openssh.com`);
}
}
ext_openssh_statvfs(path, cb) {
if (this.server)
throw new Error('Client-only method called in server mode');
const ext = this._extensions['statvfs@openssh.com'];
if (!ext || ext !== '2')
throw new Error('Server does not support this extended request');
/*
uint32 id
string "statvfs@openssh.com"
string path
*/
const pathLen = Buffer.byteLength(path);
let p = 9;
const buf = Buffer.allocUnsafe(4 + 1 + 4 + 4 + 19 + 4 + pathLen);
writeUInt32BE(buf, buf.length - 4, 0);
buf[4] = REQUEST.EXTENDED;
const reqid = this._writeReqid = (this._writeReqid + 1) & MAX_REQID;
writeUInt32BE(buf, reqid, 5);
writeUInt32BE(buf, 19, p);
buf.utf8Write('statvfs@openssh.com', p += 4, 19);
writeUInt32BE(buf, pathLen, p += 19);
buf.utf8Write(path, p += 4, pathLen);
this._requests[reqid] = { extended: 'statvfs@openssh.com', cb };
const isBuffered = sendOrBuffer(this, buf);
if (this._debug) {
const which = (isBuffered ? 'Buffered' : 'Sending');
this._debug(`SFTP: Outbound: ${which} statvfs@openssh.com`);
}
}
ext_openssh_fstatvfs(handle, cb) {
if (this.server)
throw new Error('Client-only method called in server mode');
const ext = this._extensions['fstatvfs@openssh.com'];
if (!ext || ext !== '2')
throw new Error('Server does not support this extended request');
if (!Buffer.isBuffer(handle))
throw new Error('handle is not a Buffer');
/*
uint32 id
string "fstatvfs@openssh.com"
string handle
*/
const handleLen = handle.length;
let p = 9;
const buf = Buffer.allocUnsafe(4 + 1 + 4 + 4 + 20 + 4 + handleLen);
writeUInt32BE(buf, buf.length - 4, 0);
buf[4] = REQUEST.EXTENDED;
const reqid = this._writeReqid = (this._writeReqid + 1) & MAX_REQID;
writeUInt32BE(buf, reqid, 5);
writeUInt32BE(buf, 20, p);
buf.utf8Write('fstatvfs@openssh.com', p += 4, 20);
writeUInt32BE(buf, handleLen, p += 20);
buf.set(handle, p += 4);
this._requests[reqid] = { extended: 'fstatvfs@openssh.com', cb };
const isBuffered = sendOrBuffer(this, buf);
if (this._debug) {
const which = (isBuffered ? 'Buffered' : 'Sending');
this._debug(`SFTP: Outbound: ${which} fstatvfs@openssh.com`);
}
}
ext_openssh_hardlink(oldPath, newPath, cb) {
if (this.server)
throw new Error('Client-only method called in server mode');
const ext = this._extensions['hardlink@openssh.com'];
if (ext !== '1')
throw new Error('Server does not support this extended request');
/*
uint32 id
string "hardlink@openssh.com"
string oldpath
string newpath
*/
const oldLen = Buffer.byteLength(oldPath);
const newLen = Buffer.byteLength(newPath);
let p = 9;
const buf =
Buffer.allocUnsafe(4 + 1 + 4 + 4 + 20 + 4 + oldLen + 4 + newLen);
writeUInt32BE(buf, buf.length - 4, 0);
buf[4] = REQUEST.EXTENDED;
const reqid = this._writeReqid = (this._writeReqid + 1) & MAX_REQID;
writeUInt32BE(buf, reqid, 5);
writeUInt32BE(buf, 20, p);
buf.utf8Write('hardlink@openssh.com', p += 4, 20);
writeUInt32BE(buf, oldLen, p += 20);
buf.utf8Write(oldPath, p += 4, oldLen);
writeUInt32BE(buf, newLen, p += oldLen);
buf.utf8Write(newPath, p += 4, newLen);
this._requests[reqid] = { cb };
const isBuffered = sendOrBuffer(this, buf);
if (this._debug) {
const which = (isBuffered ? 'Buffered' : 'Sending');
this._debug(`SFTP: Outbound: ${which} hardlink@openssh.com`);
}
}
ext_openssh_fsync(handle, cb) {
if (this.server)
throw new Error('Client-only method called in server mode');
const ext = this._extensions['fsync@openssh.com'];
if (ext !== '1')
throw new Error('Server does not support this extended request');
if (!Buffer.isBuffer(handle))
throw new Error('handle is not a Buffer');
/*
uint32 id
string "fsync@openssh.com"
string handle
*/
const handleLen = handle.length;
let p = 9;
const buf = Buffer.allocUnsafe(4 + 1 + 4 + 4 + 17 + 4 + handleLen);
writeUInt32BE(buf, buf.length - 4, 0);
buf[4] = REQUEST.EXTENDED;
const reqid = this._writeReqid = (this._writeReqid + 1) & MAX_REQID;
writeUInt32BE(buf, reqid, 5);
writeUInt32BE(buf, 17, p);
buf.utf8Write('fsync@openssh.com', p += 4, 17);
writeUInt32BE(buf, handleLen, p += 17);
buf.set(handle, p += 4);
this._requests[reqid] = { cb };
const isBuffered = sendOrBuffer(this, buf);
this._debug && this._debug(
`SFTP: Outbound: ${isBuffered ? 'Buffered' : 'Sending'} fsync@openssh.com`
);
}
ext_openssh_lsetstat(path, attrs, cb) {
if (this.server)
throw new Error('Client-only method called in server mode');
const ext = this._extensions['lsetstat@openssh.com'];
if (ext !== '1')
throw new Error('Server does not support this extended request');
let flags = 0;
let attrsLen = 0;
if (typeof attrs === 'object' && attrs !== null) {
attrs = attrsToBytes(attrs);
flags = attrs.flags;
attrsLen = attrs.nb;
} else if (typeof attrs === 'function') {
cb = attrs;
}
/*
uint32 id
string "lsetstat@openssh.com"
string path
ATTRS attrs
*/
const pathLen = Buffer.byteLength(path);
let p = 9;
const buf =
Buffer.allocUnsafe(4 + 1 + 4 + 4 + 20 + 4 + pathLen + 4 + attrsLen);
writeUInt32BE(buf, buf.length - 4, 0);
buf[4] = REQUEST.EXTENDED;
const reqid = this._writeReqid = (this._writeReqid + 1) & MAX_REQID;
writeUInt32BE(buf, reqid, 5);
writeUInt32BE(buf, 20, p);
buf.utf8Write('lsetstat@openssh.com', p += 4, 20);
writeUInt32BE(buf, pathLen, p += 20);
buf.utf8Write(path, p += 4, pathLen);
writeUInt32BE(buf, flags, p += pathLen);
if (attrsLen) {
p += 4;
if (attrsLen === ATTRS_BUF.length)
buf.set(ATTRS_BUF, p);
else
bufferCopy(ATTRS_BUF, buf, 0, attrsLen, p);
p += attrsLen;
}
this._requests[reqid] = { cb };
const isBuffered = sendOrBuffer(this, buf);
if (this._debug) {
const status = (isBuffered ? 'Buffered' : 'Sending');
this._debug(`SFTP: Outbound: ${status} lsetstat@openssh.com`);
}
}
ext_openssh_expandPath(path, cb) {
if (this.server)
throw new Error('Client-only method called in server mode');
const ext = this._extensions['expand-path@openssh.com'];
if (ext !== '1')
throw new Error('Server does not support this extended request');
/*
uint32 id
string "expand-path@openssh.com"
string path
*/
const pathLen = Buffer.byteLength(path);
let p = 9;
const buf = Buffer.allocUnsafe(4 + 1 + 4 + 4 + 23 + 4 + pathLen);
writeUInt32BE(buf, buf.length - 4, 0);
buf[4] = REQUEST.EXTENDED;
const reqid = this._writeReqid = (this._writeReqid + 1) & MAX_REQID;
writeUInt32BE(buf, reqid, 5);
writeUInt32BE(buf, 23, p);
buf.utf8Write('expand-path@openssh.com', p += 4, 23);
writeUInt32BE(buf, pathLen, p += 20);
buf.utf8Write(path, p += 4, pathLen);
this._requests[reqid] = {
cb: (err, names) => {
if (typeof cb !== 'function')
return;
if (err)
return cb(err);
if (!names || !names.length)
return cb(new Error('Response missing expanded path'));
cb(undefined, names[0].filename);
}
};
const isBuffered = sendOrBuffer(this, buf);
if (this._debug) {
const status = (isBuffered ? 'Buffered' : 'Sending');
this._debug(`SFTP: Outbound: ${status} expand-path@openssh.com`);
}
}
ext_copy_data(srcHandle, srcOffset, len, dstHandle, dstOffset, cb) {
if (this.server)
throw new Error('Client-only method called in server mode');
const ext = this._extensions['copy-data'];
if (ext !== '1')
throw new Error('Server does not support this extended request');
if (!Buffer.isBuffer(srcHandle))
throw new Error('Source handle is not a Buffer');
if (!Buffer.isBuffer(dstHandle))
throw new Error('Destination handle is not a Buffer');
/*
uint32 id
string "copy-data"
string read-from-handle
uint64 read-from-offset
uint64 read-data-length
string write-to-handle
uint64 write-to-offset
*/
let p = 0;
const buf = Buffer.allocUnsafe(
4 + 1
+ 4
+ 4 + 9
+ 4 + srcHandle.length
+ 8
+ 8
+ 4 + dstHandle.length
+ 8
);
writeUInt32BE(buf, buf.length - 4, p);
p += 4;
buf[p] = REQUEST.EXTENDED;
++p;
const reqid = this._writeReqid = (this._writeReqid + 1) & MAX_REQID;
writeUInt32BE(buf, reqid, p);
p += 4;
writeUInt32BE(buf, 9, p);
p += 4;
buf.utf8Write('copy-data', p, 9);
p += 9;
writeUInt32BE(buf, srcHandle.length, p);
p += 4;
buf.set(srcHandle, p);
p += srcHandle.length;
for (let i = 7; i >= 0; --i) {
buf[p + i] = srcOffset & 0xFF;
srcOffset /= 256;
}
p += 8;
for (let i = 7; i >= 0; --i) {
buf[p + i] = len & 0xFF;
len /= 256;
}
p += 8;
writeUInt32BE(buf, dstHandle.length, p);
p += 4;
buf.set(dstHandle, p);
p += dstHandle.length;
for (let i = 7; i >= 0; --i) {
buf[p + i] = dstOffset & 0xFF;
dstOffset /= 256;
}
this._requests[reqid] = { cb };
const isBuffered = sendOrBuffer(this, buf);
if (this._debug) {
const status = (isBuffered ? 'Buffered' : 'Sending');
this._debug(`SFTP: Outbound: ${status} copy-data`);
}
}
ext_home_dir(username, cb) {
if (this.server)
throw new Error('Client-only method called in server mode');
const ext = this._extensions['home-directory'];
if (ext !== '1')
throw new Error('Server does not support this extended request');
if (typeof username !== 'string')
throw new TypeError('username is not a string');
/*
uint32 id
string "home-directory"
string username
*/
let p = 0;
const usernameLen = Buffer.byteLength(username);
const buf = Buffer.allocUnsafe(
4 + 1
+ 4
+ 4 + 14
+ 4 + usernameLen
);
writeUInt32BE(buf, buf.length - 4, p);
p += 4;
buf[p] = REQUEST.EXTENDED;
++p;
const reqid = this._writeReqid = (this._writeReqid + 1) & MAX_REQID;
writeUInt32BE(buf, reqid, p);
p += 4;
writeUInt32BE(buf, 14, p);
p += 4;
buf.utf8Write('home-directory', p, 14);
p += 14;
writeUInt32BE(buf, usernameLen, p);
p += 4;
buf.utf8Write(username, p, usernameLen);
p += usernameLen;
this._requests[reqid] = {
cb: (err, names) => {
if (typeof cb !== 'function')
return;
if (err)
return cb(err);
if (!names || !names.length)
return cb(new Error('Response missing home directory'));
cb(undefined, names[0].filename);
}
};
const isBuffered = sendOrBuffer(this, buf);
if (this._debug) {
const status = (isBuffered ? 'Buffered' : 'Sending');
this._debug(`SFTP: Outbound: ${status} home-directory`);
}
}
ext_users_groups(uids, gids, cb) {
if (this.server)
throw new Error('Client-only method called in server mode');
const ext = this._extensions['users-groups-by-id@openssh.com'];
if (ext !== '1')
throw new Error('Server does not support this extended request');
if (!Array.isArray(uids))
throw new TypeError('uids is not an array');
for (const val of uids) {
if (!Number.isInteger(val) || val < 0 || val > (2 ** 32 - 1))
throw new Error('uid values must all be 32-bit unsigned integers');
}
if (!Array.isArray(gids))
throw new TypeError('gids is not an array');
for (const val of gids) {
if (!Number.isInteger(val) || val < 0 || val > (2 ** 32 - 1))
throw new Error('gid values must all be 32-bit unsigned integers');
}
/*
uint32 id
string "users-groups-by-id@openssh.com"
string uids
uint32 uid1
...
string gids
uint32 gid1
...
*/
let p = 0;
const buf = Buffer.allocUnsafe(
4 + 1
+ 4
+ 4 + 30
+ 4 + (4 * uids.length)
+ 4 + (4 * gids.length)
);
writeUInt32BE(buf, buf.length - 4, p);
p += 4;
buf[p] = REQUEST.EXTENDED;
++p;
const reqid = this._writeReqid = (this._writeReqid + 1) & MAX_REQID;
writeUInt32BE(buf, reqid, p);
p += 4;
writeUInt32BE(buf, 30, p);
p += 4;
buf.utf8Write('users-groups-by-id@openssh.com', p, 30);
p += 30;
writeUInt32BE(buf, 4 * uids.length, p);
p += 4;
for (const val of uids) {
writeUInt32BE(buf, val, p);
p += 4;
}
writeUInt32BE(buf, 4 * gids.length, p);
p += 4;
for (const val of gids) {
writeUInt32BE(buf, val, p);
p += 4;
}
this._requests[reqid] = { extended: 'users-groups-by-id@openssh.com', cb };
const isBuffered = sendOrBuffer(this, buf);
if (this._debug) {
const status = (isBuffered ? 'Buffered' : 'Sending');
this._debug(`SFTP: Outbound: ${status} users-groups-by-id@openssh.com`);
}
}
// ===========================================================================
// Server-specific ===========================================================
// ===========================================================================
handle(reqid, handle) {
if (!this.server)
throw new Error('Server-only method called in client mode');
if (!Buffer.isBuffer(handle))
throw new Error('handle is not a Buffer');
const handleLen = handle.length;
if (handleLen > 256)
throw new Error('handle too large (> 256 bytes)');
let p = 9;
const buf = Buffer.allocUnsafe(4 + 1 + 4 + 4 + handleLen);
writeUInt32BE(buf, buf.length - 4, 0);
buf[4] = RESPONSE.HANDLE;
writeUInt32BE(buf, reqid, 5);
writeUInt32BE(buf, handleLen, p);
if (handleLen)
buf.set(handle, p += 4);
const isBuffered = sendOrBuffer(this, buf);
this._debug && this._debug(
`SFTP: Outbound: ${isBuffered ? 'Buffered' : 'Sending'} HANDLE`
);
}
status(reqid, code, message) {
if (!this.server)
throw new Error('Server-only method called in client mode');
if (!VALID_STATUS_CODES.has(code))
throw new Error(`Bad status code: ${code}`);
message || (message = '');
const msgLen = Buffer.byteLength(message);
let p = 9;
const buf = Buffer.allocUnsafe(4 + 1 + 4 + 4 + 4 + msgLen + 4);
writeUInt32BE(buf, buf.length - 4, 0);
buf[4] = RESPONSE.STATUS;
writeUInt32BE(buf, reqid, 5);
writeUInt32BE(buf, code, p);
writeUInt32BE(buf, msgLen, p += 4);
p += 4;
if (msgLen) {
buf.utf8Write(message, p, msgLen);
p += msgLen;
}
writeUInt32BE(buf, 0, p); // Empty language tag
const isBuffered = sendOrBuffer(this, buf);
this._debug && this._debug(
`SFTP: Outbound: ${isBuffered ? 'Buffered' : 'Sending'} STATUS`
);
}
data(reqid, data, encoding) {
if (!this.server)
throw new Error('Server-only method called in client mode');
const isBuffer = Buffer.isBuffer(data);
if (!isBuffer && typeof data !== 'string')
throw new Error('data is not a Buffer or string');
let isUTF8;
if (!isBuffer && !encoding) {
encoding = undefined;
isUTF8 = true;
}
const dataLen = (
isBuffer
? data.length
: Buffer.byteLength(data, encoding)
);
let p = 9;
const buf = Buffer.allocUnsafe(4 + 1 + 4 + 4 + dataLen);
writeUInt32BE(buf, buf.length - 4, 0);
buf[4] = RESPONSE.DATA;
writeUInt32BE(buf, reqid, 5);
writeUInt32BE(buf, dataLen, p);
if (dataLen) {
if (isBuffer)
buf.set(data, p += 4);
else if (isUTF8)
buf.utf8Write(data, p += 4, dataLen);
else
buf.write(data, p += 4, dataLen, encoding);
}
const isBuffered = sendOrBuffer(this, buf);
this._debug && this._debug(
`SFTP: Outbound: ${isBuffered ? 'Buffered' : 'Sending'} DATA`
);
}
name(reqid, names) {
if (!this.server)
throw new Error('Server-only method called in client mode');
if (!Array.isArray(names)) {
if (typeof names !== 'object' || names === null)
throw new Error('names is not an object or array');
names = [ names ];
}
const count = names.length;
let namesLen = 0;
let nameAttrs;
const attrs = [];
for (let i = 0; i < count; ++i) {
const name = names[i];
const filename = (
!name || !name.filename || typeof name.filename !== 'string'
? ''
: name.filename
);
namesLen += 4 + Buffer.byteLength(filename);
const longname = (
!name || !name.longname || typeof name.longname !== 'string'
? ''
: name.longname
);
namesLen += 4 + Buffer.byteLength(longname);
if (typeof name.attrs === 'object' && name.attrs !== null) {
nameAttrs = attrsToBytes(name.attrs);
namesLen += 4 + nameAttrs.nb;
if (nameAttrs.nb) {
let bytes;
if (nameAttrs.nb === ATTRS_BUF.length) {
bytes = new Uint8Array(ATTRS_BUF);
} else {
bytes = new Uint8Array(nameAttrs.nb);
bufferCopy(ATTRS_BUF, bytes, 0, nameAttrs.nb, 0);
}
nameAttrs.bytes = bytes;
}
attrs.push(nameAttrs);
} else {
namesLen += 4;
attrs.push(null);
}
}
let p = 9;
const buf = Buffer.allocUnsafe(4 + 1 + 4 + 4 + namesLen);
writeUInt32BE(buf, buf.length - 4, 0);
buf[4] = RESPONSE.NAME;
writeUInt32BE(buf, reqid, 5);
writeUInt32BE(buf, count, p);
p += 4;
for (let i = 0; i < count; ++i) {
const name = names[i];
{
const filename = (
!name || !name.filename || typeof name.filename !== 'string'
? ''
: name.filename
);
const len = Buffer.byteLength(filename);
writeUInt32BE(buf, len, p);
p += 4;
if (len) {
buf.utf8Write(filename, p, len);
p += len;
}
}
{
const longname = (
!name || !name.longname || typeof name.longname !== 'string'
? ''
: name.longname
);
const len = Buffer.byteLength(longname);
writeUInt32BE(buf, len, p);
p += 4;
if (len) {
buf.utf8Write(longname, p, len);
p += len;
}
}
const attr = attrs[i];
if (attr) {
writeUInt32BE(buf, attr.flags, p);
p += 4;
if (attr.flags && attr.bytes) {
buf.set(attr.bytes, p);
p += attr.nb;
}
} else {
writeUInt32BE(buf, 0, p);
p += 4;
}
}
const isBuffered = sendOrBuffer(this, buf);
this._debug && this._debug(
`SFTP: Outbound: ${isBuffered ? 'Buffered' : 'Sending'} NAME`
);
}
attrs(reqid, attrs) {
if (!this.server)
throw new Error('Server-only method called in client mode');
if (typeof attrs !== 'object' || attrs === null)
throw new Error('attrs is not an object');
attrs = attrsToBytes(attrs);
const flags = attrs.flags;
const attrsLen = attrs.nb;
let p = 9;
const buf = Buffer.allocUnsafe(4 + 1 + 4 + 4 + attrsLen);
writeUInt32BE(buf, buf.length - 4, 0);
buf[4] = RESPONSE.ATTRS;
writeUInt32BE(buf, reqid, 5);
writeUInt32BE(buf, flags, p);
if (attrsLen) {
p += 4;
if (attrsLen === ATTRS_BUF.length)
buf.set(ATTRS_BUF, p);
else
bufferCopy(ATTRS_BUF, buf, 0, attrsLen, p);
p += attrsLen;
}
const isBuffered = sendOrBuffer(this, buf);
this._debug && this._debug(
`SFTP: Outbound: ${isBuffered ? 'Buffered' : 'Sending'} ATTRS`
);
}
}
function tryCreateBuffer(size) {
try {
return Buffer.allocUnsafe(size);
} catch (ex) {
return ex;
}
}
function read_(self, handle, buf, off, len, position, cb, req_) {
const maxDataLen = self._maxReadLen;
const overflow = Math.max(len - maxDataLen, 0);
if (overflow)
len = maxDataLen;
/*
uint32 id
string handle
uint64 offset
uint32 len
*/
const handleLen = handle.length;
let p = 9;
let pos = position;
const out = Buffer.allocUnsafe(4 + 1 + 4 + 4 + handleLen + 8 + 4);
writeUInt32BE(out, out.length - 4, 0);
out[4] = REQUEST.READ;
const reqid = self._writeReqid = (self._writeReqid + 1) & MAX_REQID;
writeUInt32BE(out, reqid, 5);
writeUInt32BE(out, handleLen, p);
out.set(handle, p += 4);
p += handleLen;
for (let i = 7; i >= 0; --i) {
out[p + i] = pos & 0xFF;
pos /= 256;
}
writeUInt32BE(out, len, p += 8);
if (typeof cb !== 'function')
cb = noop;
const req = (req_ || {
nb: 0,
position,
off,
origOff: off,
len: undefined,
overflow: undefined,
cb: (err, data, nb) => {
const len = req.len;
const overflow = req.overflow;
if (err) {
if (cb._wantEOFError || err.code !== STATUS_CODE.EOF)
return cb(err);
} else if (nb > len) {
return cb(new Error('Received more data than requested'));
} else if (nb === len && overflow) {
req.nb += nb;
req.position += nb;
req.off += nb;
read_(self, handle, buf, req.off, overflow, req.position, cb, req);
return;
}
nb = (nb || 0);
if (req.origOff === 0 && buf.length === req.nb)
data = buf;
else
data = bufferSlice(buf, req.origOff, req.origOff + req.nb + nb);
cb(undefined, req.nb + nb, data, req.position);
},
buffer: undefined,
});
req.len = len;
req.overflow = overflow;
// TODO: avoid creating multiple buffer slices when we need to re-call read_()
// because of overflow
req.buffer = bufferSlice(buf, off, off + len);
self._requests[reqid] = req;
const isBuffered = sendOrBuffer(self, out);
self._debug && self._debug(
`SFTP: Outbound: ${isBuffered ? 'Buffered' : 'Sending'} READ`
);
}
function fastXfer(src, dst, srcPath, dstPath, opts, cb) {
let concurrency = 64;
let chunkSize = 32768;
let onstep;
let mode;
let fileSize;
if (typeof opts === 'function') {
cb = opts;
} else if (typeof opts === 'object' && opts !== null) {
if (typeof opts.concurrency === 'number'
&& opts.concurrency > 0
&& !isNaN(opts.concurrency)) {
concurrency = opts.concurrency;
}
if (typeof opts.chunkSize === 'number'
&& opts.chunkSize > 0
&& !isNaN(opts.chunkSize)) {
chunkSize = opts.chunkSize;
}
if (typeof opts.fileSize === 'number'
&& opts.fileSize > 0
&& !isNaN(opts.fileSize)) {
fileSize = opts.fileSize;
}
if (typeof opts.step === 'function')
onstep = opts.step;
if (typeof opts.mode === 'string' || typeof opts.mode === 'number')
mode = modeNum(opts.mode);
}
// Internal state variables
let fsize;
let pdst = 0;
let total = 0;
let hadError = false;
let srcHandle;
let dstHandle;
let readbuf;
let bufsize = chunkSize * concurrency;
function onerror(err) {
if (hadError)
return;
hadError = true;
let left = 0;
let cbfinal;
if (srcHandle || dstHandle) {
cbfinal = () => {
if (--left === 0)
cb(err);
};
if (srcHandle && (src === fs || src.outgoing.state === 'open'))
++left;
if (dstHandle && (dst === fs || dst.outgoing.state === 'open'))
++left;
if (srcHandle && (src === fs || src.outgoing.state === 'open'))
src.close(srcHandle, cbfinal);
if (dstHandle && (dst === fs || dst.outgoing.state === 'open'))
dst.close(dstHandle, cbfinal);
} else {
cb(err);
}
}
src.open(srcPath, 'r', (err, sourceHandle) => {
if (err)
return onerror(err);
srcHandle = sourceHandle;
if (fileSize === undefined)
src.fstat(srcHandle, tryStat);
else
tryStat(null, { size: fileSize });
function tryStat(err, attrs) {
if (err) {
if (src !== fs) {
// Try stat() for sftp servers that may not support fstat() for
// whatever reason
src.stat(srcPath, (err_, attrs_) => {
if (err_)
return onerror(err);
tryStat(null, attrs_);
});
return;
}
return onerror(err);
}
fsize = attrs.size;
dst.open(dstPath, 'w', (err, destHandle) => {
if (err)
return onerror(err);
dstHandle = destHandle;
if (fsize <= 0)
return onerror();
// Use less memory where possible
while (bufsize > fsize) {
if (concurrency === 1) {
bufsize = fsize;
break;
}
bufsize -= chunkSize;
--concurrency;
}
readbuf = tryCreateBuffer(bufsize);
if (readbuf instanceof Error)
return onerror(readbuf);
if (mode !== undefined) {
dst.fchmod(dstHandle, mode, function tryAgain(err) {
if (err) {
// Try chmod() for sftp servers that may not support fchmod()
// for whatever reason
dst.chmod(dstPath, mode, (err_) => tryAgain());
return;
}
startReads();
});
} else {
startReads();
}
function onread(err, nb, data, dstpos, datapos, origChunkLen) {
if (err)
return onerror(err);
datapos = datapos || 0;
dst.write(dstHandle, readbuf, datapos, nb, dstpos, writeCb);
function writeCb(err) {
if (err)
return onerror(err);
total += nb;
onstep && onstep(total, nb, fsize);
if (nb < origChunkLen)
return singleRead(datapos, dstpos + nb, origChunkLen - nb);
if (total === fsize) {
dst.close(dstHandle, (err) => {
dstHandle = undefined;
if (err)
return onerror(err);
src.close(srcHandle, (err) => {
srcHandle = undefined;
if (err)
return onerror(err);
cb();
});
});
return;
}
if (pdst >= fsize)
return;
const chunk =
(pdst + chunkSize > fsize ? fsize - pdst : chunkSize);
singleRead(datapos, pdst, chunk);
pdst += chunk;
}
}
function makeCb(psrc, pdst, chunk) {
return (err, nb, data) => {
onread(err, nb, data, pdst, psrc, chunk);
};
}
function singleRead(psrc, pdst, chunk) {
src.read(srcHandle,
readbuf,
psrc,
chunk,
pdst,
makeCb(psrc, pdst, chunk));
}
function startReads() {
let reads = 0;
let psrc = 0;
while (pdst < fsize && reads < concurrency) {
const chunk =
(pdst + chunkSize > fsize ? fsize - pdst : chunkSize);
singleRead(psrc, pdst, chunk);
psrc += chunk;
pdst += chunk;
++reads;
}
}
});
}
});
}
function writeAll(sftp, handle, buffer, offset, length, position, callback_) {
const callback = (typeof callback_ === 'function' ? callback_ : undefined);
sftp.write(handle,
buffer,
offset,
length,
position,
(writeErr, written) => {
if (writeErr) {
return sftp.close(handle, () => {
callback && callback(writeErr);
});
}
if (written === length) {
sftp.close(handle, callback);
} else {
offset += written;
length -= written;
position += written;
writeAll(sftp, handle, buffer, offset, length, position, callback);
}
});
}
class Stats {
constructor(initial) {
this.mode = (initial && initial.mode);
this.uid = (initial && initial.uid);
this.gid = (initial && initial.gid);
this.size = (initial && initial.size);
this.atime = (initial && initial.atime);
this.mtime = (initial && initial.mtime);
this.extended = (initial && initial.extended);
}
isDirectory() {
return ((this.mode & constants.S_IFMT) === constants.S_IFDIR);
}
isFile() {
return ((this.mode & constants.S_IFMT) === constants.S_IFREG);
}
isBlockDevice() {
return ((this.mode & constants.S_IFMT) === constants.S_IFBLK);
}
isCharacterDevice() {
return ((this.mode & constants.S_IFMT) === constants.S_IFCHR);
}
isSymbolicLink() {
return ((this.mode & constants.S_IFMT) === constants.S_IFLNK);
}
isFIFO() {
return ((this.mode & constants.S_IFMT) === constants.S_IFIFO);
}
isSocket() {
return ((this.mode & constants.S_IFMT) === constants.S_IFSOCK);
}
}
function attrsToBytes(attrs) {
let flags = 0;
let nb = 0;
if (typeof attrs === 'object' && attrs !== null) {
if (typeof attrs.size === 'number') {
flags |= ATTR.SIZE;
const val = attrs.size;
// Big Endian
ATTRS_BUF[nb++] = val / 72057594037927940; // 2**56
ATTRS_BUF[nb++] = val / 281474976710656; // 2**48
ATTRS_BUF[nb++] = val / 1099511627776; // 2**40
ATTRS_BUF[nb++] = val / 4294967296; // 2**32
ATTRS_BUF[nb++] = val / 16777216; // 2**24
ATTRS_BUF[nb++] = val / 65536; // 2**16
ATTRS_BUF[nb++] = val / 256; // 2**8
ATTRS_BUF[nb++] = val;
}
if (typeof attrs.uid === 'number' && typeof attrs.gid === 'number') {
flags |= ATTR.UIDGID;
const uid = attrs.uid;
const gid = attrs.gid;
// Big Endian
ATTRS_BUF[nb++] = uid >>> 24;
ATTRS_BUF[nb++] = uid >>> 16;
ATTRS_BUF[nb++] = uid >>> 8;
ATTRS_BUF[nb++] = uid;
ATTRS_BUF[nb++] = gid >>> 24;
ATTRS_BUF[nb++] = gid >>> 16;
ATTRS_BUF[nb++] = gid >>> 8;
ATTRS_BUF[nb++] = gid;
}
if (typeof attrs.mode === 'number' || typeof attrs.mode === 'string') {
const mode = modeNum(attrs.mode);
flags |= ATTR.PERMISSIONS;
// Big Endian
ATTRS_BUF[nb++] = mode >>> 24;
ATTRS_BUF[nb++] = mode >>> 16;
ATTRS_BUF[nb++] = mode >>> 8;
ATTRS_BUF[nb++] = mode;
}
if ((typeof attrs.atime === 'number' || isDate(attrs.atime))
&& (typeof attrs.mtime === 'number' || isDate(attrs.mtime))) {
const atime = toUnixTimestamp(attrs.atime);
const mtime = toUnixTimestamp(attrs.mtime);
flags |= ATTR.ACMODTIME;
// Big Endian
ATTRS_BUF[nb++] = atime >>> 24;
ATTRS_BUF[nb++] = atime >>> 16;
ATTRS_BUF[nb++] = atime >>> 8;
ATTRS_BUF[nb++] = atime;
ATTRS_BUF[nb++] = mtime >>> 24;
ATTRS_BUF[nb++] = mtime >>> 16;
ATTRS_BUF[nb++] = mtime >>> 8;
ATTRS_BUF[nb++] = mtime;
}
// TODO: extended attributes
}
return { flags, nb };
}
function toUnixTimestamp(time) {
// eslint-disable-next-line no-self-compare
if (typeof time === 'number' && time === time) // Valid, non-NaN number
return time;
if (isDate(time))
return parseInt(time.getTime() / 1000, 10);
throw new Error(`Cannot parse time: ${time}`);
}
function modeNum(mode) {
// eslint-disable-next-line no-self-compare
if (typeof mode === 'number' && mode === mode) // Valid, non-NaN number
return mode;
if (typeof mode === 'string')
return modeNum(parseInt(mode, 8));
throw new Error(`Cannot parse mode: ${mode}`);
}
const stringFlagMap = {
'r': OPEN_MODE.READ,
'r+': OPEN_MODE.READ | OPEN_MODE.WRITE,
'w': OPEN_MODE.TRUNC | OPEN_MODE.CREAT | OPEN_MODE.WRITE,
'wx': OPEN_MODE.TRUNC | OPEN_MODE.CREAT | OPEN_MODE.WRITE | OPEN_MODE.EXCL,
'xw': OPEN_MODE.TRUNC | OPEN_MODE.CREAT | OPEN_MODE.WRITE | OPEN_MODE.EXCL,
'w+': OPEN_MODE.TRUNC | OPEN_MODE.CREAT | OPEN_MODE.READ | OPEN_MODE.WRITE,
'wx+': OPEN_MODE.TRUNC | OPEN_MODE.CREAT | OPEN_MODE.READ | OPEN_MODE.WRITE
| OPEN_MODE.EXCL,
'xw+': OPEN_MODE.TRUNC | OPEN_MODE.CREAT | OPEN_MODE.READ | OPEN_MODE.WRITE
| OPEN_MODE.EXCL,
'a': OPEN_MODE.APPEND | OPEN_MODE.CREAT | OPEN_MODE.WRITE,
'ax': OPEN_MODE.APPEND | OPEN_MODE.CREAT | OPEN_MODE.WRITE | OPEN_MODE.EXCL,
'xa': OPEN_MODE.APPEND | OPEN_MODE.CREAT | OPEN_MODE.WRITE | OPEN_MODE.EXCL,
'a+': OPEN_MODE.APPEND | OPEN_MODE.CREAT | OPEN_MODE.READ | OPEN_MODE.WRITE,
'ax+': OPEN_MODE.APPEND | OPEN_MODE.CREAT | OPEN_MODE.READ | OPEN_MODE.WRITE
| OPEN_MODE.EXCL,
'xa+': OPEN_MODE.APPEND | OPEN_MODE.CREAT | OPEN_MODE.READ | OPEN_MODE.WRITE
| OPEN_MODE.EXCL
};
function stringToFlags(str) {
const flags = stringFlagMap[str];
return (flags !== undefined ? flags : null);
}
const flagsToString = (() => {
const stringFlagMapKeys = Object.keys(stringFlagMap);
return (flags) => {
for (let i = 0; i < stringFlagMapKeys.length; ++i) {
const key = stringFlagMapKeys[i];
if (stringFlagMap[key] === flags)
return key;
}
return null;
};
})();
function readAttrs(biOpt) {
/*
uint32 flags
uint64 size present only if flag SSH_FILEXFER_ATTR_SIZE
uint32 uid present only if flag SSH_FILEXFER_ATTR_UIDGID
uint32 gid present only if flag SSH_FILEXFER_ATTR_UIDGID
uint32 permissions present only if flag SSH_FILEXFER_ATTR_PERMISSIONS
uint32 atime present only if flag SSH_FILEXFER_ACMODTIME
uint32 mtime present only if flag SSH_FILEXFER_ACMODTIME
uint32 extended_count present only if flag SSH_FILEXFER_ATTR_EXTENDED
string extended_type
string extended_data
... more extended data (extended_type - extended_data pairs),
so that number of pairs equals extended_count
*/
const flags = bufferParser.readUInt32BE();
if (flags === undefined)
return;
const attrs = new Stats();
if (flags & ATTR.SIZE) {
const size = bufferParser.readUInt64BE(biOpt);
if (size === undefined)
return;
attrs.size = size;
}
if (flags & ATTR.UIDGID) {
const uid = bufferParser.readUInt32BE();
const gid = bufferParser.readUInt32BE();
if (gid === undefined)
return;
attrs.uid = uid;
attrs.gid = gid;
}
if (flags & ATTR.PERMISSIONS) {
const mode = bufferParser.readUInt32BE();
if (mode === undefined)
return;
attrs.mode = mode;
}
if (flags & ATTR.ACMODTIME) {
const atime = bufferParser.readUInt32BE();
const mtime = bufferParser.readUInt32BE();
if (mtime === undefined)
return;
attrs.atime = atime;
attrs.mtime = mtime;
}
if (flags & ATTR.EXTENDED) {
const count = bufferParser.readUInt32BE();
if (count === undefined)
return;
const extended = {};
for (let i = 0; i < count; ++i) {
const type = bufferParser.readString(true);
const data = bufferParser.readString();
if (data === undefined)
return;
extended[type] = data;
}
attrs.extended = extended;
}
return attrs;
}
function sendOrBuffer(sftp, payload) {
const ret = tryWritePayload(sftp, payload);
if (ret !== undefined) {
sftp._buffer.push(ret);
return false;
}
return true;
}
function tryWritePayload(sftp, payload) {
const outgoing = sftp.outgoing;
if (outgoing.state !== 'open')
return;
if (outgoing.window === 0) {
sftp._waitWindow = true;
sftp._chunkcb = drainBuffer;
return payload;
}
let ret;
const len = payload.length;
let p = 0;
while (len - p > 0 && outgoing.window > 0) {
const actualLen = Math.min(len - p, outgoing.window, outgoing.packetSize);
outgoing.window -= actualLen;
if (outgoing.window === 0) {
sftp._waitWindow = true;
sftp._chunkcb = drainBuffer;
}
if (p === 0 && actualLen === len) {
sftp._protocol.channelData(sftp.outgoing.id, payload);
} else {
sftp._protocol.channelData(sftp.outgoing.id,
bufferSlice(payload, p, p + actualLen));
}
p += actualLen;
}
if (len - p > 0) {
if (p > 0)
ret = bufferSlice(payload, p, len);
else
ret = payload; // XXX: should never get here?
}
return ret;
}
function drainBuffer() {
this._chunkcb = undefined;
const buffer = this._buffer;
let i = 0;
while (i < buffer.length) {
const payload = buffer[i];
const ret = tryWritePayload(this, payload);
if (ret !== undefined) {
if (ret !== payload)
buffer[i] = ret;
if (i > 0)
this._buffer = buffer.slice(i);
return;
}
++i;
}
if (i > 0)
this._buffer = [];
}
function doFatalSFTPError(sftp, msg, noDebug) {
const err = new Error(msg);
err.level = 'sftp-protocol';
if (!noDebug && sftp._debug)
sftp._debug(`SFTP: Inbound: ${msg}`);
sftp.emit('error', err);
sftp.destroy();
cleanupRequests(sftp);
return false;
}
function cleanupRequests(sftp) {
const keys = Object.keys(sftp._requests);
if (keys.length === 0)
return;
const reqs = sftp._requests;
sftp._requests = {};
const err = new Error('No response from server');
for (let i = 0; i < keys.length; ++i) {
const req = reqs[keys[i]];
if (typeof req.cb === 'function')
req.cb(err);
}
}
function requestLimits(sftp, cb) {
/*
uint32 id
string "limits@openssh.com"
*/
let p = 9;
const buf = Buffer.allocUnsafe(4 + 1 + 4 + 4 + 18);
writeUInt32BE(buf, buf.length - 4, 0);
buf[4] = REQUEST.EXTENDED;
const reqid = sftp._writeReqid = (sftp._writeReqid + 1) & MAX_REQID;
writeUInt32BE(buf, reqid, 5);
writeUInt32BE(buf, 18, p);
buf.utf8Write('limits@openssh.com', p += 4, 18);
sftp._requests[reqid] = { extended: 'limits@openssh.com', cb };
const isBuffered = sendOrBuffer(sftp, buf);
if (sftp._debug) {
const which = (isBuffered ? 'Buffered' : 'Sending');
sftp._debug(`SFTP: Outbound: ${which} limits@openssh.com`);
}
}
const CLIENT_HANDLERS = {
[RESPONSE.VERSION]: (sftp, payload) => {
if (sftp._version !== -1)
return doFatalSFTPError(sftp, 'Duplicate VERSION packet');
const extensions = {};
/*
uint32 version
<extension data>
*/
bufferParser.init(payload, 1);
let version = bufferParser.readUInt32BE();
while (bufferParser.avail()) {
const extName = bufferParser.readString(true);
const extData = bufferParser.readString(true);
if (extData === undefined) {
version = undefined;
break;
}
extensions[extName] = extData;
}
bufferParser.clear();
if (version === undefined)
return doFatalSFTPError(sftp, 'Malformed VERSION packet');
if (sftp._debug) {
const names = Object.keys(extensions);
if (names.length) {
sftp._debug(
`SFTP: Inbound: Received VERSION (v${version}, exts:${names})`
);
} else {
sftp._debug(`SFTP: Inbound: Received VERSION (v${version})`);
}
}
sftp._version = version;
sftp._extensions = extensions;
if (extensions['limits@openssh.com'] === '1') {
return requestLimits(sftp, (err, limits) => {
if (!err) {
if (limits.maxPktLen > 0)
sftp._maxOutPktLen = limits.maxPktLen;
if (limits.maxReadLen > 0)
sftp._maxReadLen = limits.maxReadLen;
if (limits.maxWriteLen > 0)
sftp._maxWriteLen = limits.maxWriteLen;
sftp.maxOpenHandles = (
limits.maxOpenHandles > 0 ? limits.maxOpenHandles : Infinity
);
}
sftp.emit('ready');
});
}
sftp.emit('ready');
},
[RESPONSE.STATUS]: (sftp, payload) => {
bufferParser.init(payload, 1);
const reqID = bufferParser.readUInt32BE();
/*
uint32 error/status code
string error message (ISO-10646 UTF-8)
string language tag
*/
const errorCode = bufferParser.readUInt32BE();
const errorMsg = bufferParser.readString(true);
bufferParser.clear();
// Note: we avoid checking that the error message and language tag are in
// the packet because there are some broken implementations that incorrectly
// omit them. The language tag in general was never really used amongst ssh
// implementations, so in the case of a missing error message we just
// default to something sensible.
if (sftp._debug) {
const jsonMsg = JSON.stringify(errorMsg);
sftp._debug(
`SFTP: Inbound: Received STATUS (id:${reqID}, ${errorCode}, ${jsonMsg})`
);
}
const req = sftp._requests[reqID];
delete sftp._requests[reqID];
if (req && typeof req.cb === 'function') {
if (errorCode === STATUS_CODE.OK) {
req.cb();
return;
}
const err = new Error(errorMsg
|| STATUS_CODE_STR[errorCode]
|| 'Unknown status');
err.code = errorCode;
req.cb(err);
}
},
[RESPONSE.HANDLE]: (sftp, payload) => {
bufferParser.init(payload, 1);
const reqID = bufferParser.readUInt32BE();
/*
string handle
*/
const handle = bufferParser.readString();
bufferParser.clear();
if (handle === undefined) {
if (reqID !== undefined)
delete sftp._requests[reqID];
return doFatalSFTPError(sftp, 'Malformed HANDLE packet');
}
sftp._debug && sftp._debug(`SFTP: Inbound: Received HANDLE (id:${reqID})`);
const req = sftp._requests[reqID];
delete sftp._requests[reqID];
if (req && typeof req.cb === 'function')
req.cb(undefined, handle);
},
[RESPONSE.DATA]: (sftp, payload) => {
bufferParser.init(payload, 1);
const reqID = bufferParser.readUInt32BE();
let req;
if (reqID !== undefined) {
req = sftp._requests[reqID];
delete sftp._requests[reqID];
}
/*
string data
*/
if (req && typeof req.cb === 'function') {
if (req.buffer) {
// We have already pre-allocated space to store the data
const nb = bufferParser.readString(req.buffer);
bufferParser.clear();
if (nb !== undefined) {
sftp._debug && sftp._debug(
`SFTP: Inbound: Received DATA (id:${reqID}, ${nb})`
);
req.cb(undefined, req.buffer, nb);
return;
}
} else {
const data = bufferParser.readString();
bufferParser.clear();
if (data !== undefined) {
sftp._debug && sftp._debug(
`SFTP: Inbound: Received DATA (id:${reqID}, ${data.length})`
);
req.cb(undefined, data);
return;
}
}
} else {
const nb = bufferParser.skipString();
bufferParser.clear();
if (nb !== undefined) {
sftp._debug && sftp._debug(
`SFTP: Inbound: Received DATA (id:${reqID}, ${nb})`
);
return;
}
}
return doFatalSFTPError(sftp, 'Malformed DATA packet');
},
[RESPONSE.NAME]: (sftp, payload) => {
bufferParser.init(payload, 1);
const reqID = bufferParser.readUInt32BE();
let req;
if (reqID !== undefined) {
req = sftp._requests[reqID];
delete sftp._requests[reqID];
}
/*
uint32 count
repeats count times:
string filename
string longname
ATTRS attrs
*/
const count = bufferParser.readUInt32BE();
if (count !== undefined) {
let names = [];
for (let i = 0; i < count; ++i) {
// We are going to assume UTF-8 for filenames despite the SFTPv3
// spec not specifying an encoding because the specs for newer
// versions of the protocol all explicitly specify UTF-8 for
// filenames
const filename = bufferParser.readString(true);
// `longname` only exists in SFTPv3 and since it typically will
// contain the filename, we assume it is also UTF-8
const longname = bufferParser.readString(true);
const attrs = readAttrs(sftp._biOpt);
if (attrs === undefined) {
names = undefined;
break;
}
names.push({ filename, longname, attrs });
}
if (names !== undefined) {
sftp._debug && sftp._debug(
`SFTP: Inbound: Received NAME (id:${reqID}, ${names.length})`
);
bufferParser.clear();
if (req && typeof req.cb === 'function')
req.cb(undefined, names);
return;
}
}
bufferParser.clear();
return doFatalSFTPError(sftp, 'Malformed NAME packet');
},
[RESPONSE.ATTRS]: (sftp, payload) => {
bufferParser.init(payload, 1);
const reqID = bufferParser.readUInt32BE();
let req;
if (reqID !== undefined) {
req = sftp._requests[reqID];
delete sftp._requests[reqID];
}
/*
ATTRS attrs
*/
const attrs = readAttrs(sftp._biOpt);
bufferParser.clear();
if (attrs !== undefined) {
sftp._debug && sftp._debug(`SFTP: Inbound: Received ATTRS (id:${reqID})`);
if (req && typeof req.cb === 'function')
req.cb(undefined, attrs);
return;
}
return doFatalSFTPError(sftp, 'Malformed ATTRS packet');
},
[RESPONSE.EXTENDED]: (sftp, payload) => {
bufferParser.init(payload, 1);
const reqID = bufferParser.readUInt32BE();
if (reqID !== undefined) {
const req = sftp._requests[reqID];
if (req) {
delete sftp._requests[reqID];
switch (req.extended) {
case 'statvfs@openssh.com':
case 'fstatvfs@openssh.com': {
/*
uint64 f_bsize // file system block size
uint64 f_frsize // fundamental fs block size
uint64 f_blocks // number of blocks (unit f_frsize)
uint64 f_bfree // free blocks in file system
uint64 f_bavail // free blocks for non-root
uint64 f_files // total file inodes
uint64 f_ffree // free file inodes
uint64 f_favail // free file inodes for to non-root
uint64 f_fsid // file system id
uint64 f_flag // bit mask of f_flag values
uint64 f_namemax // maximum filename length
*/
const biOpt = sftp._biOpt;
const stats = {
f_bsize: bufferParser.readUInt64BE(biOpt),
f_frsize: bufferParser.readUInt64BE(biOpt),
f_blocks: bufferParser.readUInt64BE(biOpt),
f_bfree: bufferParser.readUInt64BE(biOpt),
f_bavail: bufferParser.readUInt64BE(biOpt),
f_files: bufferParser.readUInt64BE(biOpt),
f_ffree: bufferParser.readUInt64BE(biOpt),
f_favail: bufferParser.readUInt64BE(biOpt),
f_sid: bufferParser.readUInt64BE(biOpt),
f_flag: bufferParser.readUInt64BE(biOpt),
f_namemax: bufferParser.readUInt64BE(biOpt),
};
if (stats.f_namemax === undefined)
break;
if (sftp._debug) {
sftp._debug(
'SFTP: Inbound: Received EXTENDED_REPLY '
+ `(id:${reqID}, ${req.extended})`
);
}
bufferParser.clear();
if (typeof req.cb === 'function')
req.cb(undefined, stats);
return;
}
case 'limits@openssh.com': {
/*
uint64 max-packet-length
uint64 max-read-length
uint64 max-write-length
uint64 max-open-handles
*/
const limits = {
maxPktLen: bufferParser.readUInt64BE(),
maxReadLen: bufferParser.readUInt64BE(),
maxWriteLen: bufferParser.readUInt64BE(),
maxOpenHandles: bufferParser.readUInt64BE(),
};
if (limits.maxOpenHandles === undefined)
break;
if (sftp._debug) {
sftp._debug(
'SFTP: Inbound: Received EXTENDED_REPLY '
+ `(id:${reqID}, ${req.extended})`
);
}
bufferParser.clear();
if (typeof req.cb === 'function')
req.cb(undefined, limits);
return;
}
case 'users-groups-by-id@openssh.com': {
/*
string usernames
string username1
...
string groupnames
string groupname1
...
*/
const usernameCount = bufferParser.readUInt32BE();
if (usernameCount === undefined)
break;
const usernames = new Array(usernameCount);
for (let i = 0; i < usernames.length; ++i)
usernames[i] = bufferParser.readString(true);
const groupnameCount = bufferParser.readUInt32BE();
if (groupnameCount === undefined)
break;
const groupnames = new Array(groupnameCount);
for (let i = 0; i < groupnames.length; ++i)
groupnames[i] = bufferParser.readString(true);
if (groupnames.length > 0
&& groupnames[groupnames.length - 1] === undefined) {
break;
}
if (sftp._debug) {
sftp._debug(
'SFTP: Inbound: Received EXTENDED_REPLY '
+ `(id:${reqID}, ${req.extended})`
);
}
bufferParser.clear();
if (typeof req.cb === 'function')
req.cb(undefined, usernames, groupnames);
return;
}
default:
// Unknown extended request
sftp._debug && sftp._debug(
`SFTP: Inbound: Received EXTENDED_REPLY (id:${reqID}, ???)`
);
bufferParser.clear();
if (typeof req.cb === 'function')
req.cb();
return;
}
} else {
sftp._debug && sftp._debug(
`SFTP: Inbound: Received EXTENDED_REPLY (id:${reqID}, ???)`
);
bufferParser.clear();
return;
}
}
bufferParser.clear();
return doFatalSFTPError(sftp, 'Malformed EXTENDED_REPLY packet');
},
};
const SERVER_HANDLERS = {
[REQUEST.INIT]: (sftp, payload) => {
if (sftp._version !== -1)
return doFatalSFTPError(sftp, 'Duplicate INIT packet');
const extensions = {};
/*
uint32 version
<extension data>
*/
bufferParser.init(payload, 1);
let version = bufferParser.readUInt32BE();
while (bufferParser.avail()) {
const extName = bufferParser.readString(true);
const extData = bufferParser.readString(true);
if (extData === undefined) {
version = undefined;
break;
}
extensions[extName] = extData;
}
bufferParser.clear();
if (version === undefined)
return doFatalSFTPError(sftp, 'Malformed INIT packet');
if (sftp._debug) {
const names = Object.keys(extensions);
if (names.length) {
sftp._debug(
`SFTP: Inbound: Received INIT (v${version}, exts:${names})`
);
} else {
sftp._debug(`SFTP: Inbound: Received INIT (v${version})`);
}
}
sendOrBuffer(sftp, SERVER_VERSION_BUFFER);
sftp._version = version;
sftp._extensions = extensions;
sftp.emit('ready');
},
[REQUEST.OPEN]: (sftp, payload) => {
bufferParser.init(payload, 1);
const reqID = bufferParser.readUInt32BE();
/*
string filename
uint32 pflags
ATTRS attrs
*/
const filename = bufferParser.readString(true);
const pflags = bufferParser.readUInt32BE();
const attrs = readAttrs(sftp._biOpt);
bufferParser.clear();
if (attrs === undefined)
return doFatalSFTPError(sftp, 'Malformed OPEN packet');
sftp._debug && sftp._debug(`SFTP: Inbound: Received OPEN (id:${reqID})`);
if (!sftp.emit('OPEN', reqID, filename, pflags, attrs)) {
// Automatically reject request if no handler for request type
sftp.status(reqID, STATUS_CODE.OP_UNSUPPORTED);
}
},
[REQUEST.CLOSE]: (sftp, payload) => {
bufferParser.init(payload, 1);
const reqID = bufferParser.readUInt32BE();
/*
string handle
*/
const handle = bufferParser.readString();
bufferParser.clear();
if (handle === undefined || handle.length > 256)
return doFatalSFTPError(sftp, 'Malformed CLOSE packet');
sftp._debug && sftp._debug(`SFTP: Inbound: Received CLOSE (id:${reqID})`);
if (!sftp.emit('CLOSE', reqID, handle)) {
// Automatically reject request if no handler for request type
sftp.status(reqID, STATUS_CODE.OP_UNSUPPORTED);
}
},
[REQUEST.READ]: (sftp, payload) => {
bufferParser.init(payload, 1);
const reqID = bufferParser.readUInt32BE();
/*
string handle
uint64 offset
uint32 len
*/
const handle = bufferParser.readString();
const offset = bufferParser.readUInt64BE(sftp._biOpt);
const len = bufferParser.readUInt32BE();
bufferParser.clear();
if (len === undefined || handle.length > 256)
return doFatalSFTPError(sftp, 'Malformed READ packet');
sftp._debug && sftp._debug(`SFTP: Inbound: Received READ (id:${reqID})`);
if (!sftp.emit('READ', reqID, handle, offset, len)) {
// Automatically reject request if no handler for request type
sftp.status(reqID, STATUS_CODE.OP_UNSUPPORTED);
}
},
[REQUEST.WRITE]: (sftp, payload) => {
bufferParser.init(payload, 1);
const reqID = bufferParser.readUInt32BE();
/*
string handle
uint64 offset
string data
*/
const handle = bufferParser.readString();
const offset = bufferParser.readUInt64BE(sftp._biOpt);
const data = bufferParser.readString();
bufferParser.clear();
if (data === undefined || handle.length > 256)
return doFatalSFTPError(sftp, 'Malformed WRITE packet');
sftp._debug && sftp._debug(`SFTP: Inbound: Received WRITE (id:${reqID})`);
if (!sftp.emit('WRITE', reqID, handle, offset, data)) {
// Automatically reject request if no handler for request type
sftp.status(reqID, STATUS_CODE.OP_UNSUPPORTED);
}
},
[REQUEST.LSTAT]: (sftp, payload) => {
bufferParser.init(payload, 1);
const reqID = bufferParser.readUInt32BE();
/*
string path
*/
const path = bufferParser.readString(true);
bufferParser.clear();
if (path === undefined)
return doFatalSFTPError(sftp, 'Malformed LSTAT packet');
sftp._debug && sftp._debug(`SFTP: Inbound: Received LSTAT (id:${reqID})`);
if (!sftp.emit('LSTAT', reqID, path)) {
// Automatically reject request if no handler for request type
sftp.status(reqID, STATUS_CODE.OP_UNSUPPORTED);
}
},
[REQUEST.FSTAT]: (sftp, payload) => {
bufferParser.init(payload, 1);
const reqID = bufferParser.readUInt32BE();
/*
string handle
*/
const handle = bufferParser.readString();
bufferParser.clear();
if (handle === undefined || handle.length > 256)
return doFatalSFTPError(sftp, 'Malformed FSTAT packet');
sftp._debug && sftp._debug(`SFTP: Inbound: Received FSTAT (id:${reqID})`);
if (!sftp.emit('FSTAT', reqID, handle)) {
// Automatically reject request if no handler for request type
sftp.status(reqID, STATUS_CODE.OP_UNSUPPORTED);
}
},
[REQUEST.SETSTAT]: (sftp, payload) => {
bufferParser.init(payload, 1);
const reqID = bufferParser.readUInt32BE();
/*
string path
ATTRS attrs
*/
const path = bufferParser.readString(true);
const attrs = readAttrs(sftp._biOpt);
bufferParser.clear();
if (attrs === undefined)
return doFatalSFTPError(sftp, 'Malformed SETSTAT packet');
sftp._debug && sftp._debug(`SFTP: Inbound: Received SETSTAT (id:${reqID})`);
if (!sftp.emit('SETSTAT', reqID, path, attrs)) {
// Automatically reject request if no handler for request type
sftp.status(reqID, STATUS_CODE.OP_UNSUPPORTED);
}
},
[REQUEST.FSETSTAT]: (sftp, payload) => {
bufferParser.init(payload, 1);
const reqID = bufferParser.readUInt32BE();
/*
string handle
ATTRS attrs
*/
const handle = bufferParser.readString();
const attrs = readAttrs(sftp._biOpt);
bufferParser.clear();
if (attrs === undefined || handle.length > 256)
return doFatalSFTPError(sftp, 'Malformed FSETSTAT packet');
sftp._debug && sftp._debug(
`SFTP: Inbound: Received FSETSTAT (id:${reqID})`
);
if (!sftp.emit('FSETSTAT', reqID, handle, attrs)) {
// Automatically reject request if no handler for request type
sftp.status(reqID, STATUS_CODE.OP_UNSUPPORTED);
}
},
[REQUEST.OPENDIR]: (sftp, payload) => {
bufferParser.init(payload, 1);
const reqID = bufferParser.readUInt32BE();
/*
string path
*/
const path = bufferParser.readString(true);
bufferParser.clear();
if (path === undefined)
return doFatalSFTPError(sftp, 'Malformed OPENDIR packet');
sftp._debug && sftp._debug(`SFTP: Inbound: Received OPENDIR (id:${reqID})`);
if (!sftp.emit('OPENDIR', reqID, path)) {
// Automatically reject request if no handler for request type
sftp.status(reqID, STATUS_CODE.OP_UNSUPPORTED);
}
},
[REQUEST.READDIR]: (sftp, payload) => {
bufferParser.init(payload, 1);
const reqID = bufferParser.readUInt32BE();
/*
string handle
*/
const handle = bufferParser.readString();
bufferParser.clear();
if (handle === undefined || handle.length > 256)
return doFatalSFTPError(sftp, 'Malformed READDIR packet');
sftp._debug && sftp._debug(`SFTP: Inbound: Received READDIR (id:${reqID})`);
if (!sftp.emit('READDIR', reqID, handle)) {
// Automatically reject request if no handler for request type
sftp.status(reqID, STATUS_CODE.OP_UNSUPPORTED);
}
},
[REQUEST.REMOVE]: (sftp, payload) => {
bufferParser.init(payload, 1);
const reqID = bufferParser.readUInt32BE();
/*
string path
*/
const path = bufferParser.readString(true);
bufferParser.clear();
if (path === undefined)
return doFatalSFTPError(sftp, 'Malformed REMOVE packet');
sftp._debug && sftp._debug(`SFTP: Inbound: Received REMOVE (id:${reqID})`);
if (!sftp.emit('REMOVE', reqID, path)) {
// Automatically reject request if no handler for request type
sftp.status(reqID, STATUS_CODE.OP_UNSUPPORTED);
}
},
[REQUEST.MKDIR]: (sftp, payload) => {
bufferParser.init(payload, 1);
const reqID = bufferParser.readUInt32BE();
/*
string path
ATTRS attrs
*/
const path = bufferParser.readString(true);
const attrs = readAttrs(sftp._biOpt);
bufferParser.clear();
if (attrs === undefined)
return doFatalSFTPError(sftp, 'Malformed MKDIR packet');
sftp._debug && sftp._debug(`SFTP: Inbound: Received MKDIR (id:${reqID})`);
if (!sftp.emit('MKDIR', reqID, path, attrs)) {
// Automatically reject request if no handler for request type
sftp.status(reqID, STATUS_CODE.OP_UNSUPPORTED);
}
},
[REQUEST.RMDIR]: (sftp, payload) => {
bufferParser.init(payload, 1);
const reqID = bufferParser.readUInt32BE();
/*
string path
*/
const path = bufferParser.readString(true);
bufferParser.clear();
if (path === undefined)
return doFatalSFTPError(sftp, 'Malformed RMDIR packet');
sftp._debug && sftp._debug(`SFTP: Inbound: Received RMDIR (id:${reqID})`);
if (!sftp.emit('RMDIR', reqID, path)) {
// Automatically reject request if no handler for request type
sftp.status(reqID, STATUS_CODE.OP_UNSUPPORTED);
}
},
[REQUEST.REALPATH]: (sftp, payload) => {
bufferParser.init(payload, 1);
const reqID = bufferParser.readUInt32BE();
/*
string path
*/
const path = bufferParser.readString(true);
bufferParser.clear();
if (path === undefined)
return doFatalSFTPError(sftp, 'Malformed REALPATH packet');
sftp._debug && sftp._debug(
`SFTP: Inbound: Received REALPATH (id:${reqID})`
);
if (!sftp.emit('REALPATH', reqID, path)) {
// Automatically reject request if no handler for request type
sftp.status(reqID, STATUS_CODE.OP_UNSUPPORTED);
}
},
[REQUEST.STAT]: (sftp, payload) => {
bufferParser.init(payload, 1);
const reqID = bufferParser.readUInt32BE();
/*
string path
*/
const path = bufferParser.readString(true);
bufferParser.clear();
if (path === undefined)
return doFatalSFTPError(sftp, 'Malformed STAT packet');
sftp._debug && sftp._debug(`SFTP: Inbound: Received STAT (id:${reqID})`);
if (!sftp.emit('STAT', reqID, path)) {
// Automatically reject request if no handler for request type
sftp.status(reqID, STATUS_CODE.OP_UNSUPPORTED);
}
},
[REQUEST.RENAME]: (sftp, payload) => {
bufferParser.init(payload, 1);
const reqID = bufferParser.readUInt32BE();
/*
string oldpath
string newpath
*/
const oldPath = bufferParser.readString(true);
const newPath = bufferParser.readString(true);
bufferParser.clear();
if (newPath === undefined)
return doFatalSFTPError(sftp, 'Malformed RENAME packet');
sftp._debug && sftp._debug(`SFTP: Inbound: Received RENAME (id:${reqID})`);
if (!sftp.emit('RENAME', reqID, oldPath, newPath)) {
// Automatically reject request if no handler for request type
sftp.status(reqID, STATUS_CODE.OP_UNSUPPORTED);
}
},
[REQUEST.READLINK]: (sftp, payload) => {
bufferParser.init(payload, 1);
const reqID = bufferParser.readUInt32BE();
/*
string path
*/
const path = bufferParser.readString(true);
bufferParser.clear();
if (path === undefined)
return doFatalSFTPError(sftp, 'Malformed READLINK packet');
sftp._debug && sftp._debug(
`SFTP: Inbound: Received READLINK (id:${reqID})`
);
if (!sftp.emit('READLINK', reqID, path)) {
// Automatically reject request if no handler for request type
sftp.status(reqID, STATUS_CODE.OP_UNSUPPORTED);
}
},
[REQUEST.SYMLINK]: (sftp, payload) => {
bufferParser.init(payload, 1);
const reqID = bufferParser.readUInt32BE();
/*
string linkpath
string targetpath
*/
const linkPath = bufferParser.readString(true);
const targetPath = bufferParser.readString(true);
bufferParser.clear();
if (targetPath === undefined)
return doFatalSFTPError(sftp, 'Malformed SYMLINK packet');
sftp._debug && sftp._debug(`SFTP: Inbound: Received SYMLINK (id:${reqID})`);
let handled;
if (sftp._isOpenSSH) {
// OpenSSH has linkpath and targetpath positions switched
handled = sftp.emit('SYMLINK', reqID, targetPath, linkPath);
} else {
handled = sftp.emit('SYMLINK', reqID, linkPath, targetPath);
}
if (!handled) {
// Automatically reject request if no handler for request type
sftp.status(reqID, STATUS_CODE.OP_UNSUPPORTED);
}
},
[REQUEST.EXTENDED]: (sftp, payload) => {
bufferParser.init(payload, 1);
const reqID = bufferParser.readUInt32BE();
/*
string extended-request
... any request-specific data ...
*/
const extName = bufferParser.readString(true);
if (extName === undefined) {
bufferParser.clear();
return doFatalSFTPError(sftp, 'Malformed EXTENDED packet');
}
let extData;
if (bufferParser.avail())
extData = bufferParser.readRaw();
bufferParser.clear();
sftp._debug && sftp._debug(
`SFTP: Inbound: Received EXTENDED (id:${reqID})`
);
if (!sftp.emit('EXTENDED', reqID, extName, extData)) {
// Automatically reject request if no handler for request type
sftp.status(reqID, STATUS_CODE.OP_UNSUPPORTED);
}
},
};
// =============================================================================
// ReadStream/WriteStream-related ==============================================
// =============================================================================
const {
ERR_INVALID_ARG_TYPE,
ERR_OUT_OF_RANGE,
validateNumber
} = require('./node-fs-compat');
const kMinPoolSpace = 128;
let pool;
// It can happen that we expect to read a large chunk of data, and reserve
// a large chunk of the pool accordingly, but the read() call only filled
// a portion of it. If a concurrently executing read() then uses the same pool,
// the "reserved" portion cannot be used, so we allow it to be re-used as a
// new pool later.
const poolFragments = [];
function allocNewPool(poolSize) {
if (poolFragments.length > 0)
pool = poolFragments.pop();
else
pool = Buffer.allocUnsafe(poolSize);
pool.used = 0;
}
// Check the `this.start` and `this.end` of stream.
function checkPosition(pos, name) {
if (!Number.isSafeInteger(pos)) {
validateNumber(pos, name);
if (!Number.isInteger(pos))
throw new ERR_OUT_OF_RANGE(name, 'an integer', pos);
throw new ERR_OUT_OF_RANGE(name, '>= 0 and <= 2 ** 53 - 1', pos);
}
if (pos < 0)
throw new ERR_OUT_OF_RANGE(name, '>= 0 and <= 2 ** 53 - 1', pos);
}
function roundUpToMultipleOf8(n) {
return (n + 7) & ~7; // Align to 8 byte boundary.
}
function ReadStream(sftp, path, options) {
if (options === undefined)
options = {};
else if (typeof options === 'string')
options = { encoding: options };
else if (options === null || typeof options !== 'object')
throw new TypeError('"options" argument must be a string or an object');
else
options = Object.create(options);
// A little bit bigger buffer and water marks by default
if (options.highWaterMark === undefined)
options.highWaterMark = 64 * 1024;
// For backwards compat do not emit close on destroy.
options.emitClose = false;
options.autoDestroy = false; // Node 14 major change.
ReadableStream.call(this, options);
this.path = path;
this.flags = options.flags === undefined ? 'r' : options.flags;
this.mode = options.mode === undefined ? 0o666 : options.mode;
this.start = options.start;
this.end = options.end;
this.autoClose = options.autoClose === undefined ? true : options.autoClose;
this.pos = 0;
this.bytesRead = 0;
this.isClosed = false;
this.handle = options.handle === undefined ? null : options.handle;
this.sftp = sftp;
this._opening = false;
if (this.start !== undefined) {
checkPosition(this.start, 'start');
this.pos = this.start;
}
if (this.end === undefined) {
this.end = Infinity;
} else if (this.end !== Infinity) {
checkPosition(this.end, 'end');
if (this.start !== undefined && this.start > this.end) {
throw new ERR_OUT_OF_RANGE(
'start',
`<= "end" (here: ${this.end})`,
this.start
);
}
}
this.on('end', function() {
if (this.autoClose)
this.destroy();
});
if (!Buffer.isBuffer(this.handle))
this.open();
}
inherits(ReadStream, ReadableStream);
ReadStream.prototype.open = function() {
if (this._opening)
return;
this._opening = true;
this.sftp.open(this.path, this.flags, this.mode, (er, handle) => {
this._opening = false;
if (er) {
this.emit('error', er);
if (this.autoClose)
this.destroy();
return;
}
this.handle = handle;
this.emit('open', handle);
this.emit('ready');
// Start the flow of data.
this.read();
});
};
ReadStream.prototype._read = function(n) {
if (!Buffer.isBuffer(this.handle))
return this.once('open', () => this._read(n));
// XXX: safe to remove this?
if (this.destroyed)
return;
if (!pool || pool.length - pool.used < kMinPoolSpace) {
// Discard the old pool.
allocNewPool(this.readableHighWaterMark
|| this._readableState.highWaterMark);
}
// Grab another reference to the pool in the case that while we're
// in the thread pool another read() finishes up the pool, and
// allocates a new one.
const thisPool = pool;
let toRead = Math.min(pool.length - pool.used, n);
const start = pool.used;
if (this.end !== undefined)
toRead = Math.min(this.end - this.pos + 1, toRead);
// Already read everything we were supposed to read!
// treat as EOF.
if (toRead <= 0)
return this.push(null);
// the actual read.
this.sftp.read(this.handle,
pool,
pool.used,
toRead,
this.pos,
(er, bytesRead) => {
if (er) {
this.emit('error', er);
if (this.autoClose)
this.destroy();
return;
}
let b = null;
// Now that we know how much data we have actually read, re-wind the
// 'used' field if we can, and otherwise allow the remainder of our
// reservation to be used as a new pool later.
if (start + toRead === thisPool.used && thisPool === pool) {
thisPool.used = roundUpToMultipleOf8(thisPool.used + bytesRead - toRead);
} else {
// Round down to the next lowest multiple of 8 to ensure the new pool
// fragment start and end positions are aligned to an 8 byte boundary.
const alignedEnd = (start + toRead) & ~7;
const alignedStart = roundUpToMultipleOf8(start + bytesRead);
if (alignedEnd - alignedStart >= kMinPoolSpace)
poolFragments.push(thisPool.slice(alignedStart, alignedEnd));
}
if (bytesRead > 0) {
this.bytesRead += bytesRead;
b = thisPool.slice(start, start + bytesRead);
}
// Move the pool positions, and internal position for reading.
this.pos += bytesRead;
this.push(b);
});
pool.used = roundUpToMultipleOf8(pool.used + toRead);
};
ReadStream.prototype._destroy = function(err, cb) {
if (this._opening && !Buffer.isBuffer(this.handle)) {
this.once('open', closeStream.bind(null, this, cb, err));
return;
}
closeStream(this, cb, err);
this.handle = null;
this._opening = false;
};
function closeStream(stream, cb, err) {
if (!stream.handle)
return onclose();
stream.sftp.close(stream.handle, onclose);
function onclose(er) {
er = er || err;
cb(er);
stream.isClosed = true;
if (!er)
stream.emit('close');
}
}
ReadStream.prototype.close = function(cb) {
this.destroy(null, cb);
};
Object.defineProperty(ReadStream.prototype, 'pending', {
get() {
return this.handle === null;
},
configurable: true
});
// TODO: add `concurrency` setting to allow more than one in-flight WRITE
// request to server to improve throughput
function WriteStream(sftp, path, options) {
if (options === undefined)
options = {};
else if (typeof options === 'string')
options = { encoding: options };
else if (options === null || typeof options !== 'object')
throw new TypeError('"options" argument must be a string or an object');
else
options = Object.create(options);
// For backwards compat do not emit close on destroy.
options.emitClose = false;
options.autoDestroy = false; // Node 14 major change.
WritableStream.call(this, options);
this.path = path;
this.flags = options.flags === undefined ? 'w' : options.flags;
this.mode = options.mode === undefined ? 0o666 : options.mode;
this.start = options.start;
this.autoClose = options.autoClose === undefined ? true : options.autoClose;
this.pos = 0;
this.bytesWritten = 0;
this.isClosed = false;
this.handle = options.handle === undefined ? null : options.handle;
this.sftp = sftp;
this._opening = false;
if (this.start !== undefined) {
checkPosition(this.start, 'start');
this.pos = this.start;
}
if (options.encoding)
this.setDefaultEncoding(options.encoding);
// Node v6.x only
this.on('finish', function() {
if (this._writableState.finalCalled)
return;
if (this.autoClose)
this.destroy();
});
if (!Buffer.isBuffer(this.handle))
this.open();
}
inherits(WriteStream, WritableStream);
WriteStream.prototype._final = function(cb) {
if (this.autoClose)
this.destroy();
cb();
};
WriteStream.prototype.open = function() {
if (this._opening)
return;
this._opening = true;
this.sftp.open(this.path, this.flags, this.mode, (er, handle) => {
this._opening = false;
if (er) {
this.emit('error', er);
if (this.autoClose)
this.destroy();
return;
}
this.handle = handle;
const tryAgain = (err) => {
if (err) {
// Try chmod() for sftp servers that may not support fchmod() for
// whatever reason
this.sftp.chmod(this.path, this.mode, (err_) => tryAgain());
return;
}
// SFTPv3 requires absolute offsets, no matter the open flag used
if (this.flags[0] === 'a') {
const tryStat = (err, st) => {
if (err) {
// Try stat() for sftp servers that may not support fstat() for
// whatever reason
this.sftp.stat(this.path, (err_, st_) => {
if (err_) {
this.destroy();
this.emit('error', err);
return;
}
tryStat(null, st_);
});
return;
}
this.pos = st.size;
this.emit('open', handle);
this.emit('ready');
};
this.sftp.fstat(handle, tryStat);
return;
}
this.emit('open', handle);
this.emit('ready');
};
this.sftp.fchmod(handle, this.mode, tryAgain);
});
};
WriteStream.prototype._write = function(data, encoding, cb) {
if (!Buffer.isBuffer(data)) {
const err = new ERR_INVALID_ARG_TYPE('data', 'Buffer', data);
return this.emit('error', err);
}
if (!Buffer.isBuffer(this.handle)) {
return this.once('open', function() {
this._write(data, encoding, cb);
});
}
this.sftp.write(this.handle,
data,
0,
data.length,
this.pos,
(er, bytes) => {
if (er) {
if (this.autoClose)
this.destroy();
return cb(er);
}
this.bytesWritten += bytes;
cb();
});
this.pos += data.length;
};
WriteStream.prototype._writev = function(data, cb) {
if (!Buffer.isBuffer(this.handle)) {
return this.once('open', function() {
this._writev(data, cb);
});
}
const sftp = this.sftp;
const handle = this.handle;
let writesLeft = data.length;
const onwrite = (er, bytes) => {
if (er) {
this.destroy();
return cb(er);
}
this.bytesWritten += bytes;
if (--writesLeft === 0)
cb();
};
// TODO: try to combine chunks to reduce number of requests to the server?
for (let i = 0; i < data.length; ++i) {
const chunk = data[i].chunk;
sftp.write(handle, chunk, 0, chunk.length, this.pos, onwrite);
this.pos += chunk.length;
}
};
if (typeof WritableStream.prototype.destroy !== 'function')
WriteStream.prototype.destroy = ReadStream.prototype.destroy;
WriteStream.prototype._destroy = ReadStream.prototype._destroy;
WriteStream.prototype.close = function(cb) {
if (cb) {
if (this.isClosed) {
process.nextTick(cb);
return;
}
this.on('close', cb);
}
// If we are not autoClosing, we should call
// destroy on 'finish'.
if (!this.autoClose)
this.on('finish', this.destroy.bind(this));
this.end();
};
// There is no shutdown() for files.
WriteStream.prototype.destroySoon = WriteStream.prototype.end;
Object.defineProperty(WriteStream.prototype, 'pending', {
get() {
return this.handle === null;
},
configurable: true
});
// =============================================================================
module.exports = {
flagsToString,
OPEN_MODE,
SFTP,
Stats,
STATUS_CODE,
stringToFlags,
};