feat(server): add greeting and implicit TLS
This commit is contained in:
86
src/index.js
86
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();
|
||||
});
|
||||
}));
|
||||
|
||||
Reference in New Issue
Block a user