UNPKG

ftp-srv-esm

Version:

Modern, extensible FTP server (daemon) for Node.js with ESM support. Based on ftp-srv.

165 lines (149 loc) 5.99 kB
import EventEmitter from 'events'; import FileSystem from './fs.js'; import BaseConnector from './connector/base.js'; import Commands from './commands/index.js'; import errors from './errors.js'; import DEFAULT_MESSAGE from './messages.js'; import crypto from 'crypto'; async function mapSeries(arr, fn) { const results = []; for (let i = 0; i < arr.length; i++) { results.push(await fn(arr[i], i)); } return results; } class FtpConnection extends EventEmitter { constructor(server, options) { super(); this.server = server; // Use crypto to generate a unique id this.id = 'u' + crypto.randomBytes(8).toString('hex'); this.commandSocket = options.socket; this.log = options.log.child({id: this.id, ip: this.ip}); this.commands = new Commands(this); this.transferType = 'binary'; this.encoding = 'utf8'; this.bufferSize = false; this._restByteCount = 0; this._secure = false; this.connector = new BaseConnector(this); this.commandSocket.on('error', (err) => { this.log.error('Client error', err); this.server.emit('client-error', {connection: this, context: 'commandSocket', error: err}); }); this.commandSocket.on('data', this._handleData.bind(this)); this.commandSocket.on('timeout', () => { this.log.debug('Client timeout'); this.close(); }); this.commandSocket.on('close', () => { if (this.connector) this.connector.end(); if (this.commandSocket && !this.commandSocket.destroyed) this.commandSocket.destroy(); this.removeAllListeners(); }); } _handleData(data) { const messages = data.toString(this.encoding).split('\r\n').filter(Boolean); this.log.debug(messages); return mapSeries(messages, (message) => this.commands.handle(message)); } get ip() { try { return this.commandSocket ? this.commandSocket.remoteAddress : undefined; } catch { return null; } } get restByteCount() { return this._restByteCount > 0 ? this._restByteCount : undefined; } set restByteCount(rbc) { this._restByteCount = rbc; } get secure() { return this.server.isTLS || this._secure; } set secure(sec) { this._secure = sec; } close(code = 421, message = 'Closing connection') { return Promise.resolve(code) .then((_code) => _code && this.reply(_code, message)) .then(() => this.commandSocket && this.commandSocket.destroy()); } login(username, password) { return Promise.resolve().then(() => { const loginListeners = this.server.listeners('login'); if (!loginListeners || !loginListeners.length) { if (!this.server.options.anonymous) throw new errors.GeneralError('No "login" listener setup', 500); } else { return this.server.emitPromise('login', {connection: this, username, password}); } }) .then(({root, cwd, fs, blacklist = [], whitelist = []} = {}) => { this.authenticated = true; this.commands.blacklist = [...this.commands.blacklist, ...blacklist]; this.commands.whitelist = [...this.commands.whitelist, ...whitelist]; this.fs = fs || new FileSystem(this, {root, cwd}); }); } reply(options = {}, ...letters) { const satisfyParameters = () => { if (typeof options === 'number') options = {code: options}; // allow passing in code as first param if (!Array.isArray(letters)) letters = [letters]; if (!letters.length) letters = [{}]; return Promise.all(letters.map((promise, index) => { return Promise.resolve(promise) .then((letter) => { if (!letter) letter = {}; else if (typeof letter === 'string') letter = {message: letter}; // allow passing in message as first param if (!letter.socket) letter.socket = options.socket ? options.socket : this.commandSocket; if (!options.useEmptyMessage) { if (!letter.message) letter.message = DEFAULT_MESSAGE[options.code] || 'No information'; if (!letter.encoding) letter.encoding = this.encoding; } return Promise.resolve(letter.message) // allow passing in a promise as a message .then((message) => { if (!options.useEmptyMessage) { const seperator = !Object.prototype.hasOwnProperty.call(options, 'eol') ? letters.length - 1 === index ? ' ' : '-' : options.eol ? ' ' : '-'; message = !letter.raw ? [letter.code || options.code, message].filter(Boolean).join(seperator) : message; letter.message = message; } else { letter.message = ''; } return letter; }); }); })); }; const processLetter = (letter) => { return new Promise((resolve, reject) => { if (letter.socket && letter.socket.writable) { this.log.debug('Reply', {port: letter.socket.address().port, encoding: letter.encoding, message: letter.message}); letter.socket.write(letter.message + '\r\n', letter.encoding, (error) => { if (error) { this.log.error('[Process Letter] Socket Write Error', { error: error.message }); return reject(error); } resolve(); }); } else { this.log.debug('Could not write message', {message: letter.message}); reject(new errors.SocketError('Socket not writable')); } }); }; return satisfyParameters() .then((satisfiedLetters) => mapSeries(satisfiedLetters, (letter, index) => { return processLetter(letter, index); })) .catch((error) => { this.log.error('Satisfy Parameters Error', { error: error.message }); }); } } export default FtpConnection;