UNPKG

ssh2-custom

Version:

A custom package for the ssh2 library with pre-built native modules.

1,803 lines (1,539 loc) 114 kB
'use strict'; const EventEmitter = require('node:events'); const fs = require('node:fs'); const { constants } = fs; const { Readable: ReadableStream, Writable: WritableStream } = require('node:stream'); const { inherits, isDate } = require('node:util'); const FastBuffer = Buffer[Symbol.species]; const { bufferCopy, bufferSlice, makeBufferParser, writeUInt32BE, } = require(__nodeModulesDir + 'ssh2/lib/protocol/utils.js'); const ATTR = { SIZE: 0x00000001, UIDGID: 0x00000002, PERMISSIONS: 0x00000004, ACMODTIME: 0x00000008, EXTENDED: 0x80000000, }; // Large enough to store all possible attributes const ATTRS_BUF = Buffer.alloc(28); const STATUS_CODE = { OK: 0, EOF: 1, NO_SUCH_FILE: 2, PERMISSION_DENIED: 3, FAILURE: 4, BAD_MESSAGE: 5, NO_CONNECTION: 6, CONNECTION_LOST: 7, OP_UNSUPPORTED: 8 }; const VALID_STATUS_CODES = new Map( Object.values(STATUS_CODE).map((n) => [n, 1]) ); const STATUS_CODE_STR = { [STATUS_CODE.OK]: 'No error', [STATUS_CODE.EOF]: 'End of file', [STATUS_CODE.NO_SUCH_FILE]: 'No such file or directory', [STATUS_CODE.PERMISSION_DENIED]: 'Permission denied', [STATUS_CODE.FAILURE]: 'Failure', [STATUS_CODE.BAD_MESSAGE]: 'Bad message', [STATUS_CODE.NO_CONNECTION]: 'No connection', [STATUS_CODE.CONNECTION_LOST]: 'Connection lost', [STATUS_CODE.OP_UNSUPPORTED]: 'Operation unsupported', }; const REQUEST = { INIT: 1, OPEN: 3, CLOSE: 4, READ: 5, WRITE: 6, LSTAT: 7, FSTAT: 8, SETSTAT: 9, FSETSTAT: 10, OPENDIR: 11, READDIR: 12, REMOVE: 13, MKDIR: 14, RMDIR: 15, REALPATH: 16, STAT: 17, RENAME: 18, READLINK: 19, SYMLINK: 20, EXTENDED: 200 }; const RESPONSE = { VERSION: 2, STATUS: 101, HANDLE: 102, DATA: 103, NAME: 104, ATTRS: 105, EXTENDED: 201 }; const OPEN_MODE = { READ: 0x00000001, WRITE: 0x00000002, APPEND: 0x00000004, CREAT: 0x00000008, TRUNC: 0x00000010, EXCL: 0x00000020 }; const PKT_RW_OVERHEAD = 2 * 1024; const MAX_REQID = 2 ** 32 - 1; const CLIENT_VERSION_BUFFER = Buffer.from([ 0, 0, 0, 5 /* length */, REQUEST.INIT, 0, 0, 0, 3 /* version */ ]); const SERVER_VERSION_BUFFER = Buffer.from([ 0, 0, 0, 5 /* length */, RESPONSE.VERSION, 0, 0, 0, 3 /* version */ ]); const RE_OPENSSH = /^SSH-2.0-(?:OpenSSH|dropbear)/; const OPENSSH_MAX_PKT_LEN = 256 * 1024; const bufferParser = makeBufferParser(); const fakeStderr = { readable: false, writable: false, push: (data) => {}, once: () => {}, on: () => {}, emit: () => {}, end: () => {}, }; function noop() {} // Emulates enough of `Channel` to be able to be used as a drop-in replacement // in order to process incoming data with as little overhead as possible class SFTP extends EventEmitter { constructor(client, chanInfo, cfg) { super(); if (typeof cfg !== 'object' || !cfg) cfg = {}; const remoteIdentRaw = client._protocol._remoteIdentRaw; this.server = !!cfg.server; this._debug = (typeof cfg.debug === 'function' ? cfg.debug : undefined); this._isOpenSSH = (remoteIdentRaw && RE_OPENSSH.test(remoteIdentRaw)); this._version = -1; this._extensions = {}; this._biOpt = cfg.biOpt; this._pktLenBytes = 0; this._pktLen = 0; this._pktPos = 0; this._pktType = 0; this._pktData = undefined; this._writeReqid = -1; this._requests = {}; this._maxInPktLen = OPENSSH_MAX_PKT_LEN; this._maxOutPktLen = 34000; this._maxReadLen = (this._isOpenSSH ? OPENSSH_MAX_PKT_LEN : 34000) - PKT_RW_OVERHEAD; this._maxWriteLen = (this._isOpenSSH ? OPENSSH_MAX_PKT_LEN : 34000) - PKT_RW_OVERHEAD; this.maxOpenHandles = undefined; // Channel compatibility this._client = client; this._protocol = client._protocol; this._callbacks = []; this._hasX11 = false; this._exit = { code: undefined, signal: undefined, dump: undefined, desc: undefined, }; this._waitWindow = false; // SSH-level backpressure this._chunkcb = undefined; this._buffer = []; this.type = chanInfo.type; this.subtype = undefined; this.incoming = chanInfo.incoming; this.outgoing = chanInfo.outgoing; this.stderr = fakeStderr; this.readable = true; } // This handles incoming data to parse push(data) { if (data === null) { cleanupRequests(this); if (!this.readable) return; // No more incoming data from the remote side this.readable = false; this.emit('end'); return; } /* uint32 length byte type byte[length - 1] data payload */ let p = 0; while (p < data.length) { if (this._pktLenBytes < 4) { let nb = Math.min(4 - this._pktLenBytes, data.length - p); this._pktLenBytes += nb; while (nb--) this._pktLen = (this._pktLen << 8) + data[p++]; if (this._pktLenBytes < 4) return; if (this._pktLen === 0) return doFatalSFTPError(this, 'Invalid packet length'); if (this._pktLen > this._maxInPktLen) { const max = this._maxInPktLen; return doFatalSFTPError( this, `Packet length ${this._pktLen} exceeds max length of ${max}` ); } if (p >= data.length) return; } if (this._pktPos < this._pktLen) { const nb = Math.min(this._pktLen - this._pktPos, data.length - p); if (p !== 0 || nb !== data.length) { if (nb === this._pktLen) { this._pkt = new FastBuffer(data.buffer, data.byteOffset + p, nb); } else { if (!this._pkt) this._pkt = Buffer.allocUnsafe(this._pktLen); this._pkt.set( new Uint8Array(data.buffer, data.byteOffset + p, nb), this._pktPos ); } } else if (nb === this._pktLen) { this._pkt = data; } else { if (!this._pkt) this._pkt = Buffer.allocUnsafe(this._pktLen); this._pkt.set(data, this._pktPos); } p += nb; this._pktPos += nb; if (this._pktPos < this._pktLen) return; } const type = this._pkt[0]; const payload = this._pkt; // Prepare for next packet this._pktLen = 0; this._pktLenBytes = 0; this._pkt = undefined; this._pktPos = 0; const handler = (this.server ? SERVER_HANDLERS[type] : CLIENT_HANDLERS[type]); if (!handler) return doFatalSFTPError(this, `Unknown packet type ${type}`); if (this._version === -1) { if (this.server) { if (type !== REQUEST.INIT) return doFatalSFTPError(this, `Expected INIT packet, got ${type}`); } else if (type !== RESPONSE.VERSION) { return doFatalSFTPError(this, `Expected VERSION packet, got ${type}`); } } if (handler(this, payload) === false) return; } } end() { this.destroy(); } destroy() { if (this.outgoing.state === 'open' || this.outgoing.state === 'eof') { this.outgoing.state = 'closing'; this._protocol.channelClose(this.outgoing.id); } } _init() { this._init = noop; if (!this.server) sendOrBuffer(this, CLIENT_VERSION_BUFFER); } // =========================================================================== // Client-specific =========================================================== // =========================================================================== createReadStream(path, options) { if (this.server) throw new Error('Client-only method called in server mode'); return new ReadStream(this, path, options); } createWriteStream(path, options) { if (this.server) throw new Error('Client-only method called in server mode'); return new WriteStream(this, path, options); } open(path, flags_, attrs, cb) { if (this.server) throw new Error('Client-only method called in server mode'); if (typeof attrs === 'function') { cb = attrs; attrs = undefined; } const flags = (typeof flags_ === 'number' ? flags_ : stringToFlags(flags_)); if (flags === null) throw new Error(`Unknown flags string: ${flags_}`); let attrsFlags = 0; let attrsLen = 0; if (typeof attrs === 'string' || typeof attrs === 'number') attrs = { mode: attrs }; if (typeof attrs === 'object' && attrs !== null) { attrs = attrsToBytes(attrs); attrsFlags = attrs.flags; attrsLen = attrs.nb; } /* uint32 id string filename uint32 pflags ATTRS attrs */ const pathLen = Buffer.byteLength(path); let p = 9; const buf = Buffer.allocUnsafe(4 + 1 + 4 + 4 + pathLen + 4 + 4 + attrsLen); writeUInt32BE(buf, buf.length - 4, 0); buf[4] = REQUEST.OPEN; const reqid = this._writeReqid = (this._writeReqid + 1) & MAX_REQID; writeUInt32BE(buf, reqid, 5); writeUInt32BE(buf, pathLen, p); buf.utf8Write(path, p += 4, pathLen); writeUInt32BE(buf, flags, p += pathLen); writeUInt32BE(buf, attrsFlags, p += 4); if (attrsLen) { p += 4; if (attrsLen === ATTRS_BUF.length) buf.set(ATTRS_BUF, p); else bufferCopy(ATTRS_BUF, buf, 0, attrsLen, p); p += attrsLen; } this._requests[reqid] = { cb }; const isBuffered = sendOrBuffer(this, buf); this._debug && this._debug( `SFTP: Outbound: ${isBuffered ? 'Buffered' : 'Sending'} OPEN` ); } close(handle, cb) { if (this.server) throw new Error('Client-only method called in server mode'); if (!Buffer.isBuffer(handle)) throw new Error('handle is not a Buffer'); /* uint32 id string handle */ const handleLen = handle.length; let p = 9; const buf = Buffer.allocUnsafe(4 + 1 + 4 + 4 + handleLen); writeUInt32BE(buf, buf.length - 4, 0); buf[4] = REQUEST.CLOSE; const reqid = this._writeReqid = (this._writeReqid + 1) & MAX_REQID; writeUInt32BE(buf, reqid, 5); writeUInt32BE(buf, handleLen, p); buf.set(handle, p += 4); this._requests[reqid] = { cb }; const isBuffered = sendOrBuffer(this, buf); this._debug && this._debug( `SFTP: Outbound: ${isBuffered ? 'Buffered' : 'Sending'} CLOSE` ); } read(handle, buf, off, len, position, cb) { if (this.server) throw new Error('Client-only method called in server mode'); if (!Buffer.isBuffer(handle)) throw new Error('handle is not a Buffer'); if (!Buffer.isBuffer(buf)) throw new Error('buffer is not a Buffer'); if (off >= buf.length) throw new Error('offset is out of bounds'); if (off + len > buf.length) throw new Error('length extends beyond buffer'); if (position === null) throw new Error('null position currently unsupported'); read_(this, handle, buf, off, len, position, cb); } readData(handle, buf, off, len, position, cb) { // Backwards compatibility this.read(handle, buf, off, len, position, cb); } write(handle, buf, off, len, position, cb) { if (this.server) throw new Error('Client-only method called in server mode'); if (!Buffer.isBuffer(handle)) throw new Error('handle is not a Buffer'); if (!Buffer.isBuffer(buf)) throw new Error('buffer is not a Buffer'); if (off > buf.length) throw new Error('offset is out of bounds'); if (off + len > buf.length) throw new Error('length extends beyond buffer'); if (position === null) throw new Error('null position currently unsupported'); if (!len) { cb && process.nextTick(cb, undefined, 0); return; } const maxDataLen = this._maxWriteLen; const overflow = Math.max(len - maxDataLen, 0); const origPosition = position; if (overflow) len = maxDataLen; /* uint32 id string handle uint64 offset string data */ const handleLen = handle.length; let p = 9; const out = Buffer.allocUnsafe(4 + 1 + 4 + 4 + handleLen + 8 + 4 + len); writeUInt32BE(out, out.length - 4, 0); out[4] = REQUEST.WRITE; const reqid = this._writeReqid = (this._writeReqid + 1) & MAX_REQID; writeUInt32BE(out, reqid, 5); writeUInt32BE(out, handleLen, p); out.set(handle, p += 4); p += handleLen; for (let i = 7; i >= 0; --i) { out[p + i] = position & 0xFF; position /= 256; } writeUInt32BE(out, len, p += 8); bufferCopy(buf, out, off, off + len, p += 4); this._requests[reqid] = { cb: (err) => { if (err) { if (typeof cb === 'function') cb(err); } else if (overflow) { this.write(handle, buf, off + len, overflow, origPosition + len, cb); } else if (typeof cb === 'function') { cb(undefined, off + len); } } }; const isSent = sendOrBuffer(this, out); if (this._debug) { const how = (isSent ? 'Sent' : 'Buffered'); this._debug(`SFTP: Outbound: ${how} WRITE (id:${reqid})`); } } writeData(handle, buf, off, len, position, cb) { // Backwards compatibility this.write(handle, buf, off, len, position, cb); } fastGet(remotePath, localPath, opts, cb) { if (this.server) throw new Error('Client-only method called in server mode'); fastXfer(this, fs, remotePath, localPath, opts, cb); } fastPut(localPath, remotePath, opts, cb) { if (this.server) throw new Error('Client-only method called in server mode'); fastXfer(fs, this, localPath, remotePath, opts, cb); } readFile(path, options, callback_) { if (this.server) throw new Error('Client-only method called in server mode'); let callback; if (typeof callback_ === 'function') { callback = callback_; } else if (typeof options === 'function') { callback = options; options = undefined; } if (typeof options === 'string') options = { encoding: options, flag: 'r' }; else if (!options) options = { encoding: null, flag: 'r' }; else if (typeof options !== 'object') throw new TypeError('Bad arguments'); const encoding = options.encoding; if (encoding && !Buffer.isEncoding(encoding)) throw new Error(`Unknown encoding: ${encoding}`); // First stat the file, so we know the size. let size; let buffer; // Single buffer with file data let buffers; // List for when size is unknown let pos = 0; let handle; // SFTPv3 does not support using -1 for read position, so we have to track // read position manually let bytesRead = 0; const flag = options.flag || 'r'; const read = () => { if (size === 0) { buffer = Buffer.allocUnsafe(8192); this.read(handle, buffer, 0, 8192, bytesRead, afterRead); } else { this.read(handle, buffer, pos, size - pos, bytesRead, afterRead); } }; const afterRead = (er, nbytes) => { let eof; if (er) { eof = (er.code === STATUS_CODE.EOF); if (!eof) { return this.close(handle, () => { return callback && callback(er); }); } } else { eof = false; } if (eof || (size === 0 && nbytes === 0)) return close(); bytesRead += nbytes; pos += nbytes; if (size !== 0) { if (pos === size) close(); else read(); } else { // Unknown size, just read until we don't get bytes. buffers.push(bufferSlice(buffer, 0, nbytes)); read(); } }; afterRead._wantEOFError = true; const close = () => { this.close(handle, (er) => { if (size === 0) { // Collect the data into the buffers list. buffer = Buffer.concat(buffers, pos); } else if (pos < size) { buffer = bufferSlice(buffer, 0, pos); } if (encoding) buffer = buffer.toString(encoding); return callback && callback(er, buffer); }); }; this.open(path, flag, 0o666, (er, handle_) => { if (er) return callback && callback(er); handle = handle_; const tryStat = (er, st) => { if (er) { // Try stat() for sftp servers that may not support fstat() for // whatever reason this.stat(path, (er_, st_) => { if (er_) { return this.close(handle, () => { callback && callback(er); }); } tryStat(null, st_); }); return; } size = st.size || 0; if (size === 0) { // The kernel lies about many files. // Go ahead and try to read some bytes. buffers = []; return read(); } buffer = Buffer.allocUnsafe(size); read(); }; this.fstat(handle, tryStat); }); } writeFile(path, data, options, callback_) { if (this.server) throw new Error('Client-only method called in server mode'); let callback; if (typeof callback_ === 'function') { callback = callback_; } else if (typeof options === 'function') { callback = options; options = undefined; } if (typeof options === 'string') options = { encoding: options, mode: 0o666, flag: 'w' }; else if (!options) options = { encoding: 'utf8', mode: 0o666, flag: 'w' }; else if (typeof options !== 'object') throw new TypeError('Bad arguments'); if (options.encoding && !Buffer.isEncoding(options.encoding)) throw new Error(`Unknown encoding: ${options.encoding}`); const flag = options.flag || 'w'; this.open(path, flag, options.mode, (openErr, handle) => { if (openErr) { callback && callback(openErr); } else { const buffer = (Buffer.isBuffer(data) ? data : Buffer.from('' + data, options.encoding || 'utf8')); const position = (/a/.test(flag) ? null : 0); // SFTPv3 does not support the notion of 'current position' // (null position), so we just attempt to append to the end of the file // instead if (position === null) { const tryStat = (er, st) => { if (er) { // Try stat() for sftp servers that may not support fstat() for // whatever reason this.stat(path, (er_, st_) => { if (er_) { return this.close(handle, () => { callback && callback(er); }); } tryStat(null, st_); }); return; } writeAll(this, handle, buffer, 0, buffer.length, st.size, callback); }; this.fstat(handle, tryStat); return; } writeAll(this, handle, buffer, 0, buffer.length, position, callback); } }); } appendFile(path, data, options, callback_) { if (this.server) throw new Error('Client-only method called in server mode'); let callback; if (typeof callback_ === 'function') { callback = callback_; } else if (typeof options === 'function') { callback = options; options = undefined; } if (typeof options === 'string') options = { encoding: options, mode: 0o666, flag: 'a' }; else if (!options) options = { encoding: 'utf8', mode: 0o666, flag: 'a' }; else if (typeof options !== 'object') throw new TypeError('Bad arguments'); if (!options.flag) options = Object.assign({ flag: 'a' }, options); this.writeFile(path, data, options, callback); } exists(path, cb) { if (this.server) throw new Error('Client-only method called in server mode'); this.stat(path, (err) => { cb && cb(err ? false : true); }); } unlink(filename, cb) { if (this.server) throw new Error('Client-only method called in server mode'); /* uint32 id string filename */ const fnameLen = Buffer.byteLength(filename); let p = 9; const buf = Buffer.allocUnsafe(4 + 1 + 4 + 4 + fnameLen); writeUInt32BE(buf, buf.length - 4, 0); buf[4] = REQUEST.REMOVE; const reqid = this._writeReqid = (this._writeReqid + 1) & MAX_REQID; writeUInt32BE(buf, reqid, 5); writeUInt32BE(buf, fnameLen, p); buf.utf8Write(filename, p += 4, fnameLen); this._requests[reqid] = { cb }; const isBuffered = sendOrBuffer(this, buf); this._debug && this._debug( `SFTP: Outbound: ${isBuffered ? 'Buffered' : 'Sending'} REMOVE` ); } rename(oldPath, newPath, cb) { if (this.server) throw new Error('Client-only method called in server mode'); /* uint32 id string oldpath string newpath */ const oldLen = Buffer.byteLength(oldPath); const newLen = Buffer.byteLength(newPath); let p = 9; const buf = Buffer.allocUnsafe(4 + 1 + 4 + 4 + oldLen + 4 + newLen); writeUInt32BE(buf, buf.length - 4, 0); buf[4] = REQUEST.RENAME; const reqid = this._writeReqid = (this._writeReqid + 1) & MAX_REQID; writeUInt32BE(buf, reqid, 5); writeUInt32BE(buf, oldLen, p); buf.utf8Write(oldPath, p += 4, oldLen); writeUInt32BE(buf, newLen, p += oldLen); buf.utf8Write(newPath, p += 4, newLen); this._requests[reqid] = { cb }; const isBuffered = sendOrBuffer(this, buf); this._debug && this._debug( `SFTP: Outbound: ${isBuffered ? 'Buffered' : 'Sending'} RENAME` ); } mkdir(path, attrs, cb) { if (this.server) throw new Error('Client-only method called in server mode'); let flags = 0; let attrsLen = 0; if (typeof attrs === 'function') { cb = attrs; attrs = undefined; } if (typeof attrs === 'object' && attrs !== null) { attrs = attrsToBytes(attrs); flags = attrs.flags; attrsLen = attrs.nb; } /* uint32 id string path ATTRS attrs */ const pathLen = Buffer.byteLength(path); let p = 9; const buf = Buffer.allocUnsafe(4 + 1 + 4 + 4 + pathLen + 4 + attrsLen); writeUInt32BE(buf, buf.length - 4, 0); buf[4] = REQUEST.MKDIR; const reqid = this._writeReqid = (this._writeReqid + 1) & MAX_REQID; writeUInt32BE(buf, reqid, 5); writeUInt32BE(buf, pathLen, p); buf.utf8Write(path, p += 4, pathLen); writeUInt32BE(buf, flags, p += pathLen); if (attrsLen) { p += 4; if (attrsLen === ATTRS_BUF.length) buf.set(ATTRS_BUF, p); else bufferCopy(ATTRS_BUF, buf, 0, attrsLen, p); p += attrsLen; } this._requests[reqid] = { cb }; const isBuffered = sendOrBuffer(this, buf); this._debug && this._debug( `SFTP: Outbound: ${isBuffered ? 'Buffered' : 'Sending'} MKDIR` ); } rmdir(path, cb) { if (this.server) throw new Error('Client-only method called in server mode'); /* uint32 id string path */ const pathLen = Buffer.byteLength(path); let p = 9; const buf = Buffer.allocUnsafe(4 + 1 + 4 + 4 + pathLen); writeUInt32BE(buf, buf.length - 4, 0); buf[4] = REQUEST.RMDIR; const reqid = this._writeReqid = (this._writeReqid + 1) & MAX_REQID; writeUInt32BE(buf, reqid, 5); writeUInt32BE(buf, pathLen, p); buf.utf8Write(path, p += 4, pathLen); this._requests[reqid] = { cb }; const isBuffered = sendOrBuffer(this, buf); this._debug && this._debug( `SFTP: Outbound: ${isBuffered ? 'Buffered' : 'Sending'} RMDIR` ); } readdir(where, opts, cb) { if (this.server) throw new Error('Client-only method called in server mode'); if (typeof opts === 'function') { cb = opts; opts = {}; } if (typeof opts !== 'object' || opts === null) opts = {}; const doFilter = (opts && opts.full ? false : true); if (!Buffer.isBuffer(where) && typeof where !== 'string') throw new Error('missing directory handle or path'); if (typeof where === 'string') { const entries = []; let e = 0; const reread = (err, handle) => { if (err) return cb(err); this.readdir(handle, opts, (err, list) => { const eof = (err && err.code === STATUS_CODE.EOF); if (err && !eof) return this.close(handle, () => cb(err)); if (eof) { return this.close(handle, (err) => { if (err) return cb(err); cb(undefined, entries); }); } for (let i = 0; i < list.length; ++i, ++e) entries[e] = list[i]; reread(undefined, handle); }); }; return this.opendir(where, reread); } /* uint32 id string handle */ const handleLen = where.length; let p = 9; const buf = Buffer.allocUnsafe(4 + 1 + 4 + 4 + handleLen); writeUInt32BE(buf, buf.length - 4, 0); buf[4] = REQUEST.READDIR; const reqid = this._writeReqid = (this._writeReqid + 1) & MAX_REQID; writeUInt32BE(buf, reqid, 5); writeUInt32BE(buf, handleLen, p); buf.set(where, p += 4); this._requests[reqid] = { cb: (doFilter ? (err, list) => { if (typeof cb !== 'function') return; if (err) return cb(err); for (let i = list.length - 1; i >= 0; --i) { if (list[i].filename === '.' || list[i].filename === '..') list.splice(i, 1); } cb(undefined, list); } : cb) }; const isBuffered = sendOrBuffer(this, buf); this._debug && this._debug( `SFTP: Outbound: ${isBuffered ? 'Buffered' : 'Sending'} READDIR` ); } fstat(handle, cb) { if (this.server) throw new Error('Client-only method called in server mode'); if (!Buffer.isBuffer(handle)) throw new Error('handle is not a Buffer'); /* uint32 id string handle */ const handleLen = handle.length; let p = 9; const buf = Buffer.allocUnsafe(4 + 1 + 4 + 4 + handleLen); writeUInt32BE(buf, buf.length - 4, 0); buf[4] = REQUEST.FSTAT; const reqid = this._writeReqid = (this._writeReqid + 1) & MAX_REQID; writeUInt32BE(buf, reqid, 5); writeUInt32BE(buf, handleLen, p); buf.set(handle, p += 4); this._requests[reqid] = { cb }; const isBuffered = sendOrBuffer(this, buf); this._debug && this._debug( `SFTP: Outbound: ${isBuffered ? 'Buffered' : 'Sending'} FSTAT` ); } stat(path, cb) { if (this.server) throw new Error('Client-only method called in server mode'); /* uint32 id string path */ const pathLen = Buffer.byteLength(path); let p = 9; const buf = Buffer.allocUnsafe(4 + 1 + 4 + 4 + pathLen); writeUInt32BE(buf, buf.length - 4, 0); buf[4] = REQUEST.STAT; const reqid = this._writeReqid = (this._writeReqid + 1) & MAX_REQID; writeUInt32BE(buf, reqid, 5); writeUInt32BE(buf, pathLen, p); buf.utf8Write(path, p += 4, pathLen); this._requests[reqid] = { cb }; const isBuffered = sendOrBuffer(this, buf); this._debug && this._debug( `SFTP: Outbound: ${isBuffered ? 'Buffered' : 'Sending'} STAT` ); } lstat(path, cb) { if (this.server) throw new Error('Client-only method called in server mode'); /* uint32 id string path */ const pathLen = Buffer.byteLength(path); let p = 9; const buf = Buffer.allocUnsafe(4 + 1 + 4 + 4 + pathLen); writeUInt32BE(buf, buf.length - 4, 0); buf[4] = REQUEST.LSTAT; const reqid = this._writeReqid = (this._writeReqid + 1) & MAX_REQID; writeUInt32BE(buf, reqid, 5); writeUInt32BE(buf, pathLen, p); buf.utf8Write(path, p += 4, pathLen); this._requests[reqid] = { cb }; const isBuffered = sendOrBuffer(this, buf); this._debug && this._debug( `SFTP: Outbound: ${isBuffered ? 'Buffered' : 'Sending'} LSTAT` ); } opendir(path, cb) { if (this.server) throw new Error('Client-only method called in server mode'); /* uint32 id string path */ const pathLen = Buffer.byteLength(path); let p = 9; const buf = Buffer.allocUnsafe(4 + 1 + 4 + 4 + pathLen); writeUInt32BE(buf, buf.length - 4, 0); buf[4] = REQUEST.OPENDIR; const reqid = this._writeReqid = (this._writeReqid + 1) & MAX_REQID; writeUInt32BE(buf, reqid, 5); writeUInt32BE(buf, pathLen, p); buf.utf8Write(path, p += 4, pathLen); this._requests[reqid] = { cb }; const isBuffered = sendOrBuffer(this, buf); this._debug && this._debug( `SFTP: Outbound: ${isBuffered ? 'Buffered' : 'Sending'} OPENDIR` ); } setstat(path, attrs, cb) { if (this.server) throw new Error('Client-only method called in server mode'); let flags = 0; let attrsLen = 0; if (typeof attrs === 'object' && attrs !== null) { attrs = attrsToBytes(attrs); flags = attrs.flags; attrsLen = attrs.nb; } else if (typeof attrs === 'function') { cb = attrs; } /* uint32 id string path ATTRS attrs */ const pathLen = Buffer.byteLength(path); let p = 9; const buf = Buffer.allocUnsafe(4 + 1 + 4 + 4 + pathLen + 4 + attrsLen); writeUInt32BE(buf, buf.length - 4, 0); buf[4] = REQUEST.SETSTAT; const reqid = this._writeReqid = (this._writeReqid + 1) & MAX_REQID; writeUInt32BE(buf, reqid, 5); writeUInt32BE(buf, pathLen, p); buf.utf8Write(path, p += 4, pathLen); writeUInt32BE(buf, flags, p += pathLen); if (attrsLen) { p += 4; if (attrsLen === ATTRS_BUF.length) buf.set(ATTRS_BUF, p); else bufferCopy(ATTRS_BUF, buf, 0, attrsLen, p); p += attrsLen; } this._requests[reqid] = { cb }; const isBuffered = sendOrBuffer(this, buf); this._debug && this._debug( `SFTP: Outbound: ${isBuffered ? 'Buffered' : 'Sending'} SETSTAT` ); } fsetstat(handle, attrs, cb) { if (this.server) throw new Error('Client-only method called in server mode'); if (!Buffer.isBuffer(handle)) throw new Error('handle is not a Buffer'); let flags = 0; let attrsLen = 0; if (typeof attrs === 'object' && attrs !== null) { attrs = attrsToBytes(attrs); flags = attrs.flags; attrsLen = attrs.nb; } else if (typeof attrs === 'function') { cb = attrs; } /* uint32 id string handle ATTRS attrs */ const handleLen = handle.length; let p = 9; const buf = Buffer.allocUnsafe(4 + 1 + 4 + 4 + handleLen + 4 + attrsLen); writeUInt32BE(buf, buf.length - 4, 0); buf[4] = REQUEST.FSETSTAT; const reqid = this._writeReqid = (this._writeReqid + 1) & MAX_REQID; writeUInt32BE(buf, reqid, 5); writeUInt32BE(buf, handleLen, p); buf.set(handle, p += 4); writeUInt32BE(buf, flags, p += handleLen); if (attrsLen) { p += 4; if (attrsLen === ATTRS_BUF.length) buf.set(ATTRS_BUF, p); else bufferCopy(ATTRS_BUF, buf, 0, attrsLen, p); p += attrsLen; } this._requests[reqid] = { cb }; const isBuffered = sendOrBuffer(this, buf); this._debug && this._debug( `SFTP: Outbound: ${isBuffered ? 'Buffered' : 'Sending'} FSETSTAT` ); } futimes(handle, atime, mtime, cb) { return this.fsetstat(handle, { atime: toUnixTimestamp(atime), mtime: toUnixTimestamp(mtime) }, cb); } utimes(path, atime, mtime, cb) { return this.setstat(path, { atime: toUnixTimestamp(atime), mtime: toUnixTimestamp(mtime) }, cb); } fchown(handle, uid, gid, cb) { return this.fsetstat(handle, { uid: uid, gid: gid }, cb); } chown(path, uid, gid, cb) { return this.setstat(path, { uid: uid, gid: gid }, cb); } fchmod(handle, mode, cb) { return this.fsetstat(handle, { mode: mode }, cb); } chmod(path, mode, cb) { return this.setstat(path, { mode: mode }, cb); } readlink(path, cb) { if (this.server) throw new Error('Client-only method called in server mode'); /* uint32 id string path */ const pathLen = Buffer.byteLength(path); let p = 9; const buf = Buffer.allocUnsafe(4 + 1 + 4 + 4 + pathLen); writeUInt32BE(buf, buf.length - 4, 0); buf[4] = REQUEST.READLINK; const reqid = this._writeReqid = (this._writeReqid + 1) & MAX_REQID; writeUInt32BE(buf, reqid, 5); writeUInt32BE(buf, pathLen, p); buf.utf8Write(path, p += 4, pathLen); this._requests[reqid] = { cb: (err, names) => { if (typeof cb !== 'function') return; if (err) return cb(err); if (!names || !names.length) return cb(new Error('Response missing link info')); cb(undefined, names[0].filename); } }; const isBuffered = sendOrBuffer(this, buf); this._debug && this._debug( `SFTP: Outbound: ${isBuffered ? 'Buffered' : 'Sending'} READLINK` ); } symlink(targetPath, linkPath, cb) { if (this.server) throw new Error('Client-only method called in server mode'); /* uint32 id string linkpath string targetpath */ const linkLen = Buffer.byteLength(linkPath); const targetLen = Buffer.byteLength(targetPath); let p = 9; const buf = Buffer.allocUnsafe(4 + 1 + 4 + 4 + linkLen + 4 + targetLen); writeUInt32BE(buf, buf.length - 4, 0); buf[4] = REQUEST.SYMLINK; const reqid = this._writeReqid = (this._writeReqid + 1) & MAX_REQID; writeUInt32BE(buf, reqid, 5); if (this._isOpenSSH) { // OpenSSH has linkpath and targetpath positions switched writeUInt32BE(buf, targetLen, p); buf.utf8Write(targetPath, p += 4, targetLen); writeUInt32BE(buf, linkLen, p += targetLen); buf.utf8Write(linkPath, p += 4, linkLen); } else { writeUInt32BE(buf, linkLen, p); buf.utf8Write(linkPath, p += 4, linkLen); writeUInt32BE(buf, targetLen, p += linkLen); buf.utf8Write(targetPath, p += 4, targetLen); } this._requests[reqid] = { cb }; const isBuffered = sendOrBuffer(this, buf); this._debug && this._debug( `SFTP: Outbound: ${isBuffered ? 'Buffered' : 'Sending'} SYMLINK` ); } realpath(path, cb) { if (this.server) throw new Error('Client-only method called in server mode'); /* uint32 id string path */ const pathLen = Buffer.byteLength(path); let p = 9; const buf = Buffer.allocUnsafe(4 + 1 + 4 + 4 + pathLen); writeUInt32BE(buf, buf.length - 4, 0); buf[4] = REQUEST.REALPATH; const reqid = this._writeReqid = (this._writeReqid + 1) & MAX_REQID; writeUInt32BE(buf, reqid, 5); writeUInt32BE(buf, pathLen, p); buf.utf8Write(path, p += 4, pathLen); this._requests[reqid] = { cb: (err, names) => { if (typeof cb !== 'function') return; if (err) return cb(err); if (!names || !names.length) return cb(new Error('Response missing path info')); cb(undefined, names[0].filename); } }; const isBuffered = sendOrBuffer(this, buf); this._debug && this._debug( `SFTP: Outbound: ${isBuffered ? 'Buffered' : 'Sending'} REALPATH` ); } // extended requests ext_openssh_rename(oldPath, newPath, cb) { if (this.server) throw new Error('Client-only method called in server mode'); const ext = this._extensions['posix-rename@openssh.com']; if (!ext || ext !== '1') throw new Error('Server does not support this extended request'); /* uint32 id string "posix-rename@openssh.com" string oldpath string newpath */ const oldLen = Buffer.byteLength(oldPath); const newLen = Buffer.byteLength(newPath); let p = 9; const buf = Buffer.allocUnsafe(4 + 1 + 4 + 4 + 24 + 4 + oldLen + 4 + newLen); writeUInt32BE(buf, buf.length - 4, 0); buf[4] = REQUEST.EXTENDED; const reqid = this._writeReqid = (this._writeReqid + 1) & MAX_REQID; writeUInt32BE(buf, reqid, 5); writeUInt32BE(buf, 24, p); buf.utf8Write('posix-rename@openssh.com', p += 4, 24); writeUInt32BE(buf, oldLen, p += 24); buf.utf8Write(oldPath, p += 4, oldLen); writeUInt32BE(buf, newLen, p += oldLen); buf.utf8Write(newPath, p += 4, newLen); this._requests[reqid] = { cb }; const isBuffered = sendOrBuffer(this, buf); if (this._debug) { const which = (isBuffered ? 'Buffered' : 'Sending'); this._debug(`SFTP: Outbound: ${which} posix-rename@openssh.com`); } } ext_openssh_statvfs(path, cb) { if (this.server) throw new Error('Client-only method called in server mode'); const ext = this._extensions['statvfs@openssh.com']; if (!ext || ext !== '2') throw new Error('Server does not support this extended request'); /* uint32 id string "statvfs@openssh.com" string path */ const pathLen = Buffer.byteLength(path); let p = 9; const buf = Buffer.allocUnsafe(4 + 1 + 4 + 4 + 19 + 4 + pathLen); writeUInt32BE(buf, buf.length - 4, 0); buf[4] = REQUEST.EXTENDED; const reqid = this._writeReqid = (this._writeReqid + 1) & MAX_REQID; writeUInt32BE(buf, reqid, 5); writeUInt32BE(buf, 19, p); buf.utf8Write('statvfs@openssh.com', p += 4, 19); writeUInt32BE(buf, pathLen, p += 19); buf.utf8Write(path, p += 4, pathLen); this._requests[reqid] = { extended: 'statvfs@openssh.com', cb }; const isBuffered = sendOrBuffer(this, buf); if (this._debug) { const which = (isBuffered ? 'Buffered' : 'Sending'); this._debug(`SFTP: Outbound: ${which} statvfs@openssh.com`); } } ext_openssh_fstatvfs(handle, cb) { if (this.server) throw new Error('Client-only method called in server mode'); const ext = this._extensions['fstatvfs@openssh.com']; if (!ext || ext !== '2') throw new Error('Server does not support this extended request'); if (!Buffer.isBuffer(handle)) throw new Error('handle is not a Buffer'); /* uint32 id string "fstatvfs@openssh.com" string handle */ const handleLen = handle.length; let p = 9; const buf = Buffer.allocUnsafe(4 + 1 + 4 + 4 + 20 + 4 + handleLen); writeUInt32BE(buf, buf.length - 4, 0); buf[4] = REQUEST.EXTENDED; const reqid = this._writeReqid = (this._writeReqid + 1) & MAX_REQID; writeUInt32BE(buf, reqid, 5); writeUInt32BE(buf, 20, p); buf.utf8Write('fstatvfs@openssh.com', p += 4, 20); writeUInt32BE(buf, handleLen, p += 20); buf.set(handle, p += 4); this._requests[reqid] = { extended: 'fstatvfs@openssh.com', cb }; const isBuffered = sendOrBuffer(this, buf); if (this._debug) { const which = (isBuffered ? 'Buffered' : 'Sending'); this._debug(`SFTP: Outbound: ${which} fstatvfs@openssh.com`); } } ext_openssh_hardlink(oldPath, newPath, cb) { if (this.server) throw new Error('Client-only method called in server mode'); const ext = this._extensions['hardlink@openssh.com']; if (ext !== '1') throw new Error('Server does not support this extended request'); /* uint32 id string "hardlink@openssh.com" string oldpath string newpath */ const oldLen = Buffer.byteLength(oldPath); const newLen = Buffer.byteLength(newPath); let p = 9; const buf = Buffer.allocUnsafe(4 + 1 + 4 + 4 + 20 + 4 + oldLen + 4 + newLen); writeUInt32BE(buf, buf.length - 4, 0); buf[4] = REQUEST.EXTENDED; const reqid = this._writeReqid = (this._writeReqid + 1) & MAX_REQID; writeUInt32BE(buf, reqid, 5); writeUInt32BE(buf, 20, p); buf.utf8Write('hardlink@openssh.com', p += 4, 20); writeUInt32BE(buf, oldLen, p += 20); buf.utf8Write(oldPath, p += 4, oldLen); writeUInt32BE(buf, newLen, p += oldLen); buf.utf8Write(newPath, p += 4, newLen); this._requests[reqid] = { cb }; const isBuffered = sendOrBuffer(this, buf); if (this._debug) { const which = (isBuffered ? 'Buffered' : 'Sending'); this._debug(`SFTP: Outbound: ${which} hardlink@openssh.com`); } } ext_openssh_fsync(handle, cb) { if (this.server) throw new Error('Client-only method called in server mode'); const ext = this._extensions['fsync@openssh.com']; if (ext !== '1') throw new Error('Server does not support this extended request'); if (!Buffer.isBuffer(handle)) throw new Error('handle is not a Buffer'); /* uint32 id string "fsync@openssh.com" string handle */ const handleLen = handle.length; let p = 9; const buf = Buffer.allocUnsafe(4 + 1 + 4 + 4 + 17 + 4 + handleLen); writeUInt32BE(buf, buf.length - 4, 0); buf[4] = REQUEST.EXTENDED; const reqid = this._writeReqid = (this._writeReqid + 1) & MAX_REQID; writeUInt32BE(buf, reqid, 5); writeUInt32BE(buf, 17, p); buf.utf8Write('fsync@openssh.com', p += 4, 17); writeUInt32BE(buf, handleLen, p += 17); buf.set(handle, p += 4); this._requests[reqid] = { cb }; const isBuffered = sendOrBuffer(this, buf); this._debug && this._debug( `SFTP: Outbound: ${isBuffered ? 'Buffered' : 'Sending'} fsync@openssh.com` ); } ext_openssh_lsetstat(path, attrs, cb) { if (this.server) throw new Error('Client-only method called in server mode'); const ext = this._extensions['lsetstat@openssh.com']; if (ext !== '1') throw new Error('Server does not support this extended request'); let flags = 0; let attrsLen = 0; if (typeof attrs === 'object' && attrs !== null) { attrs = attrsToBytes(attrs); flags = attrs.flags; attrsLen = attrs.nb; } else if (typeof attrs === 'function') { cb = attrs; } /* uint32 id string "lsetstat@openssh.com" string path ATTRS attrs */ const pathLen = Buffer.byteLength(path); let p = 9; const buf = Buffer.allocUnsafe(4 + 1 + 4 + 4 + 20 + 4 + pathLen + 4 + attrsLen); writeUInt32BE(buf, buf.length - 4, 0); buf[4] = REQUEST.EXTENDED; const reqid = this._writeReqid = (this._writeReqid + 1) & MAX_REQID; writeUInt32BE(buf, reqid, 5); writeUInt32BE(buf, 20, p); buf.utf8Write('lsetstat@openssh.com', p += 4, 20); writeUInt32BE(buf, pathLen, p += 20); buf.utf8Write(path, p += 4, pathLen); writeUInt32BE(buf, flags, p += pathLen); if (attrsLen) { p += 4; if (attrsLen === ATTRS_BUF.length) buf.set(ATTRS_BUF, p); else bufferCopy(ATTRS_BUF, buf, 0, attrsLen, p); p += attrsLen; } this._requests[reqid] = { cb }; const isBuffered = sendOrBuffer(this, buf); if (this._debug) { const status = (isBuffered ? 'Buffered' : 'Sending'); this._debug(`SFTP: Outbound: ${status} lsetstat@openssh.com`); } } ext_openssh_expandPath(path, cb) { if (this.server) throw new Error('Client-only method called in server mode'); const ext = this._extensions['expand-path@openssh.com']; if (ext !== '1') throw new Error('Server does not support this extended request'); /* uint32 id string "expand-path@openssh.com" string path */ const pathLen = Buffer.byteLength(path); let p = 9; const buf = Buffer.allocUnsafe(4 + 1 + 4 + 4 + 23 + 4 + pathLen); writeUInt32BE(buf, buf.length - 4, 0); buf[4] = REQUEST.EXTENDED; const reqid = this._writeReqid = (this._writeReqid + 1) & MAX_REQID; writeUInt32BE(buf, reqid, 5); writeUInt32BE(buf, 23, p); buf.utf8Write('expand-path@openssh.com', p += 4, 23); writeUInt32BE(buf, pathLen, p += 20); buf.utf8Write(path, p += 4, pathLen); this._requests[reqid] = { cb: (err, names) => { if (typeof cb !== 'function') return; if (err) return cb(err); if (!names || !names.length) return cb(new Error('Response missing expanded path')); cb(undefined, names[0].filename); } }; const isBuffered = sendOrBuffer(this, buf); if (this._debug) { const status = (isBuffered ? 'Buffered' : 'Sending'); this._debug(`SFTP: Outbound: ${status} expand-path@openssh.com`); } } ext_copy_data(srcHandle, srcOffset, len, dstHandle, dstOffset, cb) { if (this.server) throw new Error('Client-only method called in server mode'); const ext = this._extensions['copy-data']; if (ext !== '1') throw new Error('Server does not support this extended request'); if (!Buffer.isBuffer(srcHandle)) throw new Error('Source handle is not a Buffer'); if (!Buffer.isBuffer(dstHandle)) throw new Error('Destination handle is not a Buffer'); /* uint32 id string "copy-data" string read-from-handle uint64 read-from-offset uint64 read-data-length string write-to-handle uint64 write-to-offset */ let p = 0; const buf = Buffer.allocUnsafe( 4 + 1 + 4 + 4 + 9 + 4 + srcHandle.length + 8 + 8 + 4 + dstHandle.length + 8 ); writeUInt32BE(buf, buf.length - 4, p); p += 4; buf[p] = REQUEST.EXTENDED; ++p; const reqid = this._writeReqid = (this._writeReqid + 1) & MAX_REQID; writeUInt32BE(buf, reqid, p); p += 4; writeUInt32BE(buf, 9, p); p += 4; buf.utf8Write('copy-data', p, 9); p += 9; writeUInt32BE(buf, srcHandle.length, p); p += 4; buf.set(srcHandle, p); p += srcHandle.length; for (let i = 7; i >= 0; --i) { buf[p + i] = srcOffset & 0xFF; srcOffset /= 256; } p += 8; for (let i = 7; i >= 0; --i) { buf[p + i] = len & 0xFF; len /= 256; } p += 8; writeUInt32BE(buf, dstHandle.length, p); p += 4; buf.set(dstHandle, p); p += dstHandle.length; for (let i = 7; i >= 0; --i) { buf[p + i] = dstOffset & 0xFF; dstOffset /= 256; } this._requests[reqid] = { cb }; const isBuffered = sendOrBuffer(this, buf); if (this._debug) { const status = (isBuffered ? 'Buffered' : 'Sending'); this._debug(`SFTP: Outbound: ${status} copy-data`); } } ext_home_dir(username, cb) { if (this.server) throw new Error('Client-only method called in server mode'); const ext = this._extensions['home-directory']; if (ext !== '1') throw new Error('Server does not support this extended request'); if (typeof username !== 'string') throw new TypeError('username is not a string'); /* uint32 id string "home-directory" string username */ let p = 0; const usernameLen = Buffer.byteLength(username); const buf = Buffer.allocUnsafe( 4 + 1 + 4 + 4 + 14 + 4 + usernameLen ); writeUInt32BE(buf, buf.length - 4, p); p += 4; buf[p] = REQUEST.EXTENDED; ++p; const reqid = this._writeReqid = (this._writeReqid + 1) & MAX_REQID; writeUInt32BE(buf, reqid, p); p += 4; writeUInt32BE(buf, 14, p); p += 4; buf.utf8Write('home-directory', p, 14); p += 14; writeUInt32BE(buf, usernameLen, p); p += 4; buf.utf8Write(username, p, usernameLen); p += usernameLen; this._requests[reqid] = { cb: (err, names) => { if (typeof cb !== 'function') return; if (err) return cb(err); if (!names || !names.length) return cb(new Error('Response missing home directory')); cb(undefined, names[0].filename); } }; const isBuffered = sendOrBuffer(this, buf); if (this._debug) { const status = (isBuffered ? 'Buffered' : 'Sending'); this._debug(`SFTP: Outbound: ${status} home-directory`); } } ext_users_groups(uids, gids, cb) { if (this.server) throw new Error('Client-only method called in server mode'); const ext = this._extensions['users-groups-by-id@openssh.com']; if (ext !== '1') throw new Error('Server does not support this extended request'); if (!Array.isArray(uids)) throw new TypeError('uids is not an array'); for (const val of uids) { if (!Number.isInteger(val) || val < 0 || val > (2 ** 32 - 1)) throw new Error('uid values must all be 32-bit unsigned integers'); } if (!Array.isArray(gids)) throw new TypeError('gids is not an array'); for (const val of gids) { if (!Number.isInteger(val) || val < 0 || val > (2 ** 32 - 1)) throw new Error('gid values must all be 32-bit unsigned integers'); } /* uint32 id string "users-groups-by-id@openssh.com" string uids uint32 uid1 ... string gids uint32 gid1 ... */ let p = 0; const buf = Buffer.allocUnsafe( 4 + 1 + 4 + 4 + 30 + 4 + (4 * uids.length) + 4 + (4 * gids.length) ); writeUInt32BE(buf, buf.length - 4, p); p += 4; buf[p] = REQUEST.EXTENDED; ++p; const re