UNPKG

ftp-srv-u

Version:
153 lines (138 loc) 5.59 kB
const _ = require('lodash'); const uuid = require('uuid'); const Promise = require('bluebird'); const EventEmitter = require('events'); const BaseConnector = require('./connector/base'); const FileSystem = require('./fs'); const Commands = require('./commands'); const errors = require('./errors'); const DEFAULT_MESSAGE = require('./messages'); class FtpConnection extends EventEmitter { constructor(server, options) { super(); this.server = server; this.id = uuid.v4(); 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 = options.socket; this.commandSocket.on('error', (err) => { this.log.error(err, 'Client error'); 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.trace('Client timeout'); this.close().catch((e) => this.log.trace(e, 'Client close error')); }); 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 = _.compact(data.toString(this.encoding).split('\r\n')); this.log.trace(messages); return Promise.mapSeries(messages, (message) => this.commands.handle(message)); } get ip() { try { return this.commandSocket ? this.commandSocket.remoteAddress : undefined; } catch (ex) { 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.end()); } login(username, password) { return Promise.try(() => { 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 = _.concat(this.commands.blacklist, blacklist); this.commands.whitelist = _.concat(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.map(letters, (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 = !options.hasOwnProperty('eol') ? letters.length - 1 === index ? ' ' : '-' : options.eol ? ' ' : '-'; message = !letter.raw ? _.compact([letter.code || options.code, message]).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.trace({port: letter.socket.address().port, encoding: letter.encoding, message: letter.message}, 'Reply'); letter.socket.write(letter.message + '\r\n', letter.encoding, (err) => { if (err) { this.log.error(err); return reject(err); } resolve(); }); } else reject(new errors.SocketError('Socket not writable')); }); }; return satisfyParameters() .then((satisfiedLetters) => Promise.mapSeries(satisfiedLetters, (letter, index) => { return processLetter(letter, index); })) .catch((err) => { this.log.error(err); }); } } module.exports = FtpConnection;