WIP: migrate logs to signale
This commit is contained in:
@@ -114,7 +114,7 @@ __Allowable values:__
|
||||
- Only one argument is passed in: a node [file stat](https://nodejs.org/api/fs.html#fs_class_fs_stats) object with additional file `name` parameter
|
||||
|
||||
##### `log`
|
||||
A [bunyan logger](https://github.com/trentm/node-bunyan) instance. Created by default.
|
||||
A [signale logger](https://github.com/klauscfhq/signale) instance. Created by default.
|
||||
|
||||
## CLI
|
||||
|
||||
|
||||
@@ -53,10 +53,10 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"bluebird": "^3.5.1",
|
||||
"bunyan": "^1.8.12",
|
||||
"ip": "^1.1.5",
|
||||
"lodash": "^4.17.10",
|
||||
"moment": "^2.22.1",
|
||||
"signale": "^1.0.1",
|
||||
"uuid": "^3.2.1",
|
||||
"yargs": "^11.0.0"
|
||||
},
|
||||
|
||||
@@ -36,19 +36,16 @@ class FtpCommands {
|
||||
const logCommand = _.clone(command);
|
||||
if (logCommand.directive === 'PASS') logCommand.arg = '********';
|
||||
|
||||
const log = this.connection.log.child({directive: command.directive});
|
||||
log.trace({command: logCommand}, 'Handle command');
|
||||
|
||||
if (!REGISTRY.hasOwnProperty(command.directive)) {
|
||||
return this.connection.reply(402, 'Command not allowed');
|
||||
}
|
||||
|
||||
if (_.includes(this.blacklist, command.directive)) {
|
||||
return this.connection.reply(502, 'Command blacklisted');
|
||||
return this.connection.reply(502, 'Command on blacklist');
|
||||
}
|
||||
|
||||
if (this.whitelist.length > 0 && !_.includes(this.whitelist, command.directive)) {
|
||||
return this.connection.reply(502, 'Command not whitelisted');
|
||||
return this.connection.reply(502, 'Command not on whitelist');
|
||||
}
|
||||
|
||||
const commandRegister = REGISTRY[command.directive];
|
||||
@@ -61,8 +58,7 @@ class FtpCommands {
|
||||
return this.connection.reply(502, 'Handler not set on command');
|
||||
}
|
||||
|
||||
const handler = commandRegister.handler.bind(this.connection);
|
||||
return Promise.resolve(handler({log, command, previous_command: this.previousCommand}))
|
||||
return Promise.try(() => commandRegister.handler.call(this, this.connection, command, this.previousCommand))
|
||||
.finally(() => {
|
||||
this.previousCommand = _.clone(command);
|
||||
});
|
||||
|
||||
@@ -1,13 +1,16 @@
|
||||
module.exports = {
|
||||
directive: 'ABOR',
|
||||
handler: function () {
|
||||
return this.connector.waitForConnection()
|
||||
handler: function (connection) {
|
||||
return connection.connector.waitForConnection()
|
||||
.then(socket => {
|
||||
return this.reply(426, {socket})
|
||||
.then(() => this.connector.end())
|
||||
.then(() => this.reply(226));
|
||||
return connection.reply(426, {socket})
|
||||
.then(() => connection.connector.end())
|
||||
.then(() => connection.reply(226));
|
||||
})
|
||||
.catch(() => this.reply(225));
|
||||
.catch(err => {
|
||||
connection.emit('error', err);
|
||||
return connection.reply(225);
|
||||
});
|
||||
},
|
||||
syntax: '{{cmd}}',
|
||||
description: 'Abort an active file transfer'
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
module.exports = {
|
||||
directive: 'ALLO',
|
||||
handler: function () {
|
||||
return this.reply(202);
|
||||
handler: function (connection) {
|
||||
return connection.reply(202);
|
||||
},
|
||||
syntax: '{{cmd}}',
|
||||
description: 'Allocate sufficient disk space to receive a file',
|
||||
|
||||
@@ -2,8 +2,8 @@ const stor = require('./stor').handler;
|
||||
|
||||
module.exports = {
|
||||
directive: 'APPE',
|
||||
handler: function (args) {
|
||||
return stor.call(this, args);
|
||||
handler: function () {
|
||||
return stor.call(this, ...arguments);
|
||||
},
|
||||
syntax: '{{cmd}} <path>',
|
||||
description: 'Append to a file'
|
||||
|
||||
@@ -3,12 +3,12 @@ const tls = require('tls');
|
||||
|
||||
module.exports = {
|
||||
directive: 'AUTH',
|
||||
handler: function ({command} = {}) {
|
||||
handler: function (connection, command) {
|
||||
const method = _.upperCase(command.arg);
|
||||
|
||||
switch (method) {
|
||||
case 'TLS': return handleTLS.call(this);
|
||||
default: return this.reply(504);
|
||||
case 'TLS': return handleTLS.call(this, connection);
|
||||
default: return connection.reply(504);
|
||||
}
|
||||
},
|
||||
syntax: '{{cmd}} <type>',
|
||||
@@ -19,24 +19,24 @@ module.exports = {
|
||||
}
|
||||
};
|
||||
|
||||
function handleTLS() {
|
||||
if (!this.server._tls) return this.reply(502);
|
||||
if (this.secure) return this.reply(202);
|
||||
function handleTLS(connection) {
|
||||
if (!connection.server._tls) return connection.reply(502);
|
||||
if (connection.secure) return connection.reply(202);
|
||||
|
||||
return this.reply(234)
|
||||
return connection.reply(234)
|
||||
.then(() => {
|
||||
const secureContext = tls.createSecureContext(this.server._tls);
|
||||
const secureSocket = new tls.TLSSocket(this.commandSocket, {
|
||||
const secureContext = tls.createSecureContext(connection.server._tls);
|
||||
const secureSocket = new tls.TLSSocket(connection.commandSocket, {
|
||||
isServer: true,
|
||||
secureContext
|
||||
});
|
||||
['data', 'timeout', 'end', 'close', 'drain', 'error'].forEach(event => {
|
||||
function forwardEvent() {
|
||||
this.emit.apply(this, arguments);
|
||||
connection.emit.apply(this, arguments);
|
||||
}
|
||||
secureSocket.on(event, forwardEvent.bind(this.commandSocket, event));
|
||||
secureSocket.on(event, forwardEvent.bind(connection.commandSocket, event));
|
||||
});
|
||||
this.commandSocket = secureSocket;
|
||||
this.secure = true;
|
||||
connection.commandSocket = secureSocket;
|
||||
connection.secure = true;
|
||||
});
|
||||
}
|
||||
|
||||
@@ -2,9 +2,9 @@ const cwd = require('./cwd').handler;
|
||||
|
||||
module.exports = {
|
||||
directive: ['CDUP', 'XCUP'],
|
||||
handler: function (args) {
|
||||
args.command.arg = '..';
|
||||
return cwd.call(this, args);
|
||||
handler: function (connection, command, ...args) {
|
||||
command.arg = '..';
|
||||
return cwd.call(this, connection, command, ...args);
|
||||
},
|
||||
syntax: '{{cmd}}',
|
||||
description: 'Change to Parent Directory'
|
||||
|
||||
@@ -3,18 +3,18 @@ const escapePath = require('../../helpers/escape-path');
|
||||
|
||||
module.exports = {
|
||||
directive: ['CWD', 'XCWD'],
|
||||
handler: function ({log, command} = {}) {
|
||||
if (!this.fs) return this.reply(550, 'File system not instantiated');
|
||||
if (!this.fs.chdir) return this.reply(402, 'Not supported by file system');
|
||||
handler: function (connection, command) {
|
||||
if (!connection.fs) return connection.reply(550, 'File system not instantiated');
|
||||
if (!connection.fs.chdir) return connection.reply(402, 'Not supported by file system');
|
||||
|
||||
return Promise.resolve(this.fs.chdir(command.arg))
|
||||
return Promise.resolve(connection.fs.chdir(command.arg))
|
||||
.then(cwd => {
|
||||
const path = cwd ? `"${escapePath(cwd)}"` : undefined;
|
||||
return this.reply(250, path);
|
||||
return connection.reply(250, path);
|
||||
})
|
||||
.catch(err => {
|
||||
log.error(err);
|
||||
return this.reply(550, err.message);
|
||||
connection.emit('error', err);
|
||||
return connection.reply(550, err.message);
|
||||
});
|
||||
},
|
||||
syntax: '{{cmd}} <path>',
|
||||
|
||||
@@ -2,17 +2,17 @@ const Promise = require('bluebird');
|
||||
|
||||
module.exports = {
|
||||
directive: 'DELE',
|
||||
handler: function ({log, command} = {}) {
|
||||
if (!this.fs) return this.reply(550, 'File system not instantiated');
|
||||
if (!this.fs.delete) return this.reply(402, 'Not supported by file system');
|
||||
handler: function (connection, command) {
|
||||
if (!connection.fs) return connection.reply(550, 'File system not instantiated');
|
||||
if (!connection.fs.delete) return connection.reply(402, 'Not supported by file system');
|
||||
|
||||
return Promise.resolve(this.fs.delete(command.arg))
|
||||
return Promise.resolve(connection.fs.delete(command.arg))
|
||||
.then(() => {
|
||||
return this.reply(250);
|
||||
return connection.reply(250);
|
||||
})
|
||||
.catch(err => {
|
||||
log.error(err);
|
||||
return this.reply(550, err.message);
|
||||
connection.emit('error', err);
|
||||
return connection.reply(550, err.message);
|
||||
});
|
||||
},
|
||||
syntax: '{{cmd}} <path>',
|
||||
|
||||
@@ -8,14 +8,14 @@ const FAMILY = {
|
||||
|
||||
module.exports = {
|
||||
directive: 'EPRT',
|
||||
handler: function ({command} = {}) {
|
||||
handler: function (connection, command) {
|
||||
const [, protocol, ip, port] = _.chain(command).get('arg', '').split('|').value();
|
||||
const family = FAMILY[protocol];
|
||||
if (!family) return this.reply(504, 'Unknown network protocol');
|
||||
if (!family) return connection.reply(504, 'Unknown network protocol');
|
||||
|
||||
this.connector = new ActiveConnector(this);
|
||||
return this.connector.setupConnection(ip, port, family)
|
||||
.then(() => this.reply(200));
|
||||
connection.connector = new ActiveConnector(connection);
|
||||
return connection.connector.setupConnection(ip, port, family)
|
||||
.then(() => connection.reply(200));
|
||||
},
|
||||
syntax: '{{cmd}} |<protocol>|<address>|<port>|',
|
||||
description: 'Specifies an address and port to which the server should connect'
|
||||
|
||||
@@ -2,13 +2,13 @@ const PassiveConnector = require('../../connector/passive');
|
||||
|
||||
module.exports = {
|
||||
directive: 'EPSV',
|
||||
handler: function () {
|
||||
this.connector = new PassiveConnector(this);
|
||||
return this.connector.setupServer()
|
||||
handler: function (connection) {
|
||||
connection.connector = new PassiveConnector(connection);
|
||||
return connection.connector.setupServer()
|
||||
.then(server => {
|
||||
const {port} = server.address();
|
||||
|
||||
return this.reply(229, `EPSV OK (|||${port}|)`);
|
||||
return connection.reply(229, `EPSV OK (|||${port}|)`);
|
||||
});
|
||||
},
|
||||
syntax: '{{cmd}} [<protocol>]',
|
||||
|
||||
@@ -2,7 +2,7 @@ const _ = require('lodash');
|
||||
|
||||
module.exports = {
|
||||
directive: 'FEAT',
|
||||
handler: function () {
|
||||
handler: function (connection) {
|
||||
const registry = require('../registry');
|
||||
const features = Object.keys(registry)
|
||||
.reduce((feats, cmd) => {
|
||||
@@ -16,8 +16,8 @@ module.exports = {
|
||||
raw: true
|
||||
}));
|
||||
return features.length
|
||||
? this.reply(211, 'Extensions supported', ...features, 'End')
|
||||
: this.reply(211, 'No features');
|
||||
? connection.reply(211, 'Extensions supported', ...features, 'End')
|
||||
: connection.reply(211, 'No features');
|
||||
},
|
||||
syntax: '{{cmd}}',
|
||||
description: 'Get the feature list implemented by the server',
|
||||
|
||||
@@ -2,18 +2,18 @@ const _ = require('lodash');
|
||||
|
||||
module.exports = {
|
||||
directive: 'HELP',
|
||||
handler: function ({command} = {}) {
|
||||
handler: function (connection, command) {
|
||||
const registry = require('../registry');
|
||||
const directive = _.upperCase(command.arg);
|
||||
if (directive) {
|
||||
if (!registry.hasOwnProperty(directive)) return this.reply(502, `Unknown command ${directive}.`);
|
||||
if (!registry.hasOwnProperty(directive)) return connection.reply(502, `Unknown command ${directive}.`);
|
||||
|
||||
const {syntax, description} = registry[directive];
|
||||
const reply = _.concat([syntax.replace('{{cmd}}', directive), description]);
|
||||
return this.reply(214, ...reply);
|
||||
return connection.reply(214, ...reply);
|
||||
} else {
|
||||
const supportedCommands = _.chunk(Object.keys(registry), 5).map(chunk => chunk.join('\t'));
|
||||
return this.reply(211, 'Supported commands:', ...supportedCommands, 'Use "HELP [command]" for syntax help.');
|
||||
return connection.reply(211, 'Supported commands:', ...supportedCommands, 'Use "HELP [command]" for syntax help.');
|
||||
}
|
||||
},
|
||||
syntax: '{{cmd}} [<command>]',
|
||||
|
||||
@@ -6,22 +6,22 @@ const getFileStat = require('../../helpers/file-stat');
|
||||
// http://cr.yp.to/ftp/list/eplf.html
|
||||
module.exports = {
|
||||
directive: 'LIST',
|
||||
handler: function ({log, command} = {}) {
|
||||
if (!this.fs) return this.reply(550, 'File system not instantiated');
|
||||
handler: function (connection, command) {
|
||||
if (!connection.fs) return this.reply(550, 'File system not instantiated');
|
||||
if (!this.fs.get) return this.reply(402, 'Not supported by file system');
|
||||
if (!this.fs.list) return this.reply(402, 'Not supported by file system');
|
||||
|
||||
const simple = command.directive === 'NLST';
|
||||
|
||||
const path = command.arg || '.';
|
||||
return this.connector.waitForConnection()
|
||||
.tap(() => this.commandSocket.pause())
|
||||
.then(() => Promise.resolve(this.fs.get(path)))
|
||||
.then(stat => stat.isDirectory() ? Promise.resolve(this.fs.list(path)) : [stat])
|
||||
return connection.connector.waitForConnection()
|
||||
.tap(() => connection.commandSocket.pause())
|
||||
.then(() => Promise.resolve(connection.fs.get(path)))
|
||||
.then(stat => stat.isDirectory() ? Promise.resolve(connection.fs.list(path)) : [stat])
|
||||
.then(files => {
|
||||
const getFileMessage = file => {
|
||||
if (simple) return file.name;
|
||||
return getFileStat(file, _.get(this, 'server.options.file_format', 'ls'));
|
||||
return getFileStat(file, _.get(connection, 'server.options.file_format', 'ls'));
|
||||
};
|
||||
|
||||
const fileList = files.map(file => {
|
||||
@@ -29,26 +29,26 @@ module.exports = {
|
||||
return {
|
||||
raw: true,
|
||||
message,
|
||||
socket: this.connector.socket
|
||||
socket: connection.connector.socket
|
||||
};
|
||||
});
|
||||
return this.reply(150)
|
||||
return connection.reply(150)
|
||||
.then(() => {
|
||||
if (fileList.length) return this.reply({}, ...fileList);
|
||||
if (fileList.length) return connection.reply({}, ...fileList);
|
||||
});
|
||||
})
|
||||
.then(() => this.reply(226))
|
||||
.then(() => connection.reply(226))
|
||||
.catch(Promise.TimeoutError, err => {
|
||||
log.error(err);
|
||||
return this.reply(425, 'No connection established');
|
||||
connection.emit('error', err);
|
||||
return connection.reply(425, 'No connection established');
|
||||
})
|
||||
.catch(err => {
|
||||
log.error(err);
|
||||
return this.reply(451, err.message || 'No directory');
|
||||
connection.emit('error', err);
|
||||
return connection.reply(451, err.message || 'No directory');
|
||||
})
|
||||
.finally(() => {
|
||||
this.connector.end();
|
||||
this.commandSocket.resume();
|
||||
connection.connector.end();
|
||||
connection.commandSocket.resume();
|
||||
});
|
||||
},
|
||||
syntax: '{{cmd}} [<path>]',
|
||||
|
||||
@@ -3,18 +3,18 @@ const moment = require('moment');
|
||||
|
||||
module.exports = {
|
||||
directive: 'MDTM',
|
||||
handler: function ({log, command} = {}) {
|
||||
if (!this.fs) return this.reply(550, 'File system not instantiated');
|
||||
if (!this.fs.get) return this.reply(402, 'Not supported by file system');
|
||||
handler: function (connection, command) {
|
||||
if (!connection.fs) return connection.reply(550, 'File system not instantiated');
|
||||
if (!connection.fs.get) return connection.reply(402, 'Not supported by file system');
|
||||
|
||||
return Promise.resolve(this.fs.get(command.arg))
|
||||
return Promise.resolve(connection.fs.get(command.arg))
|
||||
.then(fileStat => {
|
||||
const modificationTime = moment.utc(fileStat.mtime).format('YYYYMMDDHHmmss.SSS');
|
||||
return this.reply(213, modificationTime);
|
||||
return connection.reply(213, modificationTime);
|
||||
})
|
||||
.catch(err => {
|
||||
log.error(err);
|
||||
return this.reply(550, err.message);
|
||||
connection.emit('error', err);
|
||||
return connection.reply(550, err.message);
|
||||
});
|
||||
},
|
||||
syntax: '{{cmd}} <path>',
|
||||
|
||||
@@ -3,18 +3,18 @@ const escapePath = require('../../helpers/escape-path');
|
||||
|
||||
module.exports = {
|
||||
directive: ['MKD', 'XMKD'],
|
||||
handler: function ({log, command} = {}) {
|
||||
if (!this.fs) return this.reply(550, 'File system not instantiated');
|
||||
if (!this.fs.mkdir) return this.reply(402, 'Not supported by file system');
|
||||
handler: function (connection, command) {
|
||||
if (!connection.fs) return connection.reply(550, 'File system not instantiated');
|
||||
if (!connection.fs.mkdir) return connection.reply(402, 'Not supported by file system');
|
||||
|
||||
return Promise.resolve(this.fs.mkdir(command.arg))
|
||||
return Promise.resolve(connection.fs.mkdir(command.arg))
|
||||
.then(dir => {
|
||||
const path = dir ? `"${escapePath(dir)}"` : undefined;
|
||||
return this.reply(257, path);
|
||||
return connection.reply(257, path);
|
||||
})
|
||||
.catch(err => {
|
||||
log.error(err);
|
||||
return this.reply(550, err.message);
|
||||
connection.emit('error', err);
|
||||
return connection.reply(550, err.message);
|
||||
});
|
||||
},
|
||||
syntax: '{{cmd}} <path>',
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
module.exports = {
|
||||
directive: 'MODE',
|
||||
handler: function ({command} = {}) {
|
||||
return this.reply(/^S$/i.test(command.arg) ? 200 : 504);
|
||||
handler: function (connection, command) {
|
||||
return connection.reply(/^S$/i.test(command.arg) ? 200 : 504);
|
||||
},
|
||||
syntax: '{{cmd}} <mode>',
|
||||
description: 'Sets the transfer mode (Stream, Block, or Compressed)',
|
||||
|
||||
@@ -2,8 +2,8 @@ const list = require('./list').handler;
|
||||
|
||||
module.exports = {
|
||||
directive: 'NLST',
|
||||
handler: function (args) {
|
||||
return list.call(this, args);
|
||||
handler: function () {
|
||||
return list.call(this, ...arguments);
|
||||
},
|
||||
syntax: '{{cmd}} [<path>]',
|
||||
description: 'Returns a list of file names in a specified directory'
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
module.exports = {
|
||||
directive: 'NOOP',
|
||||
handler: function () {
|
||||
return this.reply(200);
|
||||
handler: function (connection) {
|
||||
return connection.reply(200);
|
||||
},
|
||||
syntax: '{{cmd}}',
|
||||
description: 'No operation',
|
||||
|
||||
@@ -7,14 +7,14 @@ const OPTIONS = {
|
||||
|
||||
module.exports = {
|
||||
directive: 'OPTS',
|
||||
handler: function ({command} = {}) {
|
||||
if (!_.has(command, 'arg')) return this.reply(501);
|
||||
handler: function (connection, command) {
|
||||
if (!_.has(command, 'arg')) return connection.reply(501);
|
||||
|
||||
const [_option, ...args] = command.arg.split(' ');
|
||||
const option = _.toUpper(_option);
|
||||
|
||||
if (!OPTIONS.hasOwnProperty(option)) return this.reply(500);
|
||||
return OPTIONS[option].call(this, args);
|
||||
if (!OPTIONS.hasOwnProperty(option)) return connection.reply(500);
|
||||
return OPTIONS[option].call(this, ...args);
|
||||
},
|
||||
syntax: '{{cmd}}',
|
||||
description: 'Select options for a feature'
|
||||
|
||||
@@ -1,20 +1,20 @@
|
||||
module.exports = {
|
||||
directive: 'PASS',
|
||||
handler: function ({log, command} = {}) {
|
||||
if (!this.username) return this.reply(503);
|
||||
handler: function (connection, command) {
|
||||
if (!connection.username) return this.reply(503);
|
||||
if (this.authenticated) return this.reply(202);
|
||||
|
||||
// 332 : require account name (ACCT)
|
||||
|
||||
const password = command.arg;
|
||||
if (!password) return this.reply(501, 'Must provide password');
|
||||
return this.login(this.username, password)
|
||||
return connection.login(connection.username, password)
|
||||
.then(() => {
|
||||
return this.reply(230);
|
||||
return connection.reply(230);
|
||||
})
|
||||
.catch(err => {
|
||||
log.error(err);
|
||||
return this.reply(530, err.message || 'Authentication failed');
|
||||
connection.emit('error', err);
|
||||
return connection.reply(530, err.message || 'Authentication failed');
|
||||
});
|
||||
},
|
||||
syntax: '{{cmd}} <password>',
|
||||
|
||||
@@ -2,17 +2,17 @@ const PassiveConnector = require('../../connector/passive');
|
||||
|
||||
module.exports = {
|
||||
directive: 'PASV',
|
||||
handler: function () {
|
||||
this.connector = new PassiveConnector(this);
|
||||
return this.connector.setupServer()
|
||||
handler: function (connection) {
|
||||
connection.connector = new PassiveConnector(connection);
|
||||
return connection.connector.setupServer()
|
||||
.then(server => {
|
||||
const address = this.server.url.hostname;
|
||||
const address = connection.server.url.hostname;
|
||||
const {port} = server.address();
|
||||
const host = address.replace(/\./g, ',');
|
||||
const portByte1 = port / 256 | 0;
|
||||
const portByte2 = port % 256;
|
||||
|
||||
return this.reply(227, `PASV OK (${host},${portByte1},${portByte2})`);
|
||||
return connection.reply(227, `PASV OK (${host},${portByte1},${portByte2})`);
|
||||
});
|
||||
},
|
||||
syntax: '{{cmd}}',
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
module.exports = {
|
||||
directive: 'PBSZ',
|
||||
handler: function ({command} = {}) {
|
||||
if (!this.secure) return this.reply(202, 'Not suppored');
|
||||
this.bufferSize = parseInt(command.arg, 10);
|
||||
return this.reply(200, this.bufferSize === 0 ? 'OK' : 'Buffer too large: PBSZ=0');
|
||||
handler: function (connection, command) {
|
||||
if (!connection.secure) return connection.reply(202, 'Not suppored');
|
||||
connection.bufferSize = parseInt(command.arg, 10);
|
||||
return connection.reply(200, connection.bufferSize === 0 ? 'OK' : 'Buffer too large: PBSZ=0');
|
||||
},
|
||||
syntax: '{{cmd}}',
|
||||
description: 'Protection Buffer Size',
|
||||
|
||||
@@ -3,18 +3,18 @@ const ActiveConnector = require('../../connector/active');
|
||||
|
||||
module.exports = {
|
||||
directive: 'PORT',
|
||||
handler: function ({command} = {}) {
|
||||
this.connector = new ActiveConnector(this);
|
||||
handler: function (connection, command) {
|
||||
connection.connector = new ActiveConnector(connection);
|
||||
|
||||
const rawConnection = _.get(command, 'arg', '').split(',');
|
||||
if (rawConnection.length !== 6) return this.reply(425);
|
||||
if (rawConnection.length !== 6) return connection.reply(425);
|
||||
|
||||
const ip = rawConnection.slice(0, 4).join('.');
|
||||
const portBytes = rawConnection.slice(4).map(p => parseInt(p));
|
||||
const port = portBytes[0] * 256 + portBytes[1];
|
||||
|
||||
return this.connector.setupConnection(ip, port)
|
||||
.then(() => this.reply(200));
|
||||
return connection.connector.setupConnection(ip, port)
|
||||
.then(() => connection.reply(200));
|
||||
},
|
||||
syntax: '{{cmd}} <x>,<x>,<x>,<x>,<y>,<y>',
|
||||
description: 'Specifies an address and port to which the server should connect'
|
||||
|
||||
@@ -2,16 +2,16 @@ const _ = require('lodash');
|
||||
|
||||
module.exports = {
|
||||
directive: 'PROT',
|
||||
handler: function ({command} = {}) {
|
||||
if (!this.secure) return this.reply(202, 'Not suppored');
|
||||
if (!this.bufferSize && typeof this.bufferSize !== 'number') return this.reply(503);
|
||||
handler: function (connection, command) {
|
||||
if (!connection.secure) return connection.reply(202, 'Not suppored');
|
||||
if (!connection.bufferSize && typeof connection.bufferSize !== 'number') return connection.reply(503);
|
||||
|
||||
switch (_.toUpper(command.arg)) {
|
||||
case 'P': return this.reply(200, 'OK');
|
||||
case 'P': return connection.reply(200, 'OK');
|
||||
case 'C':
|
||||
case 'S':
|
||||
case 'E': return this.reply(536, 'Not supported');
|
||||
default: return this.reply(504);
|
||||
case 'E': return connection.reply(536, 'Not supported');
|
||||
default: return connection.reply(504);
|
||||
}
|
||||
},
|
||||
syntax: '{{cmd}}',
|
||||
|
||||
@@ -3,18 +3,18 @@ const escapePath = require('../../helpers/escape-path');
|
||||
|
||||
module.exports = {
|
||||
directive: ['PWD', 'XPWD'],
|
||||
handler: function ({log} = {}) {
|
||||
if (!this.fs) return this.reply(550, 'File system not instantiated');
|
||||
if (!this.fs.currentDirectory) return this.reply(402, 'Not supported by file system');
|
||||
handler: function (connection) {
|
||||
if (!connection.fs) return connection.reply(550, 'File system not instantiated');
|
||||
if (!connection.fs.currentDirectory) return connection.reply(402, 'Not supported by file system');
|
||||
|
||||
return Promise.resolve(this.fs.currentDirectory())
|
||||
return Promise.resolve(connection.fs.currentDirectory())
|
||||
.then(cwd => {
|
||||
const path = cwd ? `"${escapePath(cwd)}"` : undefined;
|
||||
return this.reply(257, path);
|
||||
return connection.reply(257, path);
|
||||
})
|
||||
.catch(err => {
|
||||
log.error(err);
|
||||
return this.reply(550, err.message);
|
||||
connection.emit('error', err);
|
||||
return connection.reply(550, err.message);
|
||||
});
|
||||
},
|
||||
syntax: '{{cmd}}',
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
module.exports = {
|
||||
directive: 'QUIT',
|
||||
handler: function () {
|
||||
return this.close(221, 'Client called QUIT');
|
||||
handler: function (connection) {
|
||||
return connection.close(221, 'Client called QUIT');
|
||||
},
|
||||
syntax: '{{cmd}}',
|
||||
description: 'Disconnect',
|
||||
|
||||
@@ -2,14 +2,14 @@ const _ = require('lodash');
|
||||
|
||||
module.exports = {
|
||||
directive: 'REST',
|
||||
handler: function ({command} = {}) {
|
||||
handler: function (connection, command) {
|
||||
const arg = _.get(command, 'arg');
|
||||
const byteCount = parseInt(arg, 10);
|
||||
|
||||
if (isNaN(byteCount) || byteCount < 0) return this.reply(501, 'Byte count must be 0 or greater');
|
||||
if (isNaN(byteCount) || byteCount < 0) return connection.reply(501, 'Byte count must be 0 or greater');
|
||||
|
||||
this.restByteCount = byteCount;
|
||||
return this.reply(350, `Restarting next transfer at ${byteCount}`);
|
||||
connection.restByteCount = byteCount;
|
||||
return connection.reply(350, `Restarting next transfer at ${byteCount}`);
|
||||
},
|
||||
syntax: '{{cmd}} <byte-count>',
|
||||
description: 'Restart transfer from the specified point. Resets after any STORE or RETRIEVE'
|
||||
|
||||
@@ -2,54 +2,54 @@ const Promise = require('bluebird');
|
||||
|
||||
module.exports = {
|
||||
directive: 'RETR',
|
||||
handler: function ({log, command} = {}) {
|
||||
if (!this.fs) return this.reply(550, 'File system not instantiated');
|
||||
if (!this.fs.read) return this.reply(402, 'Not supported by file system');
|
||||
handler: function (connection, command) {
|
||||
if (!connection.fs) return connection.reply(550, 'File system not instantiated');
|
||||
if (!connection.fs.read) return connection.reply(402, 'Not supported by file system');
|
||||
|
||||
const filePath = command.arg;
|
||||
|
||||
return this.connector.waitForConnection()
|
||||
.tap(() => this.commandSocket.pause())
|
||||
.then(() => Promise.resolve(this.fs.read(filePath, {start: this.restByteCount})))
|
||||
return connection.connector.waitForConnection()
|
||||
.tap(() => connection.commandSocket.pause())
|
||||
.then(() => Promise.resolve(connection.fs.read(filePath, {start: connection.restByteCount})))
|
||||
.then(stream => {
|
||||
const destroyConnection = (connection, reject) => err => {
|
||||
if (connection) connection.destroy(err);
|
||||
const destroyConnection = (conn, reject) => err => {
|
||||
if (conn) conn.destroy(err);
|
||||
reject(err);
|
||||
};
|
||||
|
||||
const eventsPromise = new Promise((resolve, reject) => {
|
||||
stream.on('data', data => {
|
||||
if (stream) stream.pause();
|
||||
if (this.connector.socket) {
|
||||
this.connector.socket.write(data, this.transferType, () => stream && stream.resume());
|
||||
if (connection.connector.socket) {
|
||||
connection.connector.socket.write(data, connection.transferType, () => stream && stream.resume());
|
||||
}
|
||||
});
|
||||
stream.once('end', () => resolve());
|
||||
stream.once('error', destroyConnection(this.connector.socket, reject));
|
||||
stream.once('error', destroyConnection(connection.connector.socket, reject));
|
||||
|
||||
this.connector.socket.once('error', destroyConnection(stream, reject));
|
||||
connection.connector.socket.once('error', destroyConnection(stream, reject));
|
||||
});
|
||||
|
||||
this.restByteCount = 0;
|
||||
connection.restByteCount = 0;
|
||||
|
||||
return this.reply(150).then(() => stream.resume() && this.connector.socket.resume())
|
||||
return connection.reply(150).then(() => stream.resume() && connection.connector.socket.resume())
|
||||
.then(() => eventsPromise)
|
||||
.tap(() => this.emit('RETR', null, filePath))
|
||||
.tap(() => connection.emit('RETR', null, filePath))
|
||||
.finally(() => stream.destroy && stream.destroy());
|
||||
})
|
||||
.then(() => this.reply(226))
|
||||
.then(() => connection.reply(226))
|
||||
.catch(Promise.TimeoutError, err => {
|
||||
log.error(err);
|
||||
return this.reply(425, 'No connection established');
|
||||
connection.emit('error', err);
|
||||
return connection.reply(425, 'No connection established');
|
||||
})
|
||||
.catch(err => {
|
||||
log.error(err);
|
||||
this.emit('RETR', err);
|
||||
return this.reply(551, err.message);
|
||||
connection.emit('error', err);
|
||||
connection.emit('RETR', err);
|
||||
return connection.reply(551, err.message);
|
||||
})
|
||||
.finally(() => {
|
||||
this.connector.end();
|
||||
this.commandSocket.resume();
|
||||
connection.connector.end();
|
||||
connection.commandSocket.resume();
|
||||
});
|
||||
},
|
||||
syntax: '{{cmd}} <path>',
|
||||
|
||||
@@ -2,8 +2,8 @@ const {handler: dele} = require('./dele');
|
||||
|
||||
module.exports = {
|
||||
directive: ['RMD', 'XRMD'],
|
||||
handler: function (args) {
|
||||
return dele.call(this, args);
|
||||
handler: function (...args) {
|
||||
return dele.call(this, ...args);
|
||||
},
|
||||
syntax: '{{cmd}} <path>',
|
||||
description: 'Remove a directory'
|
||||
|
||||
@@ -2,19 +2,19 @@ const Promise = require('bluebird');
|
||||
|
||||
module.exports = {
|
||||
directive: 'RNFR',
|
||||
handler: function ({log, command} = {}) {
|
||||
if (!this.fs) return this.reply(550, 'File system not instantiated');
|
||||
if (!this.fs.get) return this.reply(402, 'Not supported by file system');
|
||||
handler: function (connection, command) {
|
||||
if (!connection.fs) return connection.reply(550, 'File system not instantiated');
|
||||
if (!connection.fs.get) return connection.reply(402, 'Not supported by file system');
|
||||
|
||||
const fileName = command.arg;
|
||||
return Promise.resolve(this.fs.get(fileName))
|
||||
return Promise.resolve(connection.fs.get(fileName))
|
||||
.then(() => {
|
||||
this.renameFrom = fileName;
|
||||
return this.reply(350);
|
||||
connection.renameFrom = fileName;
|
||||
return connection.reply(350);
|
||||
})
|
||||
.catch(err => {
|
||||
log.error(err);
|
||||
return this.reply(550, err.message);
|
||||
connection.emit('error', err);
|
||||
return connection.reply(550, err.message);
|
||||
});
|
||||
},
|
||||
syntax: '{{cmd}} <name>',
|
||||
|
||||
@@ -2,25 +2,25 @@ const Promise = require('bluebird');
|
||||
|
||||
module.exports = {
|
||||
directive: 'RNTO',
|
||||
handler: function ({log, command} = {}) {
|
||||
if (!this.renameFrom) return this.reply(503);
|
||||
handler: function (connection, command) {
|
||||
if (!connection.renameFrom) return connection.reply(503);
|
||||
|
||||
if (!this.fs) return this.reply(550, 'File system not instantiated');
|
||||
if (!this.fs.rename) return this.reply(402, 'Not supported by file system');
|
||||
if (!connection.fs) return connection.reply(550, 'File system not instantiated');
|
||||
if (!connection.fs.rename) return connection.reply(402, 'Not supported by file system');
|
||||
|
||||
const from = this.renameFrom;
|
||||
const from = connection.renameFrom;
|
||||
const to = command.arg;
|
||||
|
||||
return Promise.resolve(this.fs.rename(from, to))
|
||||
return Promise.resolve(connection.fs.rename(from, to))
|
||||
.then(() => {
|
||||
return this.reply(250);
|
||||
return connection.reply(250);
|
||||
})
|
||||
.catch(err => {
|
||||
log.error(err);
|
||||
return this.reply(550, err.message);
|
||||
connection.emit('error', err);
|
||||
return connection.reply(550, err.message);
|
||||
})
|
||||
.finally(() => {
|
||||
delete this.renameFrom;
|
||||
delete connection.renameFrom;
|
||||
});
|
||||
},
|
||||
syntax: '{{cmd}} <name>',
|
||||
|
||||
@@ -8,7 +8,7 @@ module.exports = {
|
||||
handler: function ({log, command} = {}) {
|
||||
const rawSubCommand = _.get(command, 'arg', '');
|
||||
const subCommand = this.commands.parse(rawSubCommand);
|
||||
const subLog = log.child({subverb: subCommand.directive});
|
||||
const subLog = log.scope(subCommand.directive);
|
||||
|
||||
if (!registry.hasOwnProperty(subCommand.directive)) return this.reply(502);
|
||||
|
||||
|
||||
@@ -2,17 +2,17 @@ const Promise = require('bluebird');
|
||||
|
||||
module.exports = {
|
||||
directive: 'SIZE',
|
||||
handler: function ({log, command} = {}) {
|
||||
if (!this.fs) return this.reply(550, 'File system not instantiated');
|
||||
if (!this.fs.get) return this.reply(402, 'Not supported by file system');
|
||||
handler: function (connection, command) {
|
||||
if (!connection.fs) return connection.reply(550, 'File system not instantiated');
|
||||
if (!connection.fs.get) return connection.reply(402, 'Not supported by file system');
|
||||
|
||||
return Promise.resolve(this.fs.get(command.arg))
|
||||
return Promise.resolve(connection.fs.get(command.arg))
|
||||
.then(fileStat => {
|
||||
return this.reply(213, {message: fileStat.size});
|
||||
return connection.reply(213, {message: fileStat.size});
|
||||
})
|
||||
.catch(err => {
|
||||
log.error(err);
|
||||
return this.reply(550, err.message);
|
||||
connection.emit('error', err);
|
||||
return connection.reply(550, err.message);
|
||||
});
|
||||
},
|
||||
syntax: '{{cmd}} <path>',
|
||||
|
||||
@@ -4,26 +4,25 @@ const getFileStat = require('../../helpers/file-stat');
|
||||
|
||||
module.exports = {
|
||||
directive: 'STAT',
|
||||
handler: function (args = {}) {
|
||||
const {log, command} = args;
|
||||
handler: function (connection, command) {
|
||||
const path = _.get(command, 'arg');
|
||||
if (path) {
|
||||
if (!this.fs) return this.reply(550, 'File system not instantiated');
|
||||
if (!this.fs.get) return this.reply(402, 'Not supported by file system');
|
||||
if (!connection.fs) return connection.reply(550, 'File system not instantiated');
|
||||
if (!connection.fs.get) return connection.reply(402, 'Not supported by file system');
|
||||
|
||||
return Promise.resolve(this.fs.get(path))
|
||||
return Promise.resolve(connection.fs.get(path))
|
||||
.then(stat => {
|
||||
if (stat.isDirectory()) {
|
||||
if (!this.fs.list) return this.reply(402, 'Not supported by file system');
|
||||
if (!connection.fs.list) return connection.reply(402, 'Not supported by file system');
|
||||
|
||||
return Promise.resolve(this.fs.list(path))
|
||||
return Promise.resolve(connection.fs.list(path))
|
||||
.then(stats => [213, stats]);
|
||||
}
|
||||
return [212, [stat]];
|
||||
})
|
||||
.then(([code, fileStats]) => {
|
||||
return Promise.map(fileStats, file => {
|
||||
const message = getFileStat(file, _.get(this, 'server.options.file_format', 'ls'));
|
||||
const message = getFileStat(file, _.get(connection, 'server.options.file_format', 'ls'));
|
||||
return {
|
||||
raw: true,
|
||||
message
|
||||
@@ -31,13 +30,13 @@ module.exports = {
|
||||
})
|
||||
.then(messages => [code, messages]);
|
||||
})
|
||||
.then(([code, messages]) => this.reply(code, 'Status begin', ...messages, 'Status end'))
|
||||
.then(([code, messages]) => connection.reply(code, 'Status begin', ...messages, 'Status end'))
|
||||
.catch(err => {
|
||||
log.error(err);
|
||||
return this.reply(450, err.message);
|
||||
connection.emit('error', err);
|
||||
return connection.reply(450, err.message);
|
||||
});
|
||||
} else {
|
||||
return this.reply(211, 'Status OK');
|
||||
return connection.reply(211, 'Status OK');
|
||||
}
|
||||
},
|
||||
syntax: '{{cmd}} [<path>]',
|
||||
|
||||
@@ -2,62 +2,62 @@ const Promise = require('bluebird');
|
||||
|
||||
module.exports = {
|
||||
directive: 'STOR',
|
||||
handler: function ({log, command} = {}) {
|
||||
if (!this.fs) return this.reply(550, 'File system not instantiated');
|
||||
if (!this.fs.write) return this.reply(402, 'Not supported by file system');
|
||||
handler: function (connection, command) {
|
||||
if (!connection.fs) return connection.reply(550, 'File system not instantiated');
|
||||
if (!connection.fs.write) return connection.reply(402, 'Not supported by file system');
|
||||
|
||||
const append = command.directive === 'APPE';
|
||||
const fileName = command.arg;
|
||||
|
||||
return this.connector.waitForConnection()
|
||||
.tap(() => this.commandSocket.pause())
|
||||
.then(() => Promise.resolve(this.fs.write(fileName, {append, start: this.restByteCount})))
|
||||
return connection.connector.waitForConnection()
|
||||
.tap(() => connection.commandSocket.pause())
|
||||
.then(() => Promise.resolve(connection.fs.write(fileName, {append, start: connection.restByteCount})))
|
||||
.then(stream => {
|
||||
const destroyConnection = (connection, reject) => err => {
|
||||
if (connection) connection.destroy(err);
|
||||
const destroyConnection = (conn, reject) => err => {
|
||||
if (conn) conn.destroy(err);
|
||||
reject(err);
|
||||
};
|
||||
|
||||
const streamPromise = new Promise((resolve, reject) => {
|
||||
stream.once('error', destroyConnection(this.connector.socket, reject));
|
||||
stream.once('error', destroyConnection(connection.connector.socket, reject));
|
||||
stream.once('finish', () => resolve());
|
||||
});
|
||||
|
||||
const socketPromise = new Promise((resolve, reject) => {
|
||||
this.connector.socket.on('data', data => {
|
||||
if (this.connector.socket) this.connector.socket.pause();
|
||||
connection.connector.socket.on('data', data => {
|
||||
if (connection.connector.socket) connection.connector.socket.pause();
|
||||
if (stream) {
|
||||
stream.write(data, this.transferType, () => this.connector.socket && this.connector.socket.resume());
|
||||
stream.write(data, connection.transferType, () => connection.connector.socket && connection.connector.socket.resume());
|
||||
}
|
||||
});
|
||||
this.connector.socket.once('end', () => {
|
||||
connection.connector.socket.once('end', () => {
|
||||
if (stream.listenerCount('close')) stream.emit('close');
|
||||
else stream.end();
|
||||
resolve();
|
||||
});
|
||||
this.connector.socket.once('error', destroyConnection(stream, reject));
|
||||
connection.connector.socket.once('error', destroyConnection(stream, reject));
|
||||
});
|
||||
|
||||
this.restByteCount = 0;
|
||||
connection.restByteCount = 0;
|
||||
|
||||
return this.reply(150).then(() => this.connector.socket.resume())
|
||||
return connection.reply(150).then(() => connection.connector.socket.resume())
|
||||
.then(() => Promise.join(streamPromise, socketPromise))
|
||||
.tap(() => this.emit('STOR', null, fileName))
|
||||
.tap(() => connection.emit('STOR', null, fileName))
|
||||
.finally(() => stream.destroy && stream.destroy());
|
||||
})
|
||||
.then(() => this.reply(226, fileName))
|
||||
.then(() => connection.reply(226, fileName))
|
||||
.catch(Promise.TimeoutError, err => {
|
||||
log.error(err);
|
||||
return this.reply(425, 'No connection established');
|
||||
connection.emit('error', err);
|
||||
return connection.reply(425, 'No connection established');
|
||||
})
|
||||
.catch(err => {
|
||||
log.error(err);
|
||||
this.emit('STOR', err);
|
||||
return this.reply(550, err.message);
|
||||
connection.emit('error', err);
|
||||
connection.emit('STOR', err);
|
||||
return connection.reply(550, err.message);
|
||||
})
|
||||
.finally(() => {
|
||||
this.connector.end();
|
||||
this.commandSocket.resume();
|
||||
connection.connector.end();
|
||||
connection.commandSocket.resume();
|
||||
});
|
||||
},
|
||||
syntax: '{{cmd}} <path>',
|
||||
|
||||
@@ -3,19 +3,19 @@ const {handler: stor} = require('./stor');
|
||||
|
||||
module.exports = {
|
||||
directive: 'STOU',
|
||||
handler: function (args) {
|
||||
handler: function (connection, command, ...args) {
|
||||
if (!this.fs) return this.reply(550, 'File system not instantiated');
|
||||
if (!this.fs.get || !this.fs.getUniqueName) return this.reply(402, 'Not supported by file system');
|
||||
|
||||
const fileName = args.command.arg;
|
||||
const fileName = command.arg;
|
||||
return Promise.try(() => {
|
||||
return Promise.resolve(this.fs.get(fileName))
|
||||
.then(() => Promise.resolve(this.fs.getUniqueName()))
|
||||
.catch(() => Promise.resolve(fileName));
|
||||
})
|
||||
.then(name => {
|
||||
args.command.arg = name;
|
||||
return stor.call(this, args);
|
||||
command.arg = name;
|
||||
return stor.call(this, connection, command, ...args);
|
||||
});
|
||||
},
|
||||
syntax: '{{cmd}}',
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
module.exports = {
|
||||
directive: 'STRU',
|
||||
handler: function ({command} = {}) {
|
||||
return this.reply(/^F$/i.test(command.arg) ? 200 : 504);
|
||||
handler: function (connection, command) {
|
||||
return connection.reply(/^F$/i.test(command.arg) ? 200 : 504);
|
||||
},
|
||||
syntax: '{{cmd}} <structure>',
|
||||
description: 'Set file transfer structure',
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
module.exports = {
|
||||
directive: 'SYST',
|
||||
handler: function () {
|
||||
return this.reply(215);
|
||||
handler: function (connection) {
|
||||
return connection.reply(215);
|
||||
},
|
||||
syntax: '{{cmd}}',
|
||||
description: 'Return system type',
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
module.exports = {
|
||||
directive: 'TYPE',
|
||||
handler: function ({command} = {}) {
|
||||
handler: function (connection, command) {
|
||||
if (/^A[0-9]?$/i.test(command.arg)) {
|
||||
this.transferType = 'ascii';
|
||||
connection.transferType = 'ascii';
|
||||
} else if (/^L[0-9]?$/i.test(command.arg) || /^I$/i.test(command.arg)) {
|
||||
this.transferType = 'binary';
|
||||
connection.transferType = 'binary';
|
||||
} else {
|
||||
return this.reply(501);
|
||||
return connection.reply(501);
|
||||
}
|
||||
return this.reply(200, `Switch to "${this.transferType}" transfer mode.`);
|
||||
return connection.reply(200, `Switch to "${connection.transferType}" transfer mode.`);
|
||||
},
|
||||
syntax: '{{cmd}} <mode>',
|
||||
description: 'Set the transfer mode, binary (I) or ascii (A)',
|
||||
|
||||
@@ -1,24 +1,24 @@
|
||||
module.exports = {
|
||||
directive: 'USER',
|
||||
handler: function ({log, command} = {}) {
|
||||
if (this.username) return this.reply(530, 'Username already set');
|
||||
if (this.authenticated) return this.reply(230);
|
||||
handler: function (connection, command) {
|
||||
if (connection.username) return connection.reply(530, 'Username already set');
|
||||
if (connection.authenticated) return connection.reply(230);
|
||||
|
||||
this.username = command.arg;
|
||||
if (!this.username) return this.reply(501, 'Must provide username');
|
||||
connection.username = command.arg;
|
||||
if (!connection.username) return connection.reply(501, 'Must provide username');
|
||||
|
||||
if (this.server.options.anonymous === true && this.username === 'anonymous' ||
|
||||
this.username === this.server.options.anonymous) {
|
||||
return this.login(this.username, '@anonymous')
|
||||
if (connection.server.options.anonymous === true && connection.username === 'anonymous' ||
|
||||
connection.username === connection.server.options.anonymous) {
|
||||
return connection.login(connection.username, '@anonymous')
|
||||
.then(() => {
|
||||
return this.reply(230);
|
||||
return connection.reply(230);
|
||||
})
|
||||
.catch(err => {
|
||||
log.error(err);
|
||||
return this.reply(530, err.message || 'Authentication failed');
|
||||
connection.emit('error', err);
|
||||
return connection.reply(530, err.message || 'Authentication failed');
|
||||
});
|
||||
}
|
||||
return this.reply(331);
|
||||
return connection.reply(331);
|
||||
},
|
||||
syntax: '{{cmd}} <username>',
|
||||
description: 'Authentication username',
|
||||
|
||||
@@ -10,11 +10,13 @@ const errors = require('./errors');
|
||||
const DEFAULT_MESSAGE = require('./messages');
|
||||
|
||||
class FtpConnection extends EventEmitter {
|
||||
constructor(server, options) {
|
||||
constructor(server, socket) {
|
||||
super();
|
||||
this.server = server;
|
||||
this.id = uuid.v4();
|
||||
this.log = options.log.child({id: this.id, ip: this.ip});
|
||||
this.commandSocket = socket;
|
||||
this.log = server.log.scope(`client: ${this.ip}`);
|
||||
// this.log = options.log.child({id: this.id, ip: this.ip});
|
||||
this.commands = new Commands(this);
|
||||
this.transferType = 'binary';
|
||||
this.encoding = 'utf8';
|
||||
@@ -24,10 +26,8 @@ class FtpConnection extends EventEmitter {
|
||||
|
||||
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.log.scope('error event').error(err);
|
||||
});
|
||||
this.commandSocket.on('data', this._handleData.bind(this));
|
||||
this.commandSocket.on('timeout', () => {});
|
||||
@@ -40,7 +40,6 @@ class FtpConnection extends EventEmitter {
|
||||
|
||||
_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));
|
||||
}
|
||||
|
||||
@@ -117,12 +116,13 @@ class FtpConnection extends EventEmitter {
|
||||
};
|
||||
|
||||
const processLetter = letter => {
|
||||
const log = this.log.scope('reply');
|
||||
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');
|
||||
log.debug(letter.message, {port: letter.socket.address().port});
|
||||
letter.socket.write(letter.message + '\r\n', letter.encoding, err => {
|
||||
if (err) {
|
||||
this.log.error(err);
|
||||
log.error(err);
|
||||
return reject(err);
|
||||
}
|
||||
resolve();
|
||||
|
||||
@@ -7,6 +7,7 @@ class Active extends Connector {
|
||||
constructor(connection) {
|
||||
super(connection);
|
||||
this.type = 'active';
|
||||
this.log = connection.log.scope('active');
|
||||
}
|
||||
|
||||
waitForConnection({timeout = 5000, delay = 250} = {}) {
|
||||
@@ -29,10 +30,16 @@ class Active extends Connector {
|
||||
.then(() => {
|
||||
this.dataSocket = new Socket();
|
||||
this.dataSocket.setEncoding(this.connection.transferType);
|
||||
this.dataSocket.on('error', err => this.server && this.server.emit('client-error', {connection: this.connection, context: 'dataSocket', error: err}));
|
||||
this.dataSocket.on('error', err => this.connection.emit('error', err));
|
||||
this.dataSocket.on('close', () => {
|
||||
this.log.debug('socket closed');
|
||||
this.end();
|
||||
});
|
||||
this.dataSocket.connect({host, port, family}, () => {
|
||||
this.dataSocket.pause();
|
||||
|
||||
this.log.debug('connection', {port, remoteAddress: this.dataSocket.remoteAddress});
|
||||
|
||||
if (this.connection.secure) {
|
||||
const secureContext = tls.createSecureContext(this.server._tls);
|
||||
const secureSocket = new tls.TLSSocket(this.dataSocket, {
|
||||
|
||||
@@ -8,10 +8,7 @@ class Connector {
|
||||
this.dataSocket = null;
|
||||
this.dataServer = null;
|
||||
this.type = false;
|
||||
}
|
||||
|
||||
get log() {
|
||||
return this.connection.log;
|
||||
this.log = connection.log.scope('connector');
|
||||
}
|
||||
|
||||
get socket() {
|
||||
|
||||
@@ -11,6 +11,7 @@ class Passive extends Connector {
|
||||
constructor(connection) {
|
||||
super(connection);
|
||||
this.type = 'passive';
|
||||
this.log = connection.log.scope('passive');
|
||||
}
|
||||
|
||||
waitForConnection({timeout = 5000, delay = 250} = {}) {
|
||||
@@ -37,16 +38,16 @@ class Passive extends Connector {
|
||||
.then(port => {
|
||||
const connectionHandler = socket => {
|
||||
if (!ip.isEqual(this.connection.commandSocket.remoteAddress, socket.remoteAddress)) {
|
||||
this.log.error({
|
||||
this.log.error('ip address mismatch', {
|
||||
pasv_connection: socket.remoteAddress,
|
||||
cmd_connection: this.connection.commandSocket.remoteAddress
|
||||
}, 'Connecting addresses do not match');
|
||||
});
|
||||
|
||||
socket.destroy();
|
||||
return this.connection.reply(550, 'Remote addresses do not match')
|
||||
return this.connection.reply(550, 'IP address mismatch')
|
||||
.finally(() => this.connection.close());
|
||||
}
|
||||
this.log.trace({port, remoteAddress: socket.remoteAddress}, 'Passive connection fulfilled.');
|
||||
this.log.debug('connection', {port, remoteAddress: socket.remoteAddress});
|
||||
|
||||
if (this.connection.secure) {
|
||||
const secureContext = tls.createSecureContext(this.server._tls);
|
||||
@@ -60,9 +61,9 @@ class Passive extends Connector {
|
||||
}
|
||||
this.dataSocket.connected = true;
|
||||
this.dataSocket.setEncoding(this.connection.transferType);
|
||||
this.dataSocket.on('error', err => this.server && this.server.emit('client-error', {connection: this.connection, context: 'dataSocket', error: err}));
|
||||
this.dataSocket.on('error', err => this.connection.emit('error', err));
|
||||
this.dataSocket.on('close', () => {
|
||||
this.log.trace('Passive connection closed');
|
||||
this.log.debug('socket closed');
|
||||
this.end();
|
||||
});
|
||||
};
|
||||
@@ -70,9 +71,9 @@ class Passive extends Connector {
|
||||
this.dataSocket = null;
|
||||
this.dataServer = net.createServer({pauseOnConnect: true}, connectionHandler);
|
||||
this.dataServer.maxConnections = 1;
|
||||
this.dataServer.on('error', err => this.server && this.server.emit('client-error', {connection: this.connection, context: 'dataServer', error: err}));
|
||||
this.dataServer.on('error', err => this.connection.emit('error', err));
|
||||
this.dataServer.on('close', () => {
|
||||
this.log.trace('Passive server closed');
|
||||
this.log.debug('server closed');
|
||||
this.dataServer = null;
|
||||
});
|
||||
|
||||
@@ -80,7 +81,7 @@ class Passive extends Connector {
|
||||
this.dataServer.listen(port, err => {
|
||||
if (err) reject(err);
|
||||
else {
|
||||
this.log.debug({port}, 'Passive connection listening');
|
||||
this.log.debug('listening', {port});
|
||||
resolve(this.dataServer);
|
||||
}
|
||||
});
|
||||
|
||||
17
src/index.js
17
src/index.js
@@ -1,7 +1,7 @@
|
||||
const _ = require('lodash');
|
||||
const Promise = require('bluebird');
|
||||
const nodeUrl = require('url');
|
||||
const buyan = require('bunyan');
|
||||
const {Signale} = require('signale');
|
||||
const net = require('net');
|
||||
const tls = require('tls');
|
||||
const fs = require('fs');
|
||||
@@ -14,7 +14,9 @@ class FtpServer extends EventEmitter {
|
||||
constructor(url, options = {}) {
|
||||
super();
|
||||
this.options = _.merge({
|
||||
log: buyan.createLogger({name: 'ftp-srv'}),
|
||||
log: new Signale({
|
||||
scope: 'ftp-srv'
|
||||
}),
|
||||
anonymous: false,
|
||||
pasv_range: 22,
|
||||
file_format: 'ls',
|
||||
@@ -35,7 +37,7 @@ class FtpServer extends EventEmitter {
|
||||
this.url = nodeUrl.parse(url || 'ftp://127.0.0.1:21');
|
||||
|
||||
const serverConnectionHandler = socket => {
|
||||
let connection = new Connection(this, {log: this.log, socket});
|
||||
let connection = new Connection(this, socket);
|
||||
this.connections[connection.id] = connection;
|
||||
|
||||
socket.on('close', () => this.disconnectClient(connection.id));
|
||||
@@ -48,7 +50,7 @@ class FtpServer extends EventEmitter {
|
||||
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'));
|
||||
this.server.on('error', err => this.log.scope('error event').error(err));
|
||||
|
||||
const quit = _.debounce(this.quit.bind(this), 100);
|
||||
|
||||
@@ -122,7 +124,8 @@ class FtpServer extends EventEmitter {
|
||||
try {
|
||||
client.close(0);
|
||||
} catch (err) {
|
||||
this.log.error(err, 'Error closing connection', {id});
|
||||
this.log.error('Error disconnecting client', err);
|
||||
this.log.debug('User ID', {id});
|
||||
} finally {
|
||||
resolve('Disconnected');
|
||||
}
|
||||
@@ -135,12 +138,12 @@ class FtpServer extends EventEmitter {
|
||||
}
|
||||
|
||||
close() {
|
||||
this.log.info('Server closing...');
|
||||
this.log.await('Closing server...');
|
||||
this.server.maxConnections = 0;
|
||||
return Promise.map(Object.keys(this.connections), id => Promise.try(this.disconnectClient.bind(this, id)))
|
||||
.then(() => new Promise(resolve => {
|
||||
this.server.close(err => {
|
||||
if (err) this.log.error(err, 'Error closing server');
|
||||
if (err) this.log.error('Error closing server', err);
|
||||
resolve('Closed');
|
||||
});
|
||||
}))
|
||||
|
||||
Reference in New Issue
Block a user