ssh2-custom
Version:
A custom package for the ssh2 library with pre-built native modules.
1,803 lines (1,539 loc) • 114 kB
JavaScript
'use strict';
const EventEmitter = require('node:events');
const fs = require('node:fs');
const { constants } = fs;
const {
Readable: ReadableStream,
Writable: WritableStream
} = require('node:stream');
const { inherits, isDate } = require('node:util');
const FastBuffer = Buffer[Symbol.species];
const {
bufferCopy,
bufferSlice,
makeBufferParser,
writeUInt32BE,
} = require(__nodeModulesDir + 'ssh2/lib/protocol/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 re