feat(server): add greeting and implicit TLS

This commit is contained in:
Tyler Stewart
2017-05-11 15:45:28 -06:00
parent 2b9e163958
commit 954e9a1252

View File

@@ -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();
});
}));