WIP: migrate logs to signale

This commit is contained in:
Tyler Stewart
2018-05-20 11:06:30 -06:00
parent a83c391d58
commit eb24c48669
47 changed files with 293 additions and 287 deletions

View File

@@ -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

View File

@@ -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"
},

View File

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

View File

@@ -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'

View File

@@ -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',

View 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'

View 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;
});
}

View File

@@ -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'

View File

@@ -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>',

View File

@@ -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>',

View File

@@ -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'

View File

@@ -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>]',

View File

@@ -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',

View File

@@ -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>]',

View File

@@ -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>]',

View File

@@ -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>',

View File

@@ -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>',

View File

@@ -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)',

View File

@@ -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'

View File

@@ -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',

View File

@@ -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'

View File

@@ -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>',

View File

@@ -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}}',

View File

@@ -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',

View File

@@ -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'

View File

@@ -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}}',

View File

@@ -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}}',

View File

@@ -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',

View File

@@ -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'

View File

@@ -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>',

View File

@@ -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'

View File

@@ -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>',

View File

@@ -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>',

View File

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

View File

@@ -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>',

View File

@@ -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>]',

View File

@@ -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>',

View File

@@ -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}}',

View File

@@ -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',

View File

@@ -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',

View File

@@ -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)',

View File

@@ -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',

View File

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

View File

@@ -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, {

View File

@@ -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() {

View File

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

View File

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