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
JavaScript
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;