Files
bare-operating-system/packages/bare-os-openssh/vendor/bare-ssh2/lib/protocol/SFTP.js
T
Raven Scott baae7b3482 VFS readFile often returns Uint8Array; SFTP DATA and CHANNEL_DATA assumed
Buffer.isBuffer or treated payloads as UTF-8, corrupting packets and risking
native aborts (bad_optional_access). Normalize ArrayBuffer views in
SFTP.prototype.data and Protocol channelData/channelExtData; align READ shim.

Also includes earlier SFTP hardening: REQUEST.READ/WRITE/FSETSTAT guards,
enqueueSftpReply(reqid) + FAILURE on handler error, CHANGELOG.
2026-04-21 18:06:22 -04:00

3682 lines
107 KiB
JavaScript

'use strict'
const EventEmitter = require('bare-node-events')
const fs = require('bare-node-fs')
const { constants } = fs
const { Readable: ReadableStream, Writable: WritableStream } = require('bare-node-stream')
const {
inherits,
types: { isDate }
} = require('bare-node-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['[email protected]']
if (!ext || ext !== '1') throw new Error('Server does not support this extended request')
/*
uint32 id
string "[email protected]"
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('[email protected]', (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} [email protected]`)
}
}
ext_openssh_statvfs(path, cb) {
if (this.server) throw new Error('Client-only method called in server mode')
const ext = this._extensions['[email protected]']
if (!ext || ext !== '2') throw new Error('Server does not support this extended request')
/*
uint32 id
string "[email protected]"
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('[email protected]', (p += 4), 19)
writeUInt32BE(buf, pathLen, (p += 19))
buf.utf8Write(path, (p += 4), pathLen)
this._requests[reqid] = { extended: '[email protected]', cb }
const isBuffered = sendOrBuffer(this, buf)
if (this._debug) {
const which = isBuffered ? 'Buffered' : 'Sending'
this._debug(`SFTP: Outbound: ${which} [email protected]`)
}
}
ext_openssh_fstatvfs(handle, cb) {
if (this.server) throw new Error('Client-only method called in server mode')
const ext = this._extensions['[email protected]']
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 "[email protected]"
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('[email protected]', (p += 4), 20)
writeUInt32BE(buf, handleLen, (p += 20))
buf.set(handle, (p += 4))
this._requests[reqid] = { extended: '[email protected]', cb }
const isBuffered = sendOrBuffer(this, buf)
if (this._debug) {
const which = isBuffered ? 'Buffered' : 'Sending'
this._debug(`SFTP: Outbound: ${which} [email protected]`)
}
}
ext_openssh_hardlink(oldPath, newPath, cb) {
if (this.server) throw new Error('Client-only method called in server mode')
const ext = this._extensions['[email protected]']
if (ext !== '1') throw new Error('Server does not support this extended request')
/*
uint32 id
string "[email protected]"
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('[email protected]', (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} [email protected]`)
}
}
ext_openssh_fsync(handle, cb) {
if (this.server) throw new Error('Client-only method called in server mode')
const ext = this._extensions['[email protected]']
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 "[email protected]"
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('[email protected]', (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'} [email protected]`)
}
ext_openssh_lsetstat(path, attrs, cb) {
if (this.server) throw new Error('Client-only method called in server mode')
const ext = this._extensions['[email protected]']
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 "[email protected]"
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('[email protected]', (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} [email protected]`)
}
}
ext_openssh_expandPath(path, cb) {
if (this.server) throw new Error('Client-only method called in server mode')
const ext = this._extensions['[email protected]']
if (ext !== '1') throw new Error('Server does not support this extended request')
/*
uint32 id
string "[email protected]"
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('[email protected]', (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} [email protected]`)
}
}
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['[email protected]']
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 "[email protected]"
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('[email protected]', 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: '[email protected]', cb }
const isBuffered = sendOrBuffer(this, buf)
if (this._debug) {
const status = isBuffered ? 'Buffered' : 'Sending'
this._debug(`SFTP: Outbound: ${status} [email protected]`)
}
}
// ===========================================================================
// 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')
// VFS readFile often returns Uint8Array; subarray() stays a typed view. Without this,
// !Buffer.isBuffer leads to utf8Write/binary confusion or throw — SFTP DATA must be raw bytes.
if (ArrayBuffer.isView(data) && !Buffer.isBuffer(data)) {
data = Buffer.from(data.buffer, data.byteOffset, data.byteLength)
}
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 "[email protected]"
*/
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('[email protected]', (p += 4), 18)
sftp._requests[reqid] = { extended: '[email protected]', cb }
const isBuffered = sendOrBuffer(sftp, buf)
if (sftp._debug) {
const which = isBuffered ? 'Buffered' : 'Sending'
sftp._debug(`SFTP: Outbound: ${which} [email protected]`)
}
}
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['[email protected]'] === '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 '[email protected]':
case '[email protected]': {
/*
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 '[email protected]': {
/*
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 '[email protected]': {
/*
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 (
handle === undefined ||
offset === undefined ||
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 === undefined ||
offset === 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 === 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
}