'use strict'; const EventEmitter = require('events'); const fs = require('fs'); const { constants } = fs; const { Readable: ReadableStream, Writable: WritableStream } = require('stream'); const { inherits, isDate } = require('util'); const FastBuffer = Buffer[Symbol.species]; const { bufferCopy, bufferSlice, makeBufferParser, writeUInt32BE, } = require('./utils.js'); const ATTR = { SIZE: 0x00000001, UIDGID: 0x00000002, PERMISSIONS: 0x00000004, ACMODTIME: 0x00000008, EXTENDED: 0x80000000, }; // Large enough to store all possible attributes const ATTRS_BUF = Buffer.alloc(28); const STATUS_CODE = { OK: 0, EOF: 1, NO_SUCH_FILE: 2, PERMISSION_DENIED: 3, FAILURE: 4, BAD_MESSAGE: 5, NO_CONNECTION: 6, CONNECTION_LOST: 7, OP_UNSUPPORTED: 8 }; const VALID_STATUS_CODES = new Map( Object.values(STATUS_CODE).map((n) => [n, 1]) ); const STATUS_CODE_STR = { [STATUS_CODE.OK]: 'No error', [STATUS_CODE.EOF]: 'End of file', [STATUS_CODE.NO_SUCH_FILE]: 'No such file or directory', [STATUS_CODE.PERMISSION_DENIED]: 'Permission denied', [STATUS_CODE.FAILURE]: 'Failure', [STATUS_CODE.BAD_MESSAGE]: 'Bad message', [STATUS_CODE.NO_CONNECTION]: 'No connection', [STATUS_CODE.CONNECTION_LOST]: 'Connection lost', [STATUS_CODE.OP_UNSUPPORTED]: 'Operation unsupported', }; const REQUEST = { INIT: 1, OPEN: 3, CLOSE: 4, READ: 5, WRITE: 6, LSTAT: 7, FSTAT: 8, SETSTAT: 9, FSETSTAT: 10, OPENDIR: 11, READDIR: 12, REMOVE: 13, MKDIR: 14, RMDIR: 15, REALPATH: 16, STAT: 17, RENAME: 18, READLINK: 19, SYMLINK: 20, EXTENDED: 200 }; const RESPONSE = { VERSION: 2, STATUS: 101, HANDLE: 102, DATA: 103, NAME: 104, ATTRS: 105, EXTENDED: 201 }; const OPEN_MODE = { READ: 0x00000001, WRITE: 0x00000002, APPEND: 0x00000004, CREAT: 0x00000008, TRUNC: 0x00000010, EXCL: 0x00000020 }; const PKT_RW_OVERHEAD = 2 * 1024; const MAX_REQID = 2 ** 32 - 1; const CLIENT_VERSION_BUFFER = Buffer.from([ 0, 0, 0, 5 /* length */, REQUEST.INIT, 0, 0, 0, 3 /* version */ ]); const SERVER_VERSION_BUFFER = Buffer.from([ 0, 0, 0, 5 /* length */, RESPONSE.VERSION, 0, 0, 0, 3 /* version */ ]); const RE_OPENSSH = /^SSH-2.0-(?:OpenSSH|dropbear)/; const OPENSSH_MAX_PKT_LEN = 256 * 1024; const bufferParser = makeBufferParser(); const fakeStderr = { readable: false, writable: false, push: (data) => {}, once: () => {}, on: () => {}, emit: () => {}, end: () => {}, }; function noop() {} // Emulates enough of `Channel` to be able to be used as a drop-in replacement // in order to process incoming data with as little overhead as possible class SFTP extends EventEmitter { constructor(client, chanInfo, cfg) { super(); if (typeof cfg !== 'object' || !cfg) cfg = {}; const remoteIdentRaw = client._protocol._remoteIdentRaw; this.server = !!cfg.server; this._debug = (typeof cfg.debug === 'function' ? cfg.debug : undefined); this._isOpenSSH = (remoteIdentRaw && RE_OPENSSH.test(remoteIdentRaw)); this._version = -1; this._extensions = {}; this._biOpt = cfg.biOpt; this._pktLenBytes = 0; this._pktLen = 0; this._pktPos = 0; this._pktType = 0; this._pktData = undefined; this._writeReqid = -1; this._requests = {}; this._maxInPktLen = OPENSSH_MAX_PKT_LEN; this._maxOutPktLen = 34000; this._maxReadLen = (this._isOpenSSH ? OPENSSH_MAX_PKT_LEN : 34000) - PKT_RW_OVERHEAD; this._maxWriteLen = (this._isOpenSSH ? OPENSSH_MAX_PKT_LEN : 34000) - PKT_RW_OVERHEAD; this.maxOpenHandles = undefined; // Channel compatibility this._client = client; this._protocol = client._protocol; this._callbacks = []; this._hasX11 = false; this._exit = { code: undefined, signal: undefined, dump: undefined, desc: undefined, }; this._waitWindow = false; // SSH-level backpressure this._chunkcb = undefined; this._buffer = []; this.type = chanInfo.type; this.subtype = undefined; this.incoming = chanInfo.incoming; this.outgoing = chanInfo.outgoing; this.stderr = fakeStderr; this.readable = true; } // This handles incoming data to parse push(data) { if (data === null) { cleanupRequests(this); if (!this.readable) return; // No more incoming data from the remote side this.readable = false; this.emit('end'); return; } /* uint32 length byte type byte[length - 1] data payload */ let p = 0; while (p < data.length) { if (this._pktLenBytes < 4) { let nb = Math.min(4 - this._pktLenBytes, data.length - p); this._pktLenBytes += nb; while (nb--) this._pktLen = (this._pktLen << 8) + data[p++]; if (this._pktLenBytes < 4) return; if (this._pktLen === 0) return doFatalSFTPError(this, 'Invalid packet length'); if (this._pktLen > this._maxInPktLen) { const max = this._maxInPktLen; return doFatalSFTPError( this, `Packet length ${this._pktLen} exceeds max length of ${max}` ); } if (p >= data.length) return; } if (this._pktPos < this._pktLen) { const nb = Math.min(this._pktLen - this._pktPos, data.length - p); if (p !== 0 || nb !== data.length) { if (nb === this._pktLen) { this._pkt = new FastBuffer(data.buffer, data.byteOffset + p, nb); } else { if (!this._pkt) this._pkt = Buffer.allocUnsafe(this._pktLen); this._pkt.set( new Uint8Array(data.buffer, data.byteOffset + p, nb), this._pktPos ); } } else if (nb === this._pktLen) { this._pkt = data; } else { if (!this._pkt) this._pkt = Buffer.allocUnsafe(this._pktLen); this._pkt.set(data, this._pktPos); } p += nb; this._pktPos += nb; if (this._pktPos < this._pktLen) return; } const type = this._pkt[0]; const payload = this._pkt; // Prepare for next packet this._pktLen = 0; this._pktLenBytes = 0; this._pkt = undefined; this._pktPos = 0; const handler = (this.server ? SERVER_HANDLERS[type] : CLIENT_HANDLERS[type]); if (!handler) return doFatalSFTPError(this, `Unknown packet type ${type}`); if (this._version === -1) { if (this.server) { if (type !== REQUEST.INIT) return doFatalSFTPError(this, `Expected INIT packet, got ${type}`); } else if (type !== RESPONSE.VERSION) { return doFatalSFTPError(this, `Expected VERSION packet, got ${type}`); } } if (handler(this, payload) === false) return; } } end() { this.destroy(); } destroy() { if (this.outgoing.state === 'open' || this.outgoing.state === 'eof') { this.outgoing.state = 'closing'; this._protocol.channelClose(this.outgoing.id); } } _init() { this._init = noop; if (!this.server) sendOrBuffer(this, CLIENT_VERSION_BUFFER); } // =========================================================================== // Client-specific =========================================================== // =========================================================================== createReadStream(path, options) { if (this.server) throw new Error('Client-only method called in server mode'); return new ReadStream(this, path, options); } createWriteStream(path, options) { if (this.server) throw new Error('Client-only method called in server mode'); return new WriteStream(this, path, options); } open(path, flags_, attrs, cb) { if (this.server) throw new Error('Client-only method called in server mode'); if (typeof attrs === 'function') { cb = attrs; attrs = undefined; } const flags = (typeof flags_ === 'number' ? flags_ : stringToFlags(flags_)); if (flags === null) throw new Error(`Unknown flags string: ${flags_}`); let attrsFlags = 0; let attrsLen = 0; if (typeof attrs === 'string' || typeof attrs === 'number') attrs = { mode: attrs }; if (typeof attrs === 'object' && attrs !== null) { attrs = attrsToBytes(attrs); attrsFlags = attrs.flags; attrsLen = attrs.nb; } /* uint32 id string filename uint32 pflags ATTRS attrs */ const pathLen = Buffer.byteLength(path); let p = 9; const buf = Buffer.allocUnsafe(4 + 1 + 4 + 4 + pathLen + 4 + 4 + attrsLen); writeUInt32BE(buf, buf.length - 4, 0); buf[4] = REQUEST.OPEN; const reqid = this._writeReqid = (this._writeReqid + 1) & MAX_REQID; writeUInt32BE(buf, reqid, 5); writeUInt32BE(buf, pathLen, p); buf.utf8Write(path, p += 4, pathLen); writeUInt32BE(buf, flags, p += pathLen); writeUInt32BE(buf, attrsFlags, p += 4); if (attrsLen) { p += 4; if (attrsLen === ATTRS_BUF.length) buf.set(ATTRS_BUF, p); else bufferCopy(ATTRS_BUF, buf, 0, attrsLen, p); p += attrsLen; } this._requests[reqid] = { cb }; const isBuffered = sendOrBuffer(this, buf); this._debug && this._debug( `SFTP: Outbound: ${isBuffered ? 'Buffered' : 'Sending'} OPEN` ); } close(handle, cb) { if (this.server) throw new Error('Client-only method called in server mode'); if (!Buffer.isBuffer(handle)) throw new Error('handle is not a Buffer'); /* uint32 id string handle */ const handleLen = handle.length; let p = 9; const buf = Buffer.allocUnsafe(4 + 1 + 4 + 4 + handleLen); writeUInt32BE(buf, buf.length - 4, 0); buf[4] = REQUEST.CLOSE; const reqid = this._writeReqid = (this._writeReqid + 1) & MAX_REQID; writeUInt32BE(buf, reqid, 5); writeUInt32BE(buf, handleLen, p); buf.set(handle, p += 4); this._requests[reqid] = { cb }; const isBuffered = sendOrBuffer(this, buf); this._debug && this._debug( `SFTP: Outbound: ${isBuffered ? 'Buffered' : 'Sending'} CLOSE` ); } read(handle, buf, off, len, position, cb) { if (this.server) throw new Error('Client-only method called in server mode'); if (!Buffer.isBuffer(handle)) throw new Error('handle is not a Buffer'); if (!Buffer.isBuffer(buf)) throw new Error('buffer is not a Buffer'); if (off >= buf.length) throw new Error('offset is out of bounds'); if (off + len > buf.length) throw new Error('length extends beyond buffer'); if (position === null) throw new Error('null position currently unsupported'); read_(this, handle, buf, off, len, position, cb); } readData(handle, buf, off, len, position, cb) { // Backwards compatibility this.read(handle, buf, off, len, position, cb); } write(handle, buf, off, len, position, cb) { if (this.server) throw new Error('Client-only method called in server mode'); if (!Buffer.isBuffer(handle)) throw new Error('handle is not a Buffer'); if (!Buffer.isBuffer(buf)) throw new Error('buffer is not a Buffer'); if (off > buf.length) throw new Error('offset is out of bounds'); if (off + len > buf.length) throw new Error('length extends beyond buffer'); if (position === null) throw new Error('null position currently unsupported'); if (!len) { cb && process.nextTick(cb, undefined, 0); return; } const maxDataLen = this._maxWriteLen; const overflow = Math.max(len - maxDataLen, 0); const origPosition = position; if (overflow) len = maxDataLen; /* uint32 id string handle uint64 offset string data */ const handleLen = handle.length; let p = 9; const out = Buffer.allocUnsafe(4 + 1 + 4 + 4 + handleLen + 8 + 4 + len); writeUInt32BE(out, out.length - 4, 0); out[4] = REQUEST.WRITE; const reqid = this._writeReqid = (this._writeReqid + 1) & MAX_REQID; writeUInt32BE(out, reqid, 5); writeUInt32BE(out, handleLen, p); out.set(handle, p += 4); p += handleLen; for (let i = 7; i >= 0; --i) { out[p + i] = position & 0xFF; position /= 256; } writeUInt32BE(out, len, p += 8); bufferCopy(buf, out, off, off + len, p += 4); this._requests[reqid] = { cb: (err) => { if (err) { if (typeof cb === 'function') cb(err); } else if (overflow) { this.write(handle, buf, off + len, overflow, origPosition + len, cb); } else if (typeof cb === 'function') { cb(undefined, off + len); } } }; const isSent = sendOrBuffer(this, out); if (this._debug) { const how = (isSent ? 'Sent' : 'Buffered'); this._debug(`SFTP: Outbound: ${how} WRITE (id:${reqid})`); } } writeData(handle, buf, off, len, position, cb) { // Backwards compatibility this.write(handle, buf, off, len, position, cb); } fastGet(remotePath, localPath, opts, cb) { if (this.server) throw new Error('Client-only method called in server mode'); fastXfer(this, fs, remotePath, localPath, opts, cb); } fastPut(localPath, remotePath, opts, cb) { if (this.server) throw new Error('Client-only method called in server mode'); fastXfer(fs, this, localPath, remotePath, opts, cb); } readFile(path, options, callback_) { if (this.server) throw new Error('Client-only method called in server mode'); let callback; if (typeof callback_ === 'function') { callback = callback_; } else if (typeof options === 'function') { callback = options; options = undefined; } if (typeof options === 'string') options = { encoding: options, flag: 'r' }; else if (!options) options = { encoding: null, flag: 'r' }; else if (typeof options !== 'object') throw new TypeError('Bad arguments'); const encoding = options.encoding; if (encoding && !Buffer.isEncoding(encoding)) throw new Error(`Unknown encoding: ${encoding}`); // First stat the file, so we know the size. let size; let buffer; // Single buffer with file data let buffers; // List for when size is unknown let pos = 0; let handle; // SFTPv3 does not support using -1 for read position, so we have to track // read position manually let bytesRead = 0; const flag = options.flag || 'r'; const read = () => { if (size === 0) { buffer = Buffer.allocUnsafe(8192); this.read(handle, buffer, 0, 8192, bytesRead, afterRead); } else { this.read(handle, buffer, pos, size - pos, bytesRead, afterRead); } }; const afterRead = (er, nbytes) => { let eof; if (er) { eof = (er.code === STATUS_CODE.EOF); if (!eof) { return this.close(handle, () => { return callback && callback(er); }); } } else { eof = false; } if (eof || (size === 0 && nbytes === 0)) return close(); bytesRead += nbytes; pos += nbytes; if (size !== 0) { if (pos === size) close(); else read(); } else { // Unknown size, just read until we don't get bytes. buffers.push(bufferSlice(buffer, 0, nbytes)); read(); } }; afterRead._wantEOFError = true; const close = () => { this.close(handle, (er) => { if (size === 0) { // Collect the data into the buffers list. buffer = Buffer.concat(buffers, pos); } else if (pos < size) { buffer = bufferSlice(buffer, 0, pos); } if (encoding) buffer = buffer.toString(encoding); return callback && callback(er, buffer); }); }; this.open(path, flag, 0o666, (er, handle_) => { if (er) return callback && callback(er); handle = handle_; const tryStat = (er, st) => { if (er) { // Try stat() for sftp servers that may not support fstat() for // whatever reason this.stat(path, (er_, st_) => { if (er_) { return this.close(handle, () => { callback && callback(er); }); } tryStat(null, st_); }); return; } size = st.size || 0; if (size === 0) { // The kernel lies about many files. // Go ahead and try to read some bytes. buffers = []; return read(); } buffer = Buffer.allocUnsafe(size); read(); }; this.fstat(handle, tryStat); }); } writeFile(path, data, options, callback_) { if (this.server) throw new Error('Client-only method called in server mode'); let callback; if (typeof callback_ === 'function') { callback = callback_; } else if (typeof options === 'function') { callback = options; options = undefined; } if (typeof options === 'string') options = { encoding: options, mode: 0o666, flag: 'w' }; else if (!options) options = { encoding: 'utf8', mode: 0o666, flag: 'w' }; else if (typeof options !== 'object') throw new TypeError('Bad arguments'); if (options.encoding && !Buffer.isEncoding(options.encoding)) throw new Error(`Unknown encoding: ${options.encoding}`); const flag = options.flag || 'w'; this.open(path, flag, options.mode, (openErr, handle) => { if (openErr) { callback && callback(openErr); } else { const buffer = (Buffer.isBuffer(data) ? data : Buffer.from('' + data, options.encoding || 'utf8')); const position = (/a/.test(flag) ? null : 0); // SFTPv3 does not support the notion of 'current position' // (null position), so we just attempt to append to the end of the file // instead if (position === null) { const tryStat = (er, st) => { if (er) { // Try stat() for sftp servers that may not support fstat() for // whatever reason this.stat(path, (er_, st_) => { if (er_) { return this.close(handle, () => { callback && callback(er); }); } tryStat(null, st_); }); return; } writeAll(this, handle, buffer, 0, buffer.length, st.size, callback); }; this.fstat(handle, tryStat); return; } writeAll(this, handle, buffer, 0, buffer.length, position, callback); } }); } appendFile(path, data, options, callback_) { if (this.server) throw new Error('Client-only method called in server mode'); let callback; if (typeof callback_ === 'function') { callback = callback_; } else if (typeof options === 'function') { callback = options; options = undefined; } if (typeof options === 'string') options = { encoding: options, mode: 0o666, flag: 'a' }; else if (!options) options = { encoding: 'utf8', mode: 0o666, flag: 'a' }; else if (typeof options !== 'object') throw new TypeError('Bad arguments'); if (!options.flag) options = Object.assign({ flag: 'a' }, options); this.writeFile(path, data, options, callback); } exists(path, cb) { if (this.server) throw new Error('Client-only method called in server mode'); this.stat(path, (err) => { cb && cb(err ? false : true); }); } unlink(filename, cb) { if (this.server) throw new Error('Client-only method called in server mode'); /* uint32 id string filename */ const fnameLen = Buffer.byteLength(filename); let p = 9; const buf = Buffer.allocUnsafe(4 + 1 + 4 + 4 + fnameLen); writeUInt32BE(buf, buf.length - 4, 0); buf[4] = REQUEST.REMOVE; const reqid = this._writeReqid = (this._writeReqid + 1) & MAX_REQID; writeUInt32BE(buf, reqid, 5); writeUInt32BE(buf, fnameLen, p); buf.utf8Write(filename, p += 4, fnameLen); this._requests[reqid] = { cb }; const isBuffered = sendOrBuffer(this, buf); this._debug && this._debug( `SFTP: Outbound: ${isBuffered ? 'Buffered' : 'Sending'} REMOVE` ); } rename(oldPath, newPath, cb) { if (this.server) throw new Error('Client-only method called in server mode'); /* uint32 id string oldpath string newpath */ const oldLen = Buffer.byteLength(oldPath); const newLen = Buffer.byteLength(newPath); let p = 9; const buf = Buffer.allocUnsafe(4 + 1 + 4 + 4 + oldLen + 4 + newLen); writeUInt32BE(buf, buf.length - 4, 0); buf[4] = REQUEST.RENAME; const reqid = this._writeReqid = (this._writeReqid + 1) & MAX_REQID; writeUInt32BE(buf, reqid, 5); writeUInt32BE(buf, oldLen, p); buf.utf8Write(oldPath, p += 4, oldLen); writeUInt32BE(buf, newLen, p += oldLen); buf.utf8Write(newPath, p += 4, newLen); this._requests[reqid] = { cb }; const isBuffered = sendOrBuffer(this, buf); this._debug && this._debug( `SFTP: Outbound: ${isBuffered ? 'Buffered' : 'Sending'} RENAME` ); } mkdir(path, attrs, cb) { if (this.server) throw new Error('Client-only method called in server mode'); let flags = 0; let attrsLen = 0; if (typeof attrs === 'function') { cb = attrs; attrs = undefined; } if (typeof attrs === 'object' && attrs !== null) { attrs = attrsToBytes(attrs); flags = attrs.flags; attrsLen = attrs.nb; } /* uint32 id string path ATTRS attrs */ const pathLen = Buffer.byteLength(path); let p = 9; const buf = Buffer.allocUnsafe(4 + 1 + 4 + 4 + pathLen + 4 + attrsLen); writeUInt32BE(buf, buf.length - 4, 0); buf[4] = REQUEST.MKDIR; const reqid = this._writeReqid = (this._writeReqid + 1) & MAX_REQID; writeUInt32BE(buf, reqid, 5); writeUInt32BE(buf, pathLen, p); buf.utf8Write(path, p += 4, pathLen); writeUInt32BE(buf, flags, p += pathLen); if (attrsLen) { p += 4; if (attrsLen === ATTRS_BUF.length) buf.set(ATTRS_BUF, p); else bufferCopy(ATTRS_BUF, buf, 0, attrsLen, p); p += attrsLen; } this._requests[reqid] = { cb }; const isBuffered = sendOrBuffer(this, buf); this._debug && this._debug( `SFTP: Outbound: ${isBuffered ? 'Buffered' : 'Sending'} MKDIR` ); } rmdir(path, cb) { if (this.server) throw new Error('Client-only method called in server mode'); /* uint32 id string path */ const pathLen = Buffer.byteLength(path); let p = 9; const buf = Buffer.allocUnsafe(4 + 1 + 4 + 4 + pathLen); writeUInt32BE(buf, buf.length - 4, 0); buf[4] = REQUEST.RMDIR; const reqid = this._writeReqid = (this._writeReqid + 1) & MAX_REQID; writeUInt32BE(buf, reqid, 5); writeUInt32BE(buf, pathLen, p); buf.utf8Write(path, p += 4, pathLen); this._requests[reqid] = { cb }; const isBuffered = sendOrBuffer(this, buf); this._debug && this._debug( `SFTP: Outbound: ${isBuffered ? 'Buffered' : 'Sending'} RMDIR` ); } readdir(where, opts, cb) { if (this.server) throw new Error('Client-only method called in server mode'); if (typeof opts === 'function') { cb = opts; opts = {}; } if (typeof opts !== 'object' || opts === null) opts = {}; const doFilter = (opts && opts.full ? false : true); if (!Buffer.isBuffer(where) && typeof where !== 'string') throw new Error('missing directory handle or path'); if (typeof where === 'string') { const entries = []; let e = 0; const reread = (err, handle) => { if (err) return cb(err); this.readdir(handle, opts, (err, list) => { const eof = (err && err.code === STATUS_CODE.EOF); if (err && !eof) return this.close(handle, () => cb(err)); if (eof) { return this.close(handle, (err) => { if (err) return cb(err); cb(undefined, entries); }); } for (let i = 0; i < list.length; ++i, ++e) entries[e] = list[i]; reread(undefined, handle); }); }; return this.opendir(where, reread); } /* uint32 id string handle */ const handleLen = where.length; let p = 9; const buf = Buffer.allocUnsafe(4 + 1 + 4 + 4 + handleLen); writeUInt32BE(buf, buf.length - 4, 0); buf[4] = REQUEST.READDIR; const reqid = this._writeReqid = (this._writeReqid + 1) & MAX_REQID; writeUInt32BE(buf, reqid, 5); writeUInt32BE(buf, handleLen, p); buf.set(where, p += 4); this._requests[reqid] = { cb: (doFilter ? (err, list) => { if (typeof cb !== 'function') return; if (err) return cb(err); for (let i = list.length - 1; i >= 0; --i) { if (list[i].filename === '.' || list[i].filename === '..') list.splice(i, 1); } cb(undefined, list); } : cb) }; const isBuffered = sendOrBuffer(this, buf); this._debug && this._debug( `SFTP: Outbound: ${isBuffered ? 'Buffered' : 'Sending'} READDIR` ); } fstat(handle, cb) { if (this.server) throw new Error('Client-only method called in server mode'); if (!Buffer.isBuffer(handle)) throw new Error('handle is not a Buffer'); /* uint32 id string handle */ const handleLen = handle.length; let p = 9; const buf = Buffer.allocUnsafe(4 + 1 + 4 + 4 + handleLen); writeUInt32BE(buf, buf.length - 4, 0); buf[4] = REQUEST.FSTAT; const reqid = this._writeReqid = (this._writeReqid + 1) & MAX_REQID; writeUInt32BE(buf, reqid, 5); writeUInt32BE(buf, handleLen, p); buf.set(handle, p += 4); this._requests[reqid] = { cb }; const isBuffered = sendOrBuffer(this, buf); this._debug && this._debug( `SFTP: Outbound: ${isBuffered ? 'Buffered' : 'Sending'} FSTAT` ); } stat(path, cb) { if (this.server) throw new Error('Client-only method called in server mode'); /* uint32 id string path */ const pathLen = Buffer.byteLength(path); let p = 9; const buf = Buffer.allocUnsafe(4 + 1 + 4 + 4 + pathLen); writeUInt32BE(buf, buf.length - 4, 0); buf[4] = REQUEST.STAT; const reqid = this._writeReqid = (this._writeReqid + 1) & MAX_REQID; writeUInt32BE(buf, reqid, 5); writeUInt32BE(buf, pathLen, p); buf.utf8Write(path, p += 4, pathLen); this._requests[reqid] = { cb }; const isBuffered = sendOrBuffer(this, buf); this._debug && this._debug( `SFTP: Outbound: ${isBuffered ? 'Buffered' : 'Sending'} STAT` ); } lstat(path, cb) { if (this.server) throw new Error('Client-only method called in server mode'); /* uint32 id string path */ const pathLen = Buffer.byteLength(path); let p = 9; const buf = Buffer.allocUnsafe(4 + 1 + 4 + 4 + pathLen); writeUInt32BE(buf, buf.length - 4, 0); buf[4] = REQUEST.LSTAT; const reqid = this._writeReqid = (this._writeReqid + 1) & MAX_REQID; writeUInt32BE(buf, reqid, 5); writeUInt32BE(buf, pathLen, p); buf.utf8Write(path, p += 4, pathLen); this._requests[reqid] = { cb }; const isBuffered = sendOrBuffer(this, buf); this._debug && this._debug( `SFTP: Outbound: ${isBuffered ? 'Buffered' : 'Sending'} LSTAT` ); } opendir(path, cb) { if (this.server) throw new Error('Client-only method called in server mode'); /* uint32 id string path */ const pathLen = Buffer.byteLength(path); let p = 9; const buf = Buffer.allocUnsafe(4 + 1 + 4 + 4 + pathLen); writeUInt32BE(buf, buf.length - 4, 0); buf[4] = REQUEST.OPENDIR; const reqid = this._writeReqid = (this._writeReqid + 1) & MAX_REQID; writeUInt32BE(buf, reqid, 5); writeUInt32BE(buf, pathLen, p); buf.utf8Write(path, p += 4, pathLen); this._requests[reqid] = { cb }; const isBuffered = sendOrBuffer(this, buf); this._debug && this._debug( `SFTP: Outbound: ${isBuffered ? 'Buffered' : 'Sending'} OPENDIR` ); } setstat(path, attrs, cb) { if (this.server) throw new Error('Client-only method called in server mode'); let flags = 0; let attrsLen = 0; if (typeof attrs === 'object' && attrs !== null) { attrs = attrsToBytes(attrs); flags = attrs.flags; attrsLen = attrs.nb; } else if (typeof attrs === 'function') { cb = attrs; } /* uint32 id string path ATTRS attrs */ const pathLen = Buffer.byteLength(path); let p = 9; const buf = Buffer.allocUnsafe(4 + 1 + 4 + 4 + pathLen + 4 + attrsLen); writeUInt32BE(buf, buf.length - 4, 0); buf[4] = REQUEST.SETSTAT; const reqid = this._writeReqid = (this._writeReqid + 1) & MAX_REQID; writeUInt32BE(buf, reqid, 5); writeUInt32BE(buf, pathLen, p); buf.utf8Write(path, p += 4, pathLen); writeUInt32BE(buf, flags, p += pathLen); if (attrsLen) { p += 4; if (attrsLen === ATTRS_BUF.length) buf.set(ATTRS_BUF, p); else bufferCopy(ATTRS_BUF, buf, 0, attrsLen, p); p += attrsLen; } this._requests[reqid] = { cb }; const isBuffered = sendOrBuffer(this, buf); this._debug && this._debug( `SFTP: Outbound: ${isBuffered ? 'Buffered' : 'Sending'} SETSTAT` ); } fsetstat(handle, attrs, cb) { if (this.server) throw new Error('Client-only method called in server mode'); if (!Buffer.isBuffer(handle)) throw new Error('handle is not a Buffer'); let flags = 0; let attrsLen = 0; if (typeof attrs === 'object' && attrs !== null) { attrs = attrsToBytes(attrs); flags = attrs.flags; attrsLen = attrs.nb; } else if (typeof attrs === 'function') { cb = attrs; } /* uint32 id string handle ATTRS attrs */ const handleLen = handle.length; let p = 9; const buf = Buffer.allocUnsafe(4 + 1 + 4 + 4 + handleLen + 4 + attrsLen); writeUInt32BE(buf, buf.length - 4, 0); buf[4] = REQUEST.FSETSTAT; const reqid = this._writeReqid = (this._writeReqid + 1) & MAX_REQID; writeUInt32BE(buf, reqid, 5); writeUInt32BE(buf, handleLen, p); buf.set(handle, p += 4); writeUInt32BE(buf, flags, p += handleLen); if (attrsLen) { p += 4; if (attrsLen === ATTRS_BUF.length) buf.set(ATTRS_BUF, p); else bufferCopy(ATTRS_BUF, buf, 0, attrsLen, p); p += attrsLen; } this._requests[reqid] = { cb }; const isBuffered = sendOrBuffer(this, buf); this._debug && this._debug( `SFTP: Outbound: ${isBuffered ? 'Buffered' : 'Sending'} FSETSTAT` ); } futimes(handle, atime, mtime, cb) { return this.fsetstat(handle, { atime: toUnixTimestamp(atime), mtime: toUnixTimestamp(mtime) }, cb); } utimes(path, atime, mtime, cb) { return this.setstat(path, { atime: toUnixTimestamp(atime), mtime: toUnixTimestamp(mtime) }, cb); } fchown(handle, uid, gid, cb) { return this.fsetstat(handle, { uid: uid, gid: gid }, cb); } chown(path, uid, gid, cb) { return this.setstat(path, { uid: uid, gid: gid }, cb); } fchmod(handle, mode, cb) { return this.fsetstat(handle, { mode: mode }, cb); } chmod(path, mode, cb) { return this.setstat(path, { mode: mode }, cb); } readlink(path, cb) { if (this.server) throw new Error('Client-only method called in server mode'); /* uint32 id string path */ const pathLen = Buffer.byteLength(path); let p = 9; const buf = Buffer.allocUnsafe(4 + 1 + 4 + 4 + pathLen); writeUInt32BE(buf, buf.length - 4, 0); buf[4] = REQUEST.READLINK; const reqid = this._writeReqid = (this._writeReqid + 1) & MAX_REQID; writeUInt32BE(buf, reqid, 5); writeUInt32BE(buf, pathLen, p); buf.utf8Write(path, p += 4, pathLen); this._requests[reqid] = { cb: (err, names) => { if (typeof cb !== 'function') return; if (err) return cb(err); if (!names || !names.length) return cb(new Error('Response missing link info')); cb(undefined, names[0].filename); } }; const isBuffered = sendOrBuffer(this, buf); this._debug && this._debug( `SFTP: Outbound: ${isBuffered ? 'Buffered' : 'Sending'} READLINK` ); } symlink(targetPath, linkPath, cb) { if (this.server) throw new Error('Client-only method called in server mode'); /* uint32 id string linkpath string targetpath */ const linkLen = Buffer.byteLength(linkPath); const targetLen = Buffer.byteLength(targetPath); let p = 9; const buf = Buffer.allocUnsafe(4 + 1 + 4 + 4 + linkLen + 4 + targetLen); writeUInt32BE(buf, buf.length - 4, 0); buf[4] = REQUEST.SYMLINK; const reqid = this._writeReqid = (this._writeReqid + 1) & MAX_REQID; writeUInt32BE(buf, reqid, 5); if (this._isOpenSSH) { // OpenSSH has linkpath and targetpath positions switched writeUInt32BE(buf, targetLen, p); buf.utf8Write(targetPath, p += 4, targetLen); writeUInt32BE(buf, linkLen, p += targetLen); buf.utf8Write(linkPath, p += 4, linkLen); } else { writeUInt32BE(buf, linkLen, p); buf.utf8Write(linkPath, p += 4, linkLen); writeUInt32BE(buf, targetLen, p += linkLen); buf.utf8Write(targetPath, p += 4, targetLen); } this._requests[reqid] = { cb }; const isBuffered = sendOrBuffer(this, buf); this._debug && this._debug( `SFTP: Outbound: ${isBuffered ? 'Buffered' : 'Sending'} SYMLINK` ); } realpath(path, cb) { if (this.server) throw new Error('Client-only method called in server mode'); /* uint32 id string path */ const pathLen = Buffer.byteLength(path); let p = 9; const buf = Buffer.allocUnsafe(4 + 1 + 4 + 4 + pathLen); writeUInt32BE(buf, buf.length - 4, 0); buf[4] = REQUEST.REALPATH; const reqid = this._writeReqid = (this._writeReqid + 1) & MAX_REQID; writeUInt32BE(buf, reqid, 5); writeUInt32BE(buf, pathLen, p); buf.utf8Write(path, p += 4, pathLen); this._requests[reqid] = { cb: (err, names) => { if (typeof cb !== 'function') return; if (err) return cb(err); if (!names || !names.length) return cb(new Error('Response missing path info')); cb(undefined, names[0].filename); } }; const isBuffered = sendOrBuffer(this, buf); this._debug && this._debug( `SFTP: Outbound: ${isBuffered ? 'Buffered' : 'Sending'} REALPATH` ); } // extended requests ext_openssh_rename(oldPath, newPath, cb) { if (this.server) throw new Error('Client-only method called in server mode'); const ext = this._extensions['posix-rename@openssh.com']; if (!ext || ext !== '1') throw new Error('Server does not support this extended request'); /* uint32 id string "posix-rename@openssh.com" string oldpath string newpath */ const oldLen = Buffer.byteLength(oldPath); const newLen = Buffer.byteLength(newPath); let p = 9; const buf = Buffer.allocUnsafe(4 + 1 + 4 + 4 + 24 + 4 + oldLen + 4 + newLen); writeUInt32BE(buf, buf.length - 4, 0); buf[4] = REQUEST.EXTENDED; const reqid = this._writeReqid = (this._writeReqid + 1) & MAX_REQID; writeUInt32BE(buf, reqid, 5); writeUInt32BE(buf, 24, p); buf.utf8Write('posix-rename@openssh.com', p += 4, 24); writeUInt32BE(buf, oldLen, p += 24); buf.utf8Write(oldPath, p += 4, oldLen); writeUInt32BE(buf, newLen, p += oldLen); buf.utf8Write(newPath, p += 4, newLen); this._requests[reqid] = { cb }; const isBuffered = sendOrBuffer(this, buf); if (this._debug) { const which = (isBuffered ? 'Buffered' : 'Sending'); this._debug(`SFTP: Outbound: ${which} posix-rename@openssh.com`); } } ext_openssh_statvfs(path, cb) { if (this.server) throw new Error('Client-only method called in server mode'); const ext = this._extensions['statvfs@openssh.com']; if (!ext || ext !== '2') throw new Error('Server does not support this extended request'); /* uint32 id string "statvfs@openssh.com" string path */ const pathLen = Buffer.byteLength(path); let p = 9; const buf = Buffer.allocUnsafe(4 + 1 + 4 + 4 + 19 + 4 + pathLen); writeUInt32BE(buf, buf.length - 4, 0); buf[4] = REQUEST.EXTENDED; const reqid = this._writeReqid = (this._writeReqid + 1) & MAX_REQID; writeUInt32BE(buf, reqid, 5); writeUInt32BE(buf, 19, p); buf.utf8Write('statvfs@openssh.com', p += 4, 19); writeUInt32BE(buf, pathLen, p += 19); buf.utf8Write(path, p += 4, pathLen); this._requests[reqid] = { extended: 'statvfs@openssh.com', cb }; const isBuffered = sendOrBuffer(this, buf); if (this._debug) { const which = (isBuffered ? 'Buffered' : 'Sending'); this._debug(`SFTP: Outbound: ${which} statvfs@openssh.com`); } } ext_openssh_fstatvfs(handle, cb) { if (this.server) throw new Error('Client-only method called in server mode'); const ext = this._extensions['fstatvfs@openssh.com']; if (!ext || ext !== '2') throw new Error('Server does not support this extended request'); if (!Buffer.isBuffer(handle)) throw new Error('handle is not a Buffer'); /* uint32 id string "fstatvfs@openssh.com" string handle */ const handleLen = handle.length; let p = 9; const buf = Buffer.allocUnsafe(4 + 1 + 4 + 4 + 20 + 4 + handleLen); writeUInt32BE(buf, buf.length - 4, 0); buf[4] = REQUEST.EXTENDED; const reqid = this._writeReqid = (this._writeReqid + 1) & MAX_REQID; writeUInt32BE(buf, reqid, 5); writeUInt32BE(buf, 20, p); buf.utf8Write('fstatvfs@openssh.com', p += 4, 20); writeUInt32BE(buf, handleLen, p += 20); buf.set(handle, p += 4); this._requests[reqid] = { extended: 'fstatvfs@openssh.com', cb }; const isBuffered = sendOrBuffer(this, buf); if (this._debug) { const which = (isBuffered ? 'Buffered' : 'Sending'); this._debug(`SFTP: Outbound: ${which} fstatvfs@openssh.com`); } } ext_openssh_hardlink(oldPath, newPath, cb) { if (this.server) throw new Error('Client-only method called in server mode'); const ext = this._extensions['hardlink@openssh.com']; if (ext !== '1') throw new Error('Server does not support this extended request'); /* uint32 id string "hardlink@openssh.com" string oldpath string newpath */ const oldLen = Buffer.byteLength(oldPath); const newLen = Buffer.byteLength(newPath); let p = 9; const buf = Buffer.allocUnsafe(4 + 1 + 4 + 4 + 20 + 4 + oldLen + 4 + newLen); writeUInt32BE(buf, buf.length - 4, 0); buf[4] = REQUEST.EXTENDED; const reqid = this._writeReqid = (this._writeReqid + 1) & MAX_REQID; writeUInt32BE(buf, reqid, 5); writeUInt32BE(buf, 20, p); buf.utf8Write('hardlink@openssh.com', p += 4, 20); writeUInt32BE(buf, oldLen, p += 20); buf.utf8Write(oldPath, p += 4, oldLen); writeUInt32BE(buf, newLen, p += oldLen); buf.utf8Write(newPath, p += 4, newLen); this._requests[reqid] = { cb }; const isBuffered = sendOrBuffer(this, buf); if (this._debug) { const which = (isBuffered ? 'Buffered' : 'Sending'); this._debug(`SFTP: Outbound: ${which} hardlink@openssh.com`); } } ext_openssh_fsync(handle, cb) { if (this.server) throw new Error('Client-only method called in server mode'); const ext = this._extensions['fsync@openssh.com']; if (ext !== '1') throw new Error('Server does not support this extended request'); if (!Buffer.isBuffer(handle)) throw new Error('handle is not a Buffer'); /* uint32 id string "fsync@openssh.com" string handle */ const handleLen = handle.length; let p = 9; const buf = Buffer.allocUnsafe(4 + 1 + 4 + 4 + 17 + 4 + handleLen); writeUInt32BE(buf, buf.length - 4, 0); buf[4] = REQUEST.EXTENDED; const reqid = this._writeReqid = (this._writeReqid + 1) & MAX_REQID; writeUInt32BE(buf, reqid, 5); writeUInt32BE(buf, 17, p); buf.utf8Write('fsync@openssh.com', p += 4, 17); writeUInt32BE(buf, handleLen, p += 17); buf.set(handle, p += 4); this._requests[reqid] = { cb }; const isBuffered = sendOrBuffer(this, buf); this._debug && this._debug( `SFTP: Outbound: ${isBuffered ? 'Buffered' : 'Sending'} fsync@openssh.com` ); } ext_openssh_lsetstat(path, attrs, cb) { if (this.server) throw new Error('Client-only method called in server mode'); const ext = this._extensions['lsetstat@openssh.com']; if (ext !== '1') throw new Error('Server does not support this extended request'); let flags = 0; let attrsLen = 0; if (typeof attrs === 'object' && attrs !== null) { attrs = attrsToBytes(attrs); flags = attrs.flags; attrsLen = attrs.nb; } else if (typeof attrs === 'function') { cb = attrs; } /* uint32 id string "lsetstat@openssh.com" string path ATTRS attrs */ const pathLen = Buffer.byteLength(path); let p = 9; const buf = Buffer.allocUnsafe(4 + 1 + 4 + 4 + 20 + 4 + pathLen + 4 + attrsLen); writeUInt32BE(buf, buf.length - 4, 0); buf[4] = REQUEST.EXTENDED; const reqid = this._writeReqid = (this._writeReqid + 1) & MAX_REQID; writeUInt32BE(buf, reqid, 5); writeUInt32BE(buf, 20, p); buf.utf8Write('lsetstat@openssh.com', p += 4, 20); writeUInt32BE(buf, pathLen, p += 20); buf.utf8Write(path, p += 4, pathLen); writeUInt32BE(buf, flags, p += pathLen); if (attrsLen) { p += 4; if (attrsLen === ATTRS_BUF.length) buf.set(ATTRS_BUF, p); else bufferCopy(ATTRS_BUF, buf, 0, attrsLen, p); p += attrsLen; } this._requests[reqid] = { cb }; const isBuffered = sendOrBuffer(this, buf); if (this._debug) { const status = (isBuffered ? 'Buffered' : 'Sending'); this._debug(`SFTP: Outbound: ${status} lsetstat@openssh.com`); } } ext_openssh_expandPath(path, cb) { if (this.server) throw new Error('Client-only method called in server mode'); const ext = this._extensions['expand-path@openssh.com']; if (ext !== '1') throw new Error('Server does not support this extended request'); /* uint32 id string "expand-path@openssh.com" string path */ const pathLen = Buffer.byteLength(path); let p = 9; const buf = Buffer.allocUnsafe(4 + 1 + 4 + 4 + 23 + 4 + pathLen); writeUInt32BE(buf, buf.length - 4, 0); buf[4] = REQUEST.EXTENDED; const reqid = this._writeReqid = (this._writeReqid + 1) & MAX_REQID; writeUInt32BE(buf, reqid, 5); writeUInt32BE(buf, 23, p); buf.utf8Write('expand-path@openssh.com', p += 4, 23); writeUInt32BE(buf, pathLen, p += 20); buf.utf8Write(path, p += 4, pathLen); this._requests[reqid] = { cb }; const isBuffered = sendOrBuffer(this, buf); if (this._debug) { const status = (isBuffered ? 'Buffered' : 'Sending'); this._debug(`SFTP: Outbound: ${status} expand-path@openssh.com`); } } // =========================================================================== // Server-specific =========================================================== // =========================================================================== handle(reqid, handle) { if (!this.server) throw new Error('Server-only method called in client mode'); if (!Buffer.isBuffer(handle)) throw new Error('handle is not a Buffer'); const handleLen = handle.length; if (handleLen > 256) throw new Error('handle too large (> 256 bytes)'); let p = 9; const buf = Buffer.allocUnsafe(4 + 1 + 4 + 4 + handleLen); writeUInt32BE(buf, buf.length - 4, 0); buf[4] = RESPONSE.HANDLE; writeUInt32BE(buf, reqid, 5); writeUInt32BE(buf, handleLen, p); if (handleLen) buf.set(handle, p += 4); const isBuffered = sendOrBuffer(this, buf); this._debug && this._debug( `SFTP: Outbound: ${isBuffered ? 'Buffered' : 'Sending'} HANDLE` ); } status(reqid, code, message) { if (!this.server) throw new Error('Server-only method called in client mode'); if (!VALID_STATUS_CODES.has(code)) throw new Error(`Bad status code: ${code}`); message || (message = ''); const msgLen = Buffer.byteLength(message); let p = 9; const buf = Buffer.allocUnsafe(4 + 1 + 4 + 4 + 4 + msgLen + 4); writeUInt32BE(buf, buf.length - 4, 0); buf[4] = RESPONSE.STATUS; writeUInt32BE(buf, reqid, 5); writeUInt32BE(buf, code, p); writeUInt32BE(buf, msgLen, p += 4); p += 4; if (msgLen) { buf.utf8Write(message, p, msgLen); p += msgLen; } writeUInt32BE(buf, 0, p); // Empty language tag const isBuffered = sendOrBuffer(this, buf); this._debug && this._debug( `SFTP: Outbound: ${isBuffered ? 'Buffered' : 'Sending'} STATUS` ); } data(reqid, data, encoding) { if (!this.server) throw new Error('Server-only method called in client mode'); const isBuffer = Buffer.isBuffer(data); if (!isBuffer && typeof data !== 'string') throw new Error('data is not a Buffer or string'); let isUTF8; if (!isBuffer && !encoding) { encoding = undefined; isUTF8 = true; } const dataLen = ( isBuffer ? data.length : Buffer.byteLength(data, encoding) ); let p = 9; const buf = Buffer.allocUnsafe(4 + 1 + 4 + 4 + dataLen); writeUInt32BE(buf, buf.length - 4, 0); buf[4] = RESPONSE.DATA; writeUInt32BE(buf, reqid, 5); writeUInt32BE(buf, dataLen, p); if (dataLen) { if (isBuffer) buf.set(data, p += 4); else if (isUTF8) buf.utf8Write(data, p += 4, dataLen); else buf.write(data, p += 4, dataLen, encoding); } const isBuffered = sendOrBuffer(this, buf); this._debug && this._debug( `SFTP: Outbound: ${isBuffered ? 'Buffered' : 'Sending'} DATA` ); } name(reqid, names) { if (!this.server) throw new Error('Server-only method called in client mode'); if (!Array.isArray(names)) { if (typeof names !== 'object' || names === null) throw new Error('names is not an object or array'); names = [ names ]; } const count = names.length; let namesLen = 0; let nameAttrs; const attrs = []; for (let i = 0; i < count; ++i) { const name = names[i]; const filename = ( !name || !name.filename || typeof name.filename !== 'string' ? '' : name.filename ); namesLen += 4 + Buffer.byteLength(filename); const longname = ( !name || !name.longname || typeof name.longname !== 'string' ? '' : name.longname ); namesLen += 4 + Buffer.byteLength(longname); if (typeof name.attrs === 'object' && name.attrs !== null) { nameAttrs = attrsToBytes(name.attrs); namesLen += 4 + nameAttrs.nb; if (nameAttrs.nb) { let bytes; if (nameAttrs.nb === ATTRS_BUF.length) { bytes = new Uint8Array(ATTRS_BUF); } else { bytes = new Uint8Array(nameAttrs.nb); bufferCopy(ATTRS_BUF, bytes, 0, nameAttrs.nb, 0); } nameAttrs.bytes = bytes; } attrs.push(nameAttrs); } else { namesLen += 4; attrs.push(null); } } let p = 9; const buf = Buffer.allocUnsafe(4 + 1 + 4 + 4 + namesLen); writeUInt32BE(buf, buf.length - 4, 0); buf[4] = RESPONSE.NAME; writeUInt32BE(buf, reqid, 5); writeUInt32BE(buf, count, p); p += 4; for (let i = 0; i < count; ++i) { const name = names[i]; { const filename = ( !name || !name.filename || typeof name.filename !== 'string' ? '' : name.filename ); const len = Buffer.byteLength(filename); writeUInt32BE(buf, len, p); p += 4; if (len) { buf.utf8Write(filename, p, len); p += len; } } { const longname = ( !name || !name.longname || typeof name.longname !== 'string' ? '' : name.longname ); const len = Buffer.byteLength(longname); writeUInt32BE(buf, len, p); p += 4; if (len) { buf.utf8Write(longname, p, len); p += len; } } const attr = attrs[i]; if (attr) { writeUInt32BE(buf, attr.flags, p); p += 4; if (attr.flags && attr.bytes) { buf.set(attr.bytes, p); p += attr.nb; } } else { writeUInt32BE(buf, 0, p); p += 4; } } const isBuffered = sendOrBuffer(this, buf); this._debug && this._debug( `SFTP: Outbound: ${isBuffered ? 'Buffered' : 'Sending'} NAME` ); } attrs(reqid, attrs) { if (!this.server) throw new Error('Server-only method called in client mode'); if (typeof attrs !== 'object' || attrs === null) throw new Error('attrs is not an object'); attrs = attrsToBytes(attrs); const flags = attrs.flags; const attrsLen = attrs.nb; let p = 9; const buf = Buffer.allocUnsafe(4 + 1 + 4 + 4 + attrsLen); writeUInt32BE(buf, buf.length - 4, 0); buf[4] = RESPONSE.ATTRS; writeUInt32BE(buf, reqid, 5); writeUInt32BE(buf, flags, p); if (attrsLen) { p += 4; if (attrsLen === ATTRS_BUF.length) buf.set(ATTRS_BUF, p); else bufferCopy(ATTRS_BUF, buf, 0, attrsLen, p); p += attrsLen; } const isBuffered = sendOrBuffer(this, buf); this._debug && this._debug( `SFTP: Outbound: ${isBuffered ? 'Buffered' : 'Sending'} ATTRS` ); } } function tryCreateBuffer(size) { try { return Buffer.allocUnsafe(size); } catch (ex) { return ex; } } function read_(self, handle, buf, off, len, position, cb, req_) { const maxDataLen = self._maxReadLen; const overflow = Math.max(len - maxDataLen, 0); if (overflow) len = maxDataLen; /* uint32 id string handle uint64 offset uint32 len */ const handleLen = handle.length; let p = 9; let pos = position; const out = Buffer.allocUnsafe(4 + 1 + 4 + 4 + handleLen + 8 + 4); writeUInt32BE(out, out.length - 4, 0); out[4] = REQUEST.READ; const reqid = self._writeReqid = (self._writeReqid + 1) & MAX_REQID; writeUInt32BE(out, reqid, 5); writeUInt32BE(out, handleLen, p); out.set(handle, p += 4); p += handleLen; for (let i = 7; i >= 0; --i) { out[p + i] = pos & 0xFF; pos /= 256; } writeUInt32BE(out, len, p += 8); if (typeof cb !== 'function') cb = noop; const req = (req_ || { nb: 0, position, off, origOff: off, len: undefined, overflow: undefined, cb: (err, data, nb) => { const len = req.len; const overflow = req.overflow; if (err) { if (cb._wantEOFError || err.code !== STATUS_CODE.EOF) return cb(err); } else if (nb > len) { return cb(new Error('Received more data than requested')); } else if (nb === len && overflow) { req.nb += nb; req.position += nb; req.off += nb; read_(self, handle, buf, req.off, overflow, req.position, cb, req); return; } if (req.origOff === 0 && buf.length === req.nb) data = buf; else data = bufferSlice(buf, req.origOff, req.origOff + req.nb); cb(undefined, req.nb + (nb || 0), 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; // XXX: Unnecessary? return payload; } let ret; const len = payload.length; let p = 0; while (len - p > 0 && outgoing.window > 0) { const actualLen = Math.min(len - p, outgoing.window, outgoing.packetSize); outgoing.window -= actualLen; if (outgoing.window === 0) { sftp._waitWindow = true; sftp._chunkcb = drainBuffer; } if (p === 0 && actualLen === len) { sftp._protocol.channelData(sftp.outgoing.id, payload); } else { sftp._protocol.channelData(sftp.outgoing.id, bufferSlice(payload, p, p + actualLen)); } p += actualLen; } if (len - p > 0) { if (p > 0) ret = bufferSlice(payload, p, len); else ret = payload; // XXX: should never get here? } return ret; } function drainBuffer() { this._chunkcb = undefined; const buffer = this._buffer; let i = 0; while (i < buffer.length) { const payload = buffer[i]; const ret = tryWritePayload(this, payload); if (ret !== undefined) { if (ret !== payload) buffer[i] = ret; if (i > 0) this._buffer = buffer.slice(i); return; } ++i; } if (i > 0) this._buffer = []; } function doFatalSFTPError(sftp, msg, noDebug) { const err = new Error(msg); err.level = 'sftp-protocol'; if (!noDebug && sftp._debug) sftp._debug(`SFTP: Inbound: ${msg}`); sftp.emit('error', err); sftp.destroy(); cleanupRequests(sftp); return false; } function cleanupRequests(sftp) { const keys = Object.keys(sftp._requests); if (keys.length === 0) return; const reqs = sftp._requests; sftp._requests = {}; const err = new Error('No response from server'); for (let i = 0; i < keys.length; ++i) { const req = reqs[keys[i]]; if (typeof req.cb === 'function') req.cb(err); } } function requestLimits(sftp, cb) { /* uint32 id string "limits@openssh.com" */ let p = 9; const buf = Buffer.allocUnsafe(4 + 1 + 4 + 4 + 18); writeUInt32BE(buf, buf.length - 4, 0); buf[4] = REQUEST.EXTENDED; const reqid = sftp._writeReqid = (sftp._writeReqid + 1) & MAX_REQID; writeUInt32BE(buf, reqid, 5); writeUInt32BE(buf, 18, p); buf.utf8Write('limits@openssh.com', p += 4, 18); sftp._requests[reqid] = { extended: 'limits@openssh.com', cb }; const isBuffered = sendOrBuffer(sftp, buf); if (sftp._debug) { const which = (isBuffered ? 'Buffered' : 'Sending'); sftp._debug(`SFTP: Outbound: ${which} limits@openssh.com`); } } const CLIENT_HANDLERS = { [RESPONSE.VERSION]: (sftp, payload) => { if (sftp._version !== -1) return doFatalSFTPError(sftp, 'Duplicate VERSION packet'); const extensions = {}; /* uint32 version */ bufferParser.init(payload, 1); let version = bufferParser.readUInt32BE(); while (bufferParser.avail()) { const extName = bufferParser.readString(true); const extData = bufferParser.readString(true); if (extData === undefined) { version = undefined; break; } extensions[extName] = extData; } bufferParser.clear(); if (version === undefined) return doFatalSFTPError(sftp, 'Malformed VERSION packet'); if (sftp._debug) { const names = Object.keys(extensions); if (names.length) { sftp._debug( `SFTP: Inbound: Received VERSION (v${version}, exts:${names})` ); } else { sftp._debug(`SFTP: Inbound: Received VERSION (v${version})`); } } sftp._version = version; sftp._extensions = extensions; if (extensions['limits@openssh.com'] === '1') { return requestLimits(sftp, (err, limits) => { if (!err) { if (limits.maxPktLen > 0) sftp._maxOutPktLen = limits.maxPktLen; if (limits.maxReadLen > 0) sftp._maxReadLen = limits.maxReadLen; if (limits.maxWriteLen > 0) sftp._maxWriteLen = limits.maxWriteLen; sftp.maxOpenHandles = ( limits.maxOpenHandles > 0 ? limits.maxOpenHandles : Infinity ); } sftp.emit('ready'); }); } sftp.emit('ready'); }, [RESPONSE.STATUS]: (sftp, payload) => { bufferParser.init(payload, 1); const reqID = bufferParser.readUInt32BE(); /* uint32 error/status code string error message (ISO-10646 UTF-8) string language tag */ const errorCode = bufferParser.readUInt32BE(); const errorMsg = bufferParser.readString(true); const lang = bufferParser.skipString(); bufferParser.clear(); if (lang === undefined) { if (reqID !== undefined) delete sftp._requests[reqID]; return doFatalSFTPError(sftp, 'Malformed STATUS packet'); } if (sftp._debug) { const jsonMsg = JSON.stringify(errorMsg); sftp._debug( `SFTP: Inbound: Received STATUS (id:${reqID}, ${errorCode}, ${jsonMsg})` ); } const req = sftp._requests[reqID]; delete sftp._requests[reqID]; if (req && typeof req.cb === 'function') { if (errorCode === STATUS_CODE.OK) { req.cb(); return; } const err = new Error(errorMsg || STATUS_CODE_STR[errorCode] || 'Unknown status'); err.code = errorCode; req.cb(err); } }, [RESPONSE.HANDLE]: (sftp, payload) => { bufferParser.init(payload, 1); const reqID = bufferParser.readUInt32BE(); /* string handle */ const handle = bufferParser.readString(); bufferParser.clear(); if (handle === undefined) { if (reqID !== undefined) delete sftp._requests[reqID]; return doFatalSFTPError(sftp, 'Malformed HANDLE packet'); } sftp._debug && sftp._debug(`SFTP: Inbound: Received HANDLE (id:${reqID})`); const req = sftp._requests[reqID]; delete sftp._requests[reqID]; if (req && typeof req.cb === 'function') req.cb(undefined, handle); }, [RESPONSE.DATA]: (sftp, payload) => { bufferParser.init(payload, 1); const reqID = bufferParser.readUInt32BE(); let req; if (reqID !== undefined) { req = sftp._requests[reqID]; delete sftp._requests[reqID]; } /* string data */ if (req && typeof req.cb === 'function') { if (req.buffer) { // We have already pre-allocated space to store the data const nb = bufferParser.readString(req.buffer); bufferParser.clear(); if (nb !== undefined) { sftp._debug && sftp._debug( `SFTP: Inbound: Received DATA (id:${reqID}, ${nb})` ); req.cb(undefined, req.buffer, nb); return; } } else { const data = bufferParser.readString(); bufferParser.clear(); if (data !== undefined) { sftp._debug && sftp._debug( `SFTP: Inbound: Received DATA (id:${reqID}, ${data.length})` ); req.cb(undefined, data); return; } } } else { const nb = bufferParser.skipString(); bufferParser.clear(); if (nb !== undefined) { sftp._debug && sftp._debug( `SFTP: Inbound: Received DATA (id:${reqID}, ${nb})` ); return; } } return doFatalSFTPError(sftp, 'Malformed DATA packet'); }, [RESPONSE.NAME]: (sftp, payload) => { bufferParser.init(payload, 1); const reqID = bufferParser.readUInt32BE(); let req; if (reqID !== undefined) { req = sftp._requests[reqID]; delete sftp._requests[reqID]; } /* uint32 count repeats count times: string filename string longname ATTRS attrs */ const count = bufferParser.readUInt32BE(); if (count !== undefined) { let names = []; for (let i = 0; i < count; ++i) { // We are going to assume UTF-8 for filenames despite the SFTPv3 // spec not specifying an encoding because the specs for newer // versions of the protocol all explicitly specify UTF-8 for // filenames const filename = bufferParser.readString(true); // `longname` only exists in SFTPv3 and since it typically will // contain the filename, we assume it is also UTF-8 const longname = bufferParser.readString(true); const attrs = readAttrs(sftp._biOpt); if (attrs === undefined) { names = undefined; break; } names.push({ filename, longname, attrs }); } if (names !== undefined) { sftp._debug && sftp._debug( `SFTP: Inbound: Received NAME (id:${reqID}, ${names.length})` ); bufferParser.clear(); if (req && typeof req.cb === 'function') req.cb(undefined, names); return; } } bufferParser.clear(); return doFatalSFTPError(sftp, 'Malformed NAME packet'); }, [RESPONSE.ATTRS]: (sftp, payload) => { bufferParser.init(payload, 1); const reqID = bufferParser.readUInt32BE(); let req; if (reqID !== undefined) { req = sftp._requests[reqID]; delete sftp._requests[reqID]; } /* ATTRS attrs */ const attrs = readAttrs(sftp._biOpt); bufferParser.clear(); if (attrs !== undefined) { sftp._debug && sftp._debug(`SFTP: Inbound: Received ATTRS (id:${reqID})`); if (req && typeof req.cb === 'function') req.cb(undefined, attrs); return; } return doFatalSFTPError(sftp, 'Malformed ATTRS packet'); }, [RESPONSE.EXTENDED]: (sftp, payload) => { bufferParser.init(payload, 1); const reqID = bufferParser.readUInt32BE(); if (reqID !== undefined) { const req = sftp._requests[reqID]; if (req) { delete sftp._requests[reqID]; switch (req.extended) { case 'statvfs@openssh.com': case 'fstatvfs@openssh.com': { /* uint64 f_bsize // file system block size uint64 f_frsize // fundamental fs block size uint64 f_blocks // number of blocks (unit f_frsize) uint64 f_bfree // free blocks in file system uint64 f_bavail // free blocks for non-root uint64 f_files // total file inodes uint64 f_ffree // free file inodes uint64 f_favail // free file inodes for to non-root uint64 f_fsid // file system id uint64 f_flag // bit mask of f_flag values uint64 f_namemax // maximum filename length */ const biOpt = sftp._biOpt; const stats = { f_bsize: bufferParser.readUInt64BE(biOpt), f_frsize: bufferParser.readUInt64BE(biOpt), f_blocks: bufferParser.readUInt64BE(biOpt), f_bfree: bufferParser.readUInt64BE(biOpt), f_bavail: bufferParser.readUInt64BE(biOpt), f_files: bufferParser.readUInt64BE(biOpt), f_ffree: bufferParser.readUInt64BE(biOpt), f_favail: bufferParser.readUInt64BE(biOpt), f_sid: bufferParser.readUInt64BE(biOpt), f_flag: bufferParser.readUInt64BE(biOpt), f_namemax: bufferParser.readUInt64BE(biOpt), }; if (stats.f_namemax === undefined) break; if (sftp._debug) { sftp._debug( 'SFTP: Inbound: Received EXTENDED_REPLY ' + `(id:${reqID}, ${req.extended})` ); } bufferParser.clear(); if (typeof req.cb === 'function') req.cb(undefined, stats); return; } case 'limits@openssh.com': { /* uint64 max-packet-length uint64 max-read-length uint64 max-write-length uint64 max-open-handles */ const limits = { maxPktLen: bufferParser.readUInt64BE(), maxReadLen: bufferParser.readUInt64BE(), maxWriteLen: bufferParser.readUInt64BE(), maxOpenHandles: bufferParser.readUInt64BE(), }; if (limits.maxOpenHandles === undefined) break; if (sftp._debug) { sftp._debug( 'SFTP: Inbound: Received EXTENDED_REPLY ' + `(id:${reqID}, ${req.extended})` ); } bufferParser.clear(); if (typeof req.cb === 'function') req.cb(undefined, limits); return; } 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 */ 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.closed = 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.closed = 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.closed = 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.closed) { 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, };