diff --git a/src/index.js b/src/index.js index c7b5f3b..fb977c6 100644 --- a/src/index.js +++ b/src/index.js @@ -3,6 +3,8 @@ const when = require('when'); const nodeUrl = require('url'); const buyan = require('bunyan'); const net = require('net'); +const tls = require('tls'); +const fs = require('fs'); const Connection = require('./connection'); const resolveHost = require('./helpers/resolve-host'); @@ -15,30 +17,48 @@ class FtpServer { pasv_range: 22, file_format: 'ls', blacklist: [], - whitelist: [] + whitelist: [], + greeting: null, + tls: {} }, options); + this._greeting = this.setupGreeting(this.options.greeting); + this._features = this.setupFeaturesMessage(); + this._tls = this.setupTLS(this.options.tls); this.connections = {}; this.log = this.options.log; this.url = nodeUrl.parse(url || 'ftp://127.0.0.1:21'); - this.server = net.createServer({pauseOnConnect: true}, socket => { + + const serverConnectionHandler = socket => { let connection = new Connection(this, {log: this.log, socket}); this.connections[connection.id] = connection; socket.on('close', () => this.disconnectClient(connection.id)); - const greeting = this.getGreetingMessage(); - const features = this.getFeaturesMessage(); - return connection.reply(220, greeting, features) + const greeting = this._greeting || []; + const features = this._features || 'Ready'; + return connection.reply(220, ...greeting, features) .finally(() => socket.resume()); - }); - this.server.on('error', err => { - this.log.error(err); - }); + }; + const serverOptions = _.assign(this.isTLS ? this._tls : {}, { pauseOnConnect: true }); + + this.server = (this.isTLS ? tls : net).createServer(serverOptions, serverConnectionHandler); + this.server.on('error', err => this.log.error(err, '[Event] error')); + if (this.isTLS) { + this.server.on('tlsClientError', err => this.log.error(err, '[Event] tlsClientError')); + } this.on = this.server.on.bind(this.server); + this.once = this.server.once.bind(this.server); this.listeners = this.server.listeners.bind(this.server); + process.on('SIGTERM', () => this.close()); process.on('SIGINT', () => this.close()); + process.on('SIGBREAK', () => this.close()); + process.on('SIGHUP', () => this.close()); + } + + get isTLS() { + return this.url.protocol === 'ftps:'; } listen() { @@ -48,25 +68,39 @@ class FtpServer { return when.promise((resolve, reject) => { this.server.listen(this.url.port, err => { if (err) return reject(err); - this.log.info({port: this.url.port}, 'Listening'); + this.log.info({ip: this.url.hostname, port: this.url.port}, `Listening${this.isTLS ? ' (TLS)' : ''}`); resolve(); }); }); }); } - emit(action, ...data) { + emitPromise(action, ...data) { const defer = when.defer(); const params = _.concat(data, [defer.resolve, defer.reject]); this.server.emit(action, ...params); return defer.promise; } - getGreetingMessage() { - return null; + emit(action, ...data) { + this.server.emit(action, ...data); } - getFeaturesMessage() { + setupTLS(_tls) { + return _.assign(_tls, { + cert: _tls.cert ? fs.readFileSync(_tls.cert) : undefined, + key: _tls.key ? fs.readFileSync(_tls.key) : undefined, + ca: _tls.ca ? Array.isArray(_tls.ca) ? _tls.ca.map(_ca => fs.readFileSync(_ca)) : [fs.readFileSync(_tls.ca)] : undefined + }); + } + + setupGreeting(greet) { + if (!greet) return []; + const greeting = Array.isArray(greet) ? greet : greet.split('\n'); + return greeting; + } + + setupFeaturesMessage() { let features = []; if (this.options.anonymous) features.push('a'); @@ -77,32 +111,28 @@ class FtpServer { return features.length ? features.join(' ') : 'Ready'; } - setGreeting(greeting) { - if (typeof greeting === 'string') { - this.options.greeting = greeting; - } else { - greeting.then(greet => { - this.options.greeting = greet; - }); - } - } - disconnectClient(id) { return when.promise(resolve => { const client = this.connections[id]; if (!client) return resolve(); delete this.connections[id]; - client.close(0); - resolve(); + try { + client.close(0); + } catch (err) { + this.log.error(err, 'Error closing connection', {id}); + } finally { + resolve(); + } }); } close() { + this.log.info('Server closing...'); this.server.maxConnections = 0; return when.map(Object.keys(this.connections), id => this.disconnectClient(id)) - .then(() => when.promise((resolve, reject) => { + .then(() => when.promise(resolve => { this.server.close(err => { - if (err) return reject(err); + if (err) this.log.error(err, 'Error closing server'); resolve(); }); }));