3662 lines
106 KiB
JavaScript
3662 lines
106 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')
|
|
|
|
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 (len === undefined || handle.length > 256)
|
|
return doFatalSFTPError(sftp, 'Malformed READ packet')
|
|
|
|
sftp._debug && sftp._debug(`SFTP: Inbound: Received READ (id:${reqID})`)
|
|
|
|
if (!sftp.emit('READ', reqID, handle, offset, len)) {
|
|
// Automatically reject request if no handler for request type
|
|
sftp.status(reqID, STATUS_CODE.OP_UNSUPPORTED)
|
|
}
|
|
},
|
|
[REQUEST.WRITE]: (sftp, payload) => {
|
|
bufferParser.init(payload, 1)
|
|
const reqID = bufferParser.readUInt32BE()
|
|
/*
|
|
string handle
|
|
uint64 offset
|
|
string data
|
|
*/
|
|
const handle = bufferParser.readString()
|
|
const offset = bufferParser.readUInt64BE(sftp._biOpt)
|
|
const data = bufferParser.readString()
|
|
bufferParser.clear()
|
|
|
|
if (data === undefined || handle.length > 256)
|
|
return doFatalSFTPError(sftp, 'Malformed WRITE packet')
|
|
|
|
sftp._debug && sftp._debug(`SFTP: Inbound: Received WRITE (id:${reqID})`)
|
|
|
|
if (!sftp.emit('WRITE', reqID, handle, offset, data)) {
|
|
// Automatically reject request if no handler for request type
|
|
sftp.status(reqID, STATUS_CODE.OP_UNSUPPORTED)
|
|
}
|
|
},
|
|
[REQUEST.LSTAT]: (sftp, payload) => {
|
|
bufferParser.init(payload, 1)
|
|
const reqID = bufferParser.readUInt32BE()
|
|
/*
|
|
string path
|
|
*/
|
|
const path = bufferParser.readString(true)
|
|
bufferParser.clear()
|
|
|
|
if (path === undefined) return doFatalSFTPError(sftp, 'Malformed LSTAT packet')
|
|
|
|
sftp._debug && sftp._debug(`SFTP: Inbound: Received LSTAT (id:${reqID})`)
|
|
|
|
if (!sftp.emit('LSTAT', reqID, path)) {
|
|
// Automatically reject request if no handler for request type
|
|
sftp.status(reqID, STATUS_CODE.OP_UNSUPPORTED)
|
|
}
|
|
},
|
|
[REQUEST.FSTAT]: (sftp, payload) => {
|
|
bufferParser.init(payload, 1)
|
|
const reqID = bufferParser.readUInt32BE()
|
|
/*
|
|
string handle
|
|
*/
|
|
const handle = bufferParser.readString()
|
|
bufferParser.clear()
|
|
|
|
if (handle === undefined || handle.length > 256)
|
|
return doFatalSFTPError(sftp, 'Malformed FSTAT packet')
|
|
|
|
sftp._debug && sftp._debug(`SFTP: Inbound: Received FSTAT (id:${reqID})`)
|
|
|
|
if (!sftp.emit('FSTAT', reqID, handle)) {
|
|
// Automatically reject request if no handler for request type
|
|
sftp.status(reqID, STATUS_CODE.OP_UNSUPPORTED)
|
|
}
|
|
},
|
|
[REQUEST.SETSTAT]: (sftp, payload) => {
|
|
bufferParser.init(payload, 1)
|
|
const reqID = bufferParser.readUInt32BE()
|
|
/*
|
|
string path
|
|
ATTRS attrs
|
|
*/
|
|
const path = bufferParser.readString(true)
|
|
const attrs = readAttrs(sftp._biOpt)
|
|
bufferParser.clear()
|
|
|
|
if (attrs === undefined) return doFatalSFTPError(sftp, 'Malformed SETSTAT packet')
|
|
|
|
sftp._debug && sftp._debug(`SFTP: Inbound: Received SETSTAT (id:${reqID})`)
|
|
|
|
if (!sftp.emit('SETSTAT', reqID, path, attrs)) {
|
|
// Automatically reject request if no handler for request type
|
|
sftp.status(reqID, STATUS_CODE.OP_UNSUPPORTED)
|
|
}
|
|
},
|
|
[REQUEST.FSETSTAT]: (sftp, payload) => {
|
|
bufferParser.init(payload, 1)
|
|
const reqID = bufferParser.readUInt32BE()
|
|
/*
|
|
string handle
|
|
ATTRS attrs
|
|
*/
|
|
const handle = bufferParser.readString()
|
|
const attrs = readAttrs(sftp._biOpt)
|
|
bufferParser.clear()
|
|
|
|
if (attrs === undefined || handle.length > 256)
|
|
return doFatalSFTPError(sftp, 'Malformed FSETSTAT packet')
|
|
|
|
sftp._debug && sftp._debug(`SFTP: Inbound: Received FSETSTAT (id:${reqID})`)
|
|
|
|
if (!sftp.emit('FSETSTAT', reqID, handle, attrs)) {
|
|
// Automatically reject request if no handler for request type
|
|
sftp.status(reqID, STATUS_CODE.OP_UNSUPPORTED)
|
|
}
|
|
},
|
|
[REQUEST.OPENDIR]: (sftp, payload) => {
|
|
bufferParser.init(payload, 1)
|
|
const reqID = bufferParser.readUInt32BE()
|
|
/*
|
|
string path
|
|
*/
|
|
const path = bufferParser.readString(true)
|
|
bufferParser.clear()
|
|
|
|
if (path === undefined) return doFatalSFTPError(sftp, 'Malformed OPENDIR packet')
|
|
|
|
sftp._debug && sftp._debug(`SFTP: Inbound: Received OPENDIR (id:${reqID})`)
|
|
|
|
if (!sftp.emit('OPENDIR', reqID, path)) {
|
|
// Automatically reject request if no handler for request type
|
|
sftp.status(reqID, STATUS_CODE.OP_UNSUPPORTED)
|
|
}
|
|
},
|
|
[REQUEST.READDIR]: (sftp, payload) => {
|
|
bufferParser.init(payload, 1)
|
|
const reqID = bufferParser.readUInt32BE()
|
|
/*
|
|
string handle
|
|
*/
|
|
const handle = bufferParser.readString()
|
|
bufferParser.clear()
|
|
|
|
if (handle === undefined || handle.length > 256)
|
|
return doFatalSFTPError(sftp, 'Malformed READDIR packet')
|
|
|
|
sftp._debug && sftp._debug(`SFTP: Inbound: Received READDIR (id:${reqID})`)
|
|
|
|
if (!sftp.emit('READDIR', reqID, handle)) {
|
|
// Automatically reject request if no handler for request type
|
|
sftp.status(reqID, STATUS_CODE.OP_UNSUPPORTED)
|
|
}
|
|
},
|
|
[REQUEST.REMOVE]: (sftp, payload) => {
|
|
bufferParser.init(payload, 1)
|
|
const reqID = bufferParser.readUInt32BE()
|
|
/*
|
|
string path
|
|
*/
|
|
const path = bufferParser.readString(true)
|
|
bufferParser.clear()
|
|
|
|
if (path === undefined) return doFatalSFTPError(sftp, 'Malformed REMOVE packet')
|
|
|
|
sftp._debug && sftp._debug(`SFTP: Inbound: Received REMOVE (id:${reqID})`)
|
|
|
|
if (!sftp.emit('REMOVE', reqID, path)) {
|
|
// Automatically reject request if no handler for request type
|
|
sftp.status(reqID, STATUS_CODE.OP_UNSUPPORTED)
|
|
}
|
|
},
|
|
[REQUEST.MKDIR]: (sftp, payload) => {
|
|
bufferParser.init(payload, 1)
|
|
const reqID = bufferParser.readUInt32BE()
|
|
/*
|
|
string path
|
|
ATTRS attrs
|
|
*/
|
|
const path = bufferParser.readString(true)
|
|
const attrs = readAttrs(sftp._biOpt)
|
|
bufferParser.clear()
|
|
|
|
if (attrs === undefined) return doFatalSFTPError(sftp, 'Malformed MKDIR packet')
|
|
|
|
sftp._debug && sftp._debug(`SFTP: Inbound: Received MKDIR (id:${reqID})`)
|
|
|
|
if (!sftp.emit('MKDIR', reqID, path, attrs)) {
|
|
// Automatically reject request if no handler for request type
|
|
sftp.status(reqID, STATUS_CODE.OP_UNSUPPORTED)
|
|
}
|
|
},
|
|
[REQUEST.RMDIR]: (sftp, payload) => {
|
|
bufferParser.init(payload, 1)
|
|
const reqID = bufferParser.readUInt32BE()
|
|
/*
|
|
string path
|
|
*/
|
|
const path = bufferParser.readString(true)
|
|
bufferParser.clear()
|
|
|
|
if (path === undefined) return doFatalSFTPError(sftp, 'Malformed RMDIR packet')
|
|
|
|
sftp._debug && sftp._debug(`SFTP: Inbound: Received RMDIR (id:${reqID})`)
|
|
|
|
if (!sftp.emit('RMDIR', reqID, path)) {
|
|
// Automatically reject request if no handler for request type
|
|
sftp.status(reqID, STATUS_CODE.OP_UNSUPPORTED)
|
|
}
|
|
},
|
|
[REQUEST.REALPATH]: (sftp, payload) => {
|
|
bufferParser.init(payload, 1)
|
|
const reqID = bufferParser.readUInt32BE()
|
|
/*
|
|
string path
|
|
*/
|
|
const path = bufferParser.readString(true)
|
|
bufferParser.clear()
|
|
|
|
if (path === undefined) return doFatalSFTPError(sftp, 'Malformed REALPATH packet')
|
|
|
|
sftp._debug && sftp._debug(`SFTP: Inbound: Received REALPATH (id:${reqID})`)
|
|
|
|
if (!sftp.emit('REALPATH', reqID, path)) {
|
|
// Automatically reject request if no handler for request type
|
|
sftp.status(reqID, STATUS_CODE.OP_UNSUPPORTED)
|
|
}
|
|
},
|
|
[REQUEST.STAT]: (sftp, payload) => {
|
|
bufferParser.init(payload, 1)
|
|
const reqID = bufferParser.readUInt32BE()
|
|
/*
|
|
string path
|
|
*/
|
|
const path = bufferParser.readString(true)
|
|
bufferParser.clear()
|
|
|
|
if (path === undefined) return doFatalSFTPError(sftp, 'Malformed STAT packet')
|
|
|
|
sftp._debug && sftp._debug(`SFTP: Inbound: Received STAT (id:${reqID})`)
|
|
|
|
if (!sftp.emit('STAT', reqID, path)) {
|
|
// Automatically reject request if no handler for request type
|
|
sftp.status(reqID, STATUS_CODE.OP_UNSUPPORTED)
|
|
}
|
|
},
|
|
[REQUEST.RENAME]: (sftp, payload) => {
|
|
bufferParser.init(payload, 1)
|
|
const reqID = bufferParser.readUInt32BE()
|
|
/*
|
|
string oldpath
|
|
string newpath
|
|
*/
|
|
const oldPath = bufferParser.readString(true)
|
|
const newPath = bufferParser.readString(true)
|
|
bufferParser.clear()
|
|
|
|
if (newPath === undefined) return doFatalSFTPError(sftp, 'Malformed RENAME packet')
|
|
|
|
sftp._debug && sftp._debug(`SFTP: Inbound: Received RENAME (id:${reqID})`)
|
|
|
|
if (!sftp.emit('RENAME', reqID, oldPath, newPath)) {
|
|
// Automatically reject request if no handler for request type
|
|
sftp.status(reqID, STATUS_CODE.OP_UNSUPPORTED)
|
|
}
|
|
},
|
|
[REQUEST.READLINK]: (sftp, payload) => {
|
|
bufferParser.init(payload, 1)
|
|
const reqID = bufferParser.readUInt32BE()
|
|
/*
|
|
string path
|
|
*/
|
|
const path = bufferParser.readString(true)
|
|
bufferParser.clear()
|
|
|
|
if (path === undefined) return doFatalSFTPError(sftp, 'Malformed READLINK packet')
|
|
|
|
sftp._debug && sftp._debug(`SFTP: Inbound: Received READLINK (id:${reqID})`)
|
|
|
|
if (!sftp.emit('READLINK', reqID, path)) {
|
|
// Automatically reject request if no handler for request type
|
|
sftp.status(reqID, STATUS_CODE.OP_UNSUPPORTED)
|
|
}
|
|
},
|
|
[REQUEST.SYMLINK]: (sftp, payload) => {
|
|
bufferParser.init(payload, 1)
|
|
const reqID = bufferParser.readUInt32BE()
|
|
/*
|
|
string linkpath
|
|
string targetpath
|
|
*/
|
|
const linkPath = bufferParser.readString(true)
|
|
const targetPath = bufferParser.readString(true)
|
|
bufferParser.clear()
|
|
|
|
if (targetPath === undefined) return doFatalSFTPError(sftp, 'Malformed SYMLINK packet')
|
|
|
|
sftp._debug && sftp._debug(`SFTP: Inbound: Received SYMLINK (id:${reqID})`)
|
|
|
|
let handled
|
|
if (sftp._isOpenSSH) {
|
|
// OpenSSH has linkpath and targetpath positions switched
|
|
handled = sftp.emit('SYMLINK', reqID, targetPath, linkPath)
|
|
} else {
|
|
handled = sftp.emit('SYMLINK', reqID, linkPath, targetPath)
|
|
}
|
|
if (!handled) {
|
|
// Automatically reject request if no handler for request type
|
|
sftp.status(reqID, STATUS_CODE.OP_UNSUPPORTED)
|
|
}
|
|
},
|
|
[REQUEST.EXTENDED]: (sftp, payload) => {
|
|
bufferParser.init(payload, 1)
|
|
const reqID = bufferParser.readUInt32BE()
|
|
/*
|
|
string extended-request
|
|
... any request-specific data ...
|
|
*/
|
|
const extName = bufferParser.readString(true)
|
|
if (extName === undefined) {
|
|
bufferParser.clear()
|
|
return doFatalSFTPError(sftp, 'Malformed EXTENDED packet')
|
|
}
|
|
|
|
let extData
|
|
if (bufferParser.avail()) extData = bufferParser.readRaw()
|
|
bufferParser.clear()
|
|
|
|
sftp._debug && sftp._debug(`SFTP: Inbound: Received EXTENDED (id:${reqID})`)
|
|
|
|
if (!sftp.emit('EXTENDED', reqID, extName, extData)) {
|
|
// Automatically reject request if no handler for request type
|
|
sftp.status(reqID, STATUS_CODE.OP_UNSUPPORTED)
|
|
}
|
|
}
|
|
}
|
|
|
|
// =============================================================================
|
|
// ReadStream/WriteStream-related ==============================================
|
|
// =============================================================================
|
|
const { ERR_INVALID_ARG_TYPE, ERR_OUT_OF_RANGE, validateNumber } = require('./node-fs-compat')
|
|
|
|
const kMinPoolSpace = 128
|
|
|
|
let pool
|
|
// It can happen that we expect to read a large chunk of data, and reserve
|
|
// a large chunk of the pool accordingly, but the read() call only filled
|
|
// a portion of it. If a concurrently executing read() then uses the same pool,
|
|
// the "reserved" portion cannot be used, so we allow it to be re-used as a
|
|
// new pool later.
|
|
const poolFragments = []
|
|
|
|
function allocNewPool(poolSize) {
|
|
if (poolFragments.length > 0) pool = poolFragments.pop()
|
|
else pool = Buffer.allocUnsafe(poolSize)
|
|
pool.used = 0
|
|
}
|
|
|
|
// Check the `this.start` and `this.end` of stream.
|
|
function checkPosition(pos, name) {
|
|
if (!Number.isSafeInteger(pos)) {
|
|
validateNumber(pos, name)
|
|
if (!Number.isInteger(pos)) throw new ERR_OUT_OF_RANGE(name, 'an integer', pos)
|
|
throw new ERR_OUT_OF_RANGE(name, '>= 0 and <= 2 ** 53 - 1', pos)
|
|
}
|
|
if (pos < 0) throw new ERR_OUT_OF_RANGE(name, '>= 0 and <= 2 ** 53 - 1', pos)
|
|
}
|
|
|
|
function roundUpToMultipleOf8(n) {
|
|
return (n + 7) & ~7 // Align to 8 byte boundary.
|
|
}
|
|
|
|
function ReadStream(sftp, path, options) {
|
|
if (options === undefined) options = {}
|
|
else if (typeof options === 'string') options = { encoding: options }
|
|
else if (options === null || typeof options !== 'object')
|
|
throw new TypeError('"options" argument must be a string or an object')
|
|
else options = Object.create(options)
|
|
|
|
// A little bit bigger buffer and water marks by default
|
|
if (options.highWaterMark === undefined) options.highWaterMark = 64 * 1024
|
|
|
|
// For backwards compat do not emit close on destroy.
|
|
options.emitClose = false
|
|
options.autoDestroy = false // Node 14 major change.
|
|
|
|
ReadableStream.call(this, options)
|
|
|
|
this.path = path
|
|
this.flags = options.flags === undefined ? 'r' : options.flags
|
|
this.mode = options.mode === undefined ? 0o666 : options.mode
|
|
|
|
this.start = options.start
|
|
this.end = options.end
|
|
this.autoClose = options.autoClose === undefined ? true : options.autoClose
|
|
this.pos = 0
|
|
this.bytesRead = 0
|
|
this.isClosed = false
|
|
|
|
this.handle = options.handle === undefined ? null : options.handle
|
|
this.sftp = sftp
|
|
this._opening = false
|
|
|
|
if (this.start !== undefined) {
|
|
checkPosition(this.start, 'start')
|
|
|
|
this.pos = this.start
|
|
}
|
|
|
|
if (this.end === undefined) {
|
|
this.end = Infinity
|
|
} else if (this.end !== Infinity) {
|
|
checkPosition(this.end, 'end')
|
|
|
|
if (this.start !== undefined && this.start > this.end) {
|
|
throw new ERR_OUT_OF_RANGE('start', `<= "end" (here: ${this.end})`, this.start)
|
|
}
|
|
}
|
|
|
|
this.on('end', function () {
|
|
if (this.autoClose) this.destroy()
|
|
})
|
|
|
|
if (!Buffer.isBuffer(this.handle)) this.open()
|
|
}
|
|
inherits(ReadStream, ReadableStream)
|
|
|
|
ReadStream.prototype.open = function () {
|
|
if (this._opening) return
|
|
|
|
this._opening = true
|
|
|
|
this.sftp.open(this.path, this.flags, this.mode, (er, handle) => {
|
|
this._opening = false
|
|
|
|
if (er) {
|
|
this.emit('error', er)
|
|
if (this.autoClose) this.destroy()
|
|
return
|
|
}
|
|
|
|
this.handle = handle
|
|
this.emit('open', handle)
|
|
this.emit('ready')
|
|
// Start the flow of data.
|
|
this.read()
|
|
})
|
|
}
|
|
|
|
ReadStream.prototype._read = function (n) {
|
|
if (!Buffer.isBuffer(this.handle)) return this.once('open', () => this._read(n))
|
|
|
|
// XXX: safe to remove this?
|
|
if (this.destroyed) return
|
|
|
|
if (!pool || pool.length - pool.used < kMinPoolSpace) {
|
|
// Discard the old pool.
|
|
allocNewPool(this.readableHighWaterMark || this._readableState.highWaterMark)
|
|
}
|
|
|
|
// Grab another reference to the pool in the case that while we're
|
|
// in the thread pool another read() finishes up the pool, and
|
|
// allocates a new one.
|
|
const thisPool = pool
|
|
let toRead = Math.min(pool.length - pool.used, n)
|
|
const start = pool.used
|
|
|
|
if (this.end !== undefined) toRead = Math.min(this.end - this.pos + 1, toRead)
|
|
|
|
// Already read everything we were supposed to read!
|
|
// treat as EOF.
|
|
if (toRead <= 0) return this.push(null)
|
|
|
|
// the actual read.
|
|
this.sftp.read(this.handle, pool, pool.used, toRead, this.pos, (er, bytesRead) => {
|
|
if (er) {
|
|
this.emit('error', er)
|
|
if (this.autoClose) this.destroy()
|
|
return
|
|
}
|
|
let b = null
|
|
|
|
// Now that we know how much data we have actually read, re-wind the
|
|
// 'used' field if we can, and otherwise allow the remainder of our
|
|
// reservation to be used as a new pool later.
|
|
if (start + toRead === thisPool.used && thisPool === pool) {
|
|
thisPool.used = roundUpToMultipleOf8(thisPool.used + bytesRead - toRead)
|
|
} else {
|
|
// Round down to the next lowest multiple of 8 to ensure the new pool
|
|
// fragment start and end positions are aligned to an 8 byte boundary.
|
|
const alignedEnd = (start + toRead) & ~7
|
|
const alignedStart = roundUpToMultipleOf8(start + bytesRead)
|
|
if (alignedEnd - alignedStart >= kMinPoolSpace)
|
|
poolFragments.push(thisPool.slice(alignedStart, alignedEnd))
|
|
}
|
|
|
|
if (bytesRead > 0) {
|
|
this.bytesRead += bytesRead
|
|
b = thisPool.slice(start, start + bytesRead)
|
|
}
|
|
|
|
// Move the pool positions, and internal position for reading.
|
|
this.pos += bytesRead
|
|
|
|
this.push(b)
|
|
})
|
|
|
|
pool.used = roundUpToMultipleOf8(pool.used + toRead)
|
|
}
|
|
|
|
ReadStream.prototype._destroy = function (err, cb) {
|
|
if (this._opening && !Buffer.isBuffer(this.handle)) {
|
|
this.once('open', closeStream.bind(null, this, cb, err))
|
|
return
|
|
}
|
|
|
|
closeStream(this, cb, err)
|
|
this.handle = null
|
|
this._opening = false
|
|
}
|
|
|
|
function closeStream(stream, cb, err) {
|
|
if (!stream.handle) return onclose()
|
|
|
|
stream.sftp.close(stream.handle, onclose)
|
|
|
|
function onclose(er) {
|
|
er = er || err
|
|
cb(er)
|
|
stream.isClosed = true
|
|
if (!er) stream.emit('close')
|
|
}
|
|
}
|
|
|
|
ReadStream.prototype.close = function (cb) {
|
|
this.destroy(null, cb)
|
|
}
|
|
|
|
Object.defineProperty(ReadStream.prototype, 'pending', {
|
|
get() {
|
|
return this.handle === null
|
|
},
|
|
configurable: true
|
|
})
|
|
|
|
// TODO: add `concurrency` setting to allow more than one in-flight WRITE
|
|
// request to server to improve throughput
|
|
function WriteStream(sftp, path, options) {
|
|
if (options === undefined) options = {}
|
|
else if (typeof options === 'string') options = { encoding: options }
|
|
else if (options === null || typeof options !== 'object')
|
|
throw new TypeError('"options" argument must be a string or an object')
|
|
else options = Object.create(options)
|
|
|
|
// For backwards compat do not emit close on destroy.
|
|
options.emitClose = false
|
|
options.autoDestroy = false // Node 14 major change.
|
|
|
|
WritableStream.call(this, options)
|
|
|
|
this.path = path
|
|
this.flags = options.flags === undefined ? 'w' : options.flags
|
|
this.mode = options.mode === undefined ? 0o666 : options.mode
|
|
|
|
this.start = options.start
|
|
this.autoClose = options.autoClose === undefined ? true : options.autoClose
|
|
this.pos = 0
|
|
this.bytesWritten = 0
|
|
this.isClosed = false
|
|
|
|
this.handle = options.handle === undefined ? null : options.handle
|
|
this.sftp = sftp
|
|
this._opening = false
|
|
|
|
if (this.start !== undefined) {
|
|
checkPosition(this.start, 'start')
|
|
|
|
this.pos = this.start
|
|
}
|
|
|
|
if (options.encoding) this.setDefaultEncoding(options.encoding)
|
|
|
|
// Node v6.x only
|
|
this.on('finish', function () {
|
|
if (this._writableState.finalCalled) return
|
|
if (this.autoClose) this.destroy()
|
|
})
|
|
|
|
if (!Buffer.isBuffer(this.handle)) this.open()
|
|
}
|
|
inherits(WriteStream, WritableStream)
|
|
|
|
WriteStream.prototype._final = function (cb) {
|
|
if (this.autoClose) this.destroy()
|
|
cb()
|
|
}
|
|
|
|
WriteStream.prototype.open = function () {
|
|
if (this._opening) return
|
|
|
|
this._opening = true
|
|
|
|
this.sftp.open(this.path, this.flags, this.mode, (er, handle) => {
|
|
this._opening = false
|
|
|
|
if (er) {
|
|
this.emit('error', er)
|
|
if (this.autoClose) this.destroy()
|
|
return
|
|
}
|
|
|
|
this.handle = handle
|
|
|
|
const tryAgain = (err) => {
|
|
if (err) {
|
|
// Try chmod() for sftp servers that may not support fchmod() for
|
|
// whatever reason
|
|
this.sftp.chmod(this.path, this.mode, (err_) => tryAgain())
|
|
return
|
|
}
|
|
|
|
// SFTPv3 requires absolute offsets, no matter the open flag used
|
|
if (this.flags[0] === 'a') {
|
|
const tryStat = (err, st) => {
|
|
if (err) {
|
|
// Try stat() for sftp servers that may not support fstat() for
|
|
// whatever reason
|
|
this.sftp.stat(this.path, (err_, st_) => {
|
|
if (err_) {
|
|
this.destroy()
|
|
this.emit('error', err)
|
|
return
|
|
}
|
|
tryStat(null, st_)
|
|
})
|
|
return
|
|
}
|
|
|
|
this.pos = st.size
|
|
this.emit('open', handle)
|
|
this.emit('ready')
|
|
}
|
|
|
|
this.sftp.fstat(handle, tryStat)
|
|
return
|
|
}
|
|
|
|
this.emit('open', handle)
|
|
this.emit('ready')
|
|
}
|
|
|
|
this.sftp.fchmod(handle, this.mode, tryAgain)
|
|
})
|
|
}
|
|
|
|
WriteStream.prototype._write = function (data, encoding, cb) {
|
|
if (!Buffer.isBuffer(data)) {
|
|
const err = new ERR_INVALID_ARG_TYPE('data', 'Buffer', data)
|
|
return this.emit('error', err)
|
|
}
|
|
|
|
if (!Buffer.isBuffer(this.handle)) {
|
|
return this.once('open', function () {
|
|
this._write(data, encoding, cb)
|
|
})
|
|
}
|
|
|
|
this.sftp.write(this.handle, data, 0, data.length, this.pos, (er, bytes) => {
|
|
if (er) {
|
|
if (this.autoClose) this.destroy()
|
|
return cb(er)
|
|
}
|
|
this.bytesWritten += bytes
|
|
cb()
|
|
})
|
|
|
|
this.pos += data.length
|
|
}
|
|
|
|
WriteStream.prototype._writev = function (data, cb) {
|
|
if (!Buffer.isBuffer(this.handle)) {
|
|
return this.once('open', function () {
|
|
this._writev(data, cb)
|
|
})
|
|
}
|
|
|
|
const sftp = this.sftp
|
|
const handle = this.handle
|
|
let writesLeft = data.length
|
|
|
|
const onwrite = (er, bytes) => {
|
|
if (er) {
|
|
this.destroy()
|
|
return cb(er)
|
|
}
|
|
this.bytesWritten += bytes
|
|
if (--writesLeft === 0) cb()
|
|
}
|
|
|
|
// TODO: try to combine chunks to reduce number of requests to the server?
|
|
for (let i = 0; i < data.length; ++i) {
|
|
const chunk = data[i].chunk
|
|
|
|
sftp.write(handle, chunk, 0, chunk.length, this.pos, onwrite)
|
|
this.pos += chunk.length
|
|
}
|
|
}
|
|
|
|
if (typeof WritableStream.prototype.destroy !== 'function')
|
|
WriteStream.prototype.destroy = ReadStream.prototype.destroy
|
|
|
|
WriteStream.prototype._destroy = ReadStream.prototype._destroy
|
|
WriteStream.prototype.close = function (cb) {
|
|
if (cb) {
|
|
if (this.isClosed) {
|
|
process.nextTick(cb)
|
|
return
|
|
}
|
|
this.on('close', cb)
|
|
}
|
|
|
|
// If we are not autoClosing, we should call
|
|
// destroy on 'finish'.
|
|
if (!this.autoClose) this.on('finish', this.destroy.bind(this))
|
|
|
|
this.end()
|
|
}
|
|
|
|
// There is no shutdown() for files.
|
|
WriteStream.prototype.destroySoon = WriteStream.prototype.end
|
|
|
|
Object.defineProperty(WriteStream.prototype, 'pending', {
|
|
get() {
|
|
return this.handle === null
|
|
},
|
|
configurable: true
|
|
})
|
|
// =============================================================================
|
|
|
|
module.exports = {
|
|
flagsToString,
|
|
OPEN_MODE,
|
|
SFTP,
|
|
Stats,
|
|
STATUS_CODE,
|
|
stringToFlags
|
|
}
|