diff --git a/README.md b/README.md index aceed6bc..bedbd27e 100644 --- a/README.md +++ b/README.md @@ -60,10 +60,10 @@ __whitelist__ : `[]` __file_format__ : `ls` > Format to use for [file stat](https://nodejs.org/api/fs.html#fs_class_fs_stats) responses (such as with the `LIST` command). -Possible values: -- `ls` : [bin/ls format](https://cr.yp.to/ftp/list/binls.html) -- `ep` : [Easily Parsed LIST format](https://cr.yp.to/ftp/list/eplf.html) -- `function` : pass in your own format function, returning a string: +Possible values: + `ls` : [bin/ls format](https://cr.yp.to/ftp/list/binls.html) + `ep` : [Easily Parsed LIST format](https://cr.yp.to/ftp/list/eplf.html) + `function` : pass in your own format function, returning a string: `function (fileStats) { ... }` __log__ : `bunyan` diff --git a/package.json b/package.json index cba02625..1c1df6bc 100644 --- a/package.json +++ b/package.json @@ -6,6 +6,7 @@ "ftp", "ftp-server", "ftp-srv", + "ftp-svr", "ftpd", "server" ], diff --git a/src/commands/allo.js b/src/commands/allo.js deleted file mode 100644 index 49496bbd..00000000 --- a/src/commands/allo.js +++ /dev/null @@ -1,3 +0,0 @@ -module.exports = function () { - return this.reply(202); -} diff --git a/src/commands/auth.js b/src/commands/auth.js deleted file mode 100644 index ca6e3764..00000000 --- a/src/commands/auth.js +++ /dev/null @@ -1,19 +0,0 @@ -const _ = require('lodash'); - -module.exports = function ({command} = {}) { - const method = _.upperCase(command._[1]); - - switch (method) { - case 'TLS': return handleTLS.call(this); - case 'SSL': return handleSSL.call(this); - default: return this.reply(504); - } -} - -function handleTLS() { - return this.reply(504); -} - -function handleSSL() { - return this.reply(504); -} diff --git a/src/commands/cdup.js b/src/commands/cdup.js deleted file mode 100644 index bb598fb8..00000000 --- a/src/commands/cdup.js +++ /dev/null @@ -1,6 +0,0 @@ -const cwd = require('./cwd'); - -module.exports = function(args) { - args.command._ = [args.command._[0], '..']; - return cwd.call(this, args); -} diff --git a/src/commands/cwd.js b/src/commands/cwd.js deleted file mode 100644 index 7c90e0ff..00000000 --- a/src/commands/cwd.js +++ /dev/null @@ -1,17 +0,0 @@ -const when = require('when'); -const escapePath = require('../helpers/escape-path'); - -module.exports = 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'); - - return when(this.fs.chdir(command._[1])) - .then(cwd => { - const path = cwd ? `"${escapePath(cwd)}"` : undefined; - return this.reply(250, path); - }) - .catch(err => { - log.error(err); - return this.reply(550, err.message); - }); -} diff --git a/src/commands/dele.js b/src/commands/dele.js deleted file mode 100644 index 7c8b7805..00000000 --- a/src/commands/dele.js +++ /dev/null @@ -1,15 +0,0 @@ -const when = require('when'); - -module.exports = 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'); - - return when(this.fs.delete(command._[1])) - .then(() => { - return this.reply(250); - }) - .catch(err => { - log.error(err); - return this.reply(550); - }); -} diff --git a/src/commands/feat.js b/src/commands/feat.js deleted file mode 100644 index 8ded7d87..00000000 --- a/src/commands/feat.js +++ /dev/null @@ -1,10 +0,0 @@ -const _ = require('lodash'); - -module.exports = function () { - const registry = require('./registry'); - const features = Object.keys(registry) - .filter(cmd => registry[cmd].hasOwnProperty('feat')) - .reduce((feats, cmd) => _.concat(feats, registry[cmd].feat), []) - .map(feat => ` ${feat}`); - return this.reply(211, 'Extensions supported', ...features, 'END'); -} diff --git a/src/commands/help.js b/src/commands/help.js deleted file mode 100644 index f42a333c..00000000 --- a/src/commands/help.js +++ /dev/null @@ -1,16 +0,0 @@ -const _ = require('lodash'); - -module.exports = function ({command} = {}) { - const registry = require('./registry'); - const directive = _.upperCase(command._[1]); - if (directive) { - if (!registry.hasOwnProperty(directive)) return this.reply(502, `Unknown command ${directive}.`); - - const {syntax, help, obsolete} = registry[directive]; - const reply = _.concat([syntax, help, obsolete ? 'Obsolete' : null]); - return this.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.'); - } -}; diff --git a/src/commands/index.js b/src/commands/index.js index 27b89f35..5d5c10b7 100644 --- a/src/commands/index.js +++ b/src/commands/index.js @@ -1,10 +1,12 @@ const _ = require('lodash'); const when = require('when'); +const REGISTRY = require('./registry'); + class FtpCommands { constructor(connection) { + console.log(REGISTRY) this.connection = connection; - this.registry = require('./registry'); this.previousCommand = {}; this.blacklist = _.get(this.connection, 'server.options.blacklist', []).map(cmd => _.upperCase(cmd)); this.whitelist = _.get(this.connection, 'server.options.whitelist', []).map(cmd => _.upperCase(cmd)); @@ -14,7 +16,7 @@ class FtpCommands { const log = this.connection.log.child({command}); log.trace('Handle command'); - if (!this.registry.hasOwnProperty(command.directive)) { + if (!REGISTRY.hasOwnProperty(command.directive)) { return this.connection.reply(402, 'Command not allowed'); } @@ -26,8 +28,9 @@ class FtpCommands { return this.connection.reply(502, 'Command not whitelisted'); } - const commandRegister = this.registry[command.directive]; - if (!commandRegister.no_auth && !this.connection.authenticated) { + const commandRegister = REGISTRY[command.directive]; + const commandFlags = _.get(commandRegister, 'flags', {}); + if (!commandFlags.no_auth && !this.connection.authenticated) { return this.connection.reply(530); } diff --git a/src/commands/list.js b/src/commands/list.js deleted file mode 100644 index 93589e54..00000000 --- a/src/commands/list.js +++ /dev/null @@ -1,53 +0,0 @@ -const _ = require('lodash'); -const when = require('when'); -const getFileStat = require('../helpers/file-stat'); - -// http://cr.yp.to/ftp/list.html -// http://cr.yp.to/ftp/list/eplf.html -module.exports = function ({log, command, previous_command} = {}) { - if (!this.fs) return this.reply(550, 'File system not instantiated'); - if (!this.fs.list) return this.reply(402, 'Not supported by file system'); - - const simple = command.directive === 'NLST'; - - let dataSocket; - const directory = command._[1] || '.'; - return this.connector.waitForConnection() - .then(socket => { - this.commandSocket.pause(); - dataSocket = socket; - }) - .then(() => when(this.fs.list(directory))) - .then(files => { - const getFileMessage = (file) => { - if (simple) return file.name; - return getFileStat(file, _.get(this, 'server.options.file_format', 'ls')); - }; - - const fileList = files.map(file => { - const message = getFileMessage(file); - return { - raw: true, - message, - socket: dataSocket - }; - }) - return this.reply(150) - .then(() => this.reply(...fileList)); - }) - .then(() => { - return this.reply(226, 'Transfer OK'); - }) - .catch(when.TimeoutError, err => { - log.error(err); - return this.reply(425, 'No connection established'); - }) - .catch(err => { - log.error(err); - return this.reply(err.code || 451, err.message || 'No directory'); - }) - .finally(() => { - this.connector.end(); - this.commandSocket.resume(); - }); -} diff --git a/src/commands/mdtm.js b/src/commands/mdtm.js deleted file mode 100644 index dc9fd89a..00000000 --- a/src/commands/mdtm.js +++ /dev/null @@ -1,17 +0,0 @@ -const when = require('when'); -const format = require('date-fns/format'); - -module.exports = 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'); - - return when(this.fs.get(command._[1])) - .then(fileStat => { - const modificationTime = format(fileStat.mtime, 'YYYYMMDDHHmmss.SSS'); - return this.reply(213, modificationTime) - }) - .catch(err => { - log.error(err); - return this.reply(550); - }); -} diff --git a/src/commands/mkd.js b/src/commands/mkd.js deleted file mode 100644 index 7bd91148..00000000 --- a/src/commands/mkd.js +++ /dev/null @@ -1,17 +0,0 @@ -const when = require('when'); -const escapePath = require('../helpers/escape-path'); - -module.exports = 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'); - - return when(this.fs.mkdir(command._[1])) - .then(dir => { - const path = dir ? `"${escapePath(dir)}"` : undefined; - return this.reply(257, path); - }) - .catch(err => { - log.error(err); - return this.reply(550); - }); -} diff --git a/src/commands/mode.js b/src/commands/mode.js deleted file mode 100644 index fb4be67f..00000000 --- a/src/commands/mode.js +++ /dev/null @@ -1,3 +0,0 @@ -module.exports = function ({command} = {}) { - return this.reply(command._[1] === 'S' ? 200 : 504); -} diff --git a/src/commands/noop.js b/src/commands/noop.js deleted file mode 100644 index 9f9461cf..00000000 --- a/src/commands/noop.js +++ /dev/null @@ -1,3 +0,0 @@ -module.exports = function () { - return this.reply(200); -} diff --git a/src/commands/opts.js b/src/commands/opts.js deleted file mode 100644 index fc8104b7..00000000 --- a/src/commands/opts.js +++ /dev/null @@ -1,3 +0,0 @@ -module.exports = function () { - return this.reply(501); -} diff --git a/src/commands/pass.js b/src/commands/pass.js deleted file mode 100644 index 17347b18..00000000 --- a/src/commands/pass.js +++ /dev/null @@ -1,19 +0,0 @@ -const _ = require('lodash'); - -module.exports = function ({log, command} = {}) { - if (!this.username) return this.reply(503); - if (this.username && this.authenticated && - _.get(this, 'server.options.anonymous') === true) return this.reply(230); - - // 332 : require account name (ACCT) - - const password = command._[1]; - return this.login(this.username, password) - .then(() => { - return this.reply(230); - }) - .catch(err => { - log.error(err); - return this.reply(530, err.message || 'Authentication failed'); - }); -}; diff --git a/src/commands/pasv.js b/src/commands/pasv.js deleted file mode 100644 index 272a9e29..00000000 --- a/src/commands/pasv.js +++ /dev/null @@ -1,15 +0,0 @@ -const PassiveConnector = require('../connector/passive'); - -module.exports = function ({command} = {}) { - this.connector = new PassiveConnector(this); - return this.connector.setupServer() - .then(server => { - const address = this.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})`); - }); -} diff --git a/src/commands/port.js b/src/commands/port.js deleted file mode 100644 index b4204035..00000000 --- a/src/commands/port.js +++ /dev/null @@ -1,16 +0,0 @@ -const ActiveConnector = require('../connector/active'); - -module.exports = function ({command} = {}) { - this.connector = new ActiveConnector(this); - const rawConnection = command._[1].split(','); - if (rawConnection.length !== 6) return this.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(socket => { - return this.reply(200); - }) -} diff --git a/src/commands/pwd.js b/src/commands/pwd.js deleted file mode 100644 index 15701a99..00000000 --- a/src/commands/pwd.js +++ /dev/null @@ -1,17 +0,0 @@ -const when = require('when'); -const escapePath = require('../helpers/escape-path'); - -module.exports = function ({log, command} = {}) { - if (!this.fs) return this.reply(550, 'File system not instantiated'); - if (!this.fs.currentDirectory) return this.reply(402, 'Not supported by file system'); - - return when(this.fs.currentDirectory()) - .then(cwd => { - const path = cwd ? `"${escapePath(cwd)}"` : undefined; - return this.reply(257, path); - }) - .catch(err => { - log.error(err); - return this.reply(550, err.message); - }); -} diff --git a/src/commands/quit.js b/src/commands/quit.js deleted file mode 100644 index 8aa75d6a..00000000 --- a/src/commands/quit.js +++ /dev/null @@ -1,3 +0,0 @@ -module.exports = function () { - return this.close(221); -} diff --git a/src/commands/registration/allo.js b/src/commands/registration/allo.js new file mode 100644 index 00000000..0848bd24 --- /dev/null +++ b/src/commands/registration/allo.js @@ -0,0 +1,11 @@ +module.exports = { + directive: 'ALLO', + handler: function () { + return this.reply(202); + }, + syntax: '{{cmd}}', + description: 'Allocate sufficient disk space to receive a file', + flags: { + obsolete: true + } +}; diff --git a/src/commands/registration/appe.js b/src/commands/registration/appe.js new file mode 100644 index 00000000..01fced53 --- /dev/null +++ b/src/commands/registration/appe.js @@ -0,0 +1,10 @@ +const stor = require('./stor').handler; + +module.exports = { + directive: 'APPE', + handler: function (args) { + return stor.call(this, args); + }, + syntax: '{{cmd}} [path]', + description: 'Append to a file' +} diff --git a/src/commands/registration/auth.js b/src/commands/registration/auth.js new file mode 100644 index 00000000..4d570ca1 --- /dev/null +++ b/src/commands/registration/auth.js @@ -0,0 +1,27 @@ +const _ = require('lodash'); + +module.exports = { + directive: 'AUTH', + handler: function ({command} = {}) { + const method = _.upperCase(command._[1]); + + switch (method) { + case 'TLS': return handleTLS.call(this); + case 'SSL': return handleSSL.call(this); + default: return this.reply(504); + } + }, + syntax: '{{cmd}} [type]', + description: 'Set authentication mechanism', + flags: { + no_auth: true + } +} + +function handleTLS() { + return this.reply(504); +} + +function handleSSL() { + return this.reply(504); +} diff --git a/src/commands/registration/cdup.js b/src/commands/registration/cdup.js new file mode 100644 index 00000000..a37a6e7b --- /dev/null +++ b/src/commands/registration/cdup.js @@ -0,0 +1,11 @@ +const cwd = require('./cwd').handler; + +module.exports = { + directive: ['CDUP', 'XCUP'], + handler: function(args) { + args.command._ = [args.command._[0], '..']; + return cwd.call(this, args); + }, + syntax: '{{cmd}}', + description: 'Change to Parent Directory' +} diff --git a/src/commands/registration/cwd.js b/src/commands/registration/cwd.js new file mode 100644 index 00000000..d8dfa577 --- /dev/null +++ b/src/commands/registration/cwd.js @@ -0,0 +1,22 @@ +const when = require('when'); +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'); + + return when(this.fs.chdir(command._[1])) + .then(cwd => { + const path = cwd ? `"${escapePath(cwd)}"` : undefined; + return this.reply(250, path); + }) + .catch(err => { + log.error(err); + return this.reply(550, err.message); + }); + }, + syntax: '{{cmd}}[path]', + description: 'Change working directory' +} diff --git a/src/commands/registration/dele.js b/src/commands/registration/dele.js new file mode 100644 index 00000000..ca06ba3b --- /dev/null +++ b/src/commands/registration/dele.js @@ -0,0 +1,20 @@ +const when = require('when'); + +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'); + + return when(this.fs.delete(command._[1])) + .then(() => { + return this.reply(250); + }) + .catch(err => { + log.error(err); + return this.reply(550); + }); + }, + syntax: '{{cmd}} [path]', + description: 'Delete file' +} diff --git a/src/commands/registration/feat.js b/src/commands/registration/feat.js new file mode 100644 index 00000000..4f681f58 --- /dev/null +++ b/src/commands/registration/feat.js @@ -0,0 +1,18 @@ +const _ = require('lodash'); + +module.exports = { + directive: 'FEAT', + handler: function () { + const registry = require('../registry'); + const features = Object.keys(registry) + .filter(cmd => registry[cmd].hasOwnProperty('feat')) + .reduce((feats, cmd) => _.concat(feats, registry[cmd].feat), []) + .map(feat => ` ${feat}`); + return this.reply(211, 'Extensions supported', ...features, 'END'); + }, + syntax: '{{cmd}}', + description: 'Get the feature list implemented by the server', + flags: { + no_auth: true + } +} diff --git a/src/commands/registration/help.js b/src/commands/registration/help.js new file mode 100644 index 00000000..cb61b233 --- /dev/null +++ b/src/commands/registration/help.js @@ -0,0 +1,24 @@ +const _ = require('lodash'); + +module.exports = { + directive: 'HELP', + handler: function ({command} = {}) { + const registry = require('../registry'); + const directive = _.upperCase(command._[1]); + if (directive) { + if (!registry.hasOwnProperty(directive)) return this.reply(502, `Unknown command ${directive}.`); + + const {syntax, help, obsolete} = registry[directive]; + const reply = _.concat([syntax, help, obsolete ? 'Obsolete' : null]); + return this.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.'); + } + }, + syntax: '{{cmd}} [command(optional)]', + description: 'Returns usage documentation on a command if specified, else a general help document is returned', + flags: { + no_auth: true + } +} diff --git a/src/commands/registration/list.js b/src/commands/registration/list.js new file mode 100644 index 00000000..7afc2da4 --- /dev/null +++ b/src/commands/registration/list.js @@ -0,0 +1,60 @@ +const _ = require('lodash'); +const when = require('when'); +const getFileStat = require('../../helpers/file-stat'); + +// http://cr.yp.to/ftp/list.html +// http://cr.yp.to/ftp/list/eplf.html +module.exports = { + directive: 'LIST', + handler: function ({log, command, previous_command} = {}) { + if (!this.fs) return this.reply(550, 'File system not instantiated'); + if (!this.fs.list) return this.reply(402, 'Not supported by file system'); + + const simple = command.directive === 'NLST'; + + let dataSocket; + const directory = command._[1] || '.'; + return this.connector.waitForConnection() + .then(socket => { + this.commandSocket.pause(); + dataSocket = socket; + }) + .then(() => when(this.fs.list(directory))) + .then(files => { + const getFileMessage = (file) => { + if (simple) return file.name; + return getFileStat(file, _.get(this, 'server.options.file_format', 'ls')); + }; + + const fileList = files.map(file => { + const message = getFileMessage(file); + return { + raw: true, + message, + socket: dataSocket + }; + }) + return this.reply(150) + .then(() => { + if (fileList.length) return this.reply(...fileList); + }); + }) + .then(() => { + return this.reply(226, 'Transfer OK'); + }) + .catch(when.TimeoutError, err => { + log.error(err); + return this.reply(425, 'No connection established'); + }) + .catch(err => { + log.error(err); + return this.reply(err.code || 451, err.message || 'No directory'); + }) + .finally(() => { + this.connector.end(); + this.commandSocket.resume(); + }); + }, + syntax: '{{cmd}} [path(optional)]', + description: 'Returns information of a file or directory if specified, else information of the current working directory is returned' +} diff --git a/src/commands/registration/mdtm.js b/src/commands/registration/mdtm.js new file mode 100644 index 00000000..af01a689 --- /dev/null +++ b/src/commands/registration/mdtm.js @@ -0,0 +1,25 @@ +const when = require('when'); +const format = require('date-fns/format'); + +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'); + + return when(this.fs.get(command._[1])) + .then(fileStat => { + const modificationTime = format(fileStat.mtime, 'YYYYMMDDHHmmss.SSS'); + return this.reply(213, modificationTime) + }) + .catch(err => { + log.error(err); + return this.reply(550); + }); + }, + syntax: '{{cmd}} [path]', + description: 'Return the last-modified time of a specified file', + flags: { + feat: 'MDTM' + } +} diff --git a/src/commands/registration/mkd.js b/src/commands/registration/mkd.js new file mode 100644 index 00000000..38989056 --- /dev/null +++ b/src/commands/registration/mkd.js @@ -0,0 +1,22 @@ +const when = require('when'); +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'); + + return when(this.fs.mkdir(command._[1])) + .then(dir => { + const path = dir ? `"${escapePath(dir)}"` : undefined; + return this.reply(257, path); + }) + .catch(err => { + log.error(err); + return this.reply(550); + }); + }, + syntax: '{{cmd}}[path]', + description: 'Make directory' +} diff --git a/src/commands/registration/mode.js b/src/commands/registration/mode.js new file mode 100644 index 00000000..31ba113c --- /dev/null +++ b/src/commands/registration/mode.js @@ -0,0 +1,11 @@ +module.exports = { + directive: 'MODE', + handler: function ({command} = {}) { + return this.reply(command._[1] === 'S' ? 200 : 504); + }, + syntax: '{{cmd}} [mode]', + description: 'Sets the transfer mode (Stream, Block, or Compressed)', + flags: { + obsolete: true + } +} diff --git a/src/commands/registration/nlst.js b/src/commands/registration/nlst.js new file mode 100644 index 00000000..7cea9461 --- /dev/null +++ b/src/commands/registration/nlst.js @@ -0,0 +1,10 @@ +const list = require('./list').handler; + +module.exports = { + directive: 'NLST', + handler: function (args) { + return list.call(this, args); + }, + syntax: '{{cmd}} [path(optional)]', + description: 'Returns a list of file names in a specified directory' +} diff --git a/src/commands/registration/noop.js b/src/commands/registration/noop.js new file mode 100644 index 00000000..b462a3d7 --- /dev/null +++ b/src/commands/registration/noop.js @@ -0,0 +1,11 @@ +module.exports = { + directive: 'NOOP', + handler: function () { + return this.reply(200); + }, + syntax: '{{cmd}}', + description: 'No operation', + flags: { + no_auth: true + } +} diff --git a/src/commands/registration/opts.js b/src/commands/registration/opts.js new file mode 100644 index 00000000..10ac98db --- /dev/null +++ b/src/commands/registration/opts.js @@ -0,0 +1,8 @@ +module.exports = { + directive: 'OPTS', + handler: function () { + return this.reply(501); + }, + syntax: '{{cmd}}', + description: 'Select options for a feature' +} diff --git a/src/commands/registration/pass.js b/src/commands/registration/pass.js new file mode 100644 index 00000000..a774a8aa --- /dev/null +++ b/src/commands/registration/pass.js @@ -0,0 +1,27 @@ +const _ = require('lodash'); + +module.exports = { + directive: 'PASS', + handler: function ({log, command} = {}) { + if (!this.username) return this.reply(503); + if (this.username && this.authenticated && + _.get(this, 'server.options.anonymous') === true) return this.reply(230); + + // 332 : require account name (ACCT) + + const password = command._[1]; + return this.login(this.username, password) + .then(() => { + return this.reply(230); + }) + .catch(err => { + log.error(err); + return this.reply(530, err.message || 'Authentication failed'); + }); + }, + syntax: '{{cmd}} [password]', + description: 'Authentication password', + flags: { + no_auth: true + } +} diff --git a/src/commands/registration/pasv.js b/src/commands/registration/pasv.js new file mode 100644 index 00000000..d2eeb1c4 --- /dev/null +++ b/src/commands/registration/pasv.js @@ -0,0 +1,20 @@ +const PassiveConnector = require('../../connector/passive'); + +module.exports = { + directive: 'PASV', + handler: function ({command} = {}) { + this.connector = new PassiveConnector(this); + return this.connector.setupServer() + .then(server => { + const address = this.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})`); + }); + }, + syntax: '{{cmd}}', + description: 'Initiate passive mode' +} diff --git a/src/commands/registration/port.js b/src/commands/registration/port.js new file mode 100644 index 00000000..d3d2556e --- /dev/null +++ b/src/commands/registration/port.js @@ -0,0 +1,21 @@ +const ActiveConnector = require('../../connector/active'); + +module.exports = { + directive: 'PORT', + handler: function ({command} = {}) { + this.connector = new ActiveConnector(this); + const rawConnection = command._[1].split(','); + if (rawConnection.length !== 6) return this.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(socket => { + return this.reply(200); + }) + }, + syntax: '{{cmd}} [x,x,x,x,y,y]', + description: 'Specifies an address and port to which the server should connect' +} diff --git a/src/commands/registration/pwd.js b/src/commands/registration/pwd.js new file mode 100644 index 00000000..dce520e6 --- /dev/null +++ b/src/commands/registration/pwd.js @@ -0,0 +1,22 @@ +const when = require('when'); +const escapePath = require('../../helpers/escape-path'); + +module.exports = { + directive: ['PWD', 'XPWD'], + handler: function ({log, command} = {}) { + if (!this.fs) return this.reply(550, 'File system not instantiated'); + if (!this.fs.currentDirectory) return this.reply(402, 'Not supported by file system'); + + return when(this.fs.currentDirectory()) + .then(cwd => { + const path = cwd ? `"${escapePath(cwd)}"` : undefined; + return this.reply(257, path); + }) + .catch(err => { + log.error(err); + return this.reply(550, err.message); + }) + }, + syntax: '{{cmd}}', + description: 'Print current working directory' +} diff --git a/src/commands/registration/quit.js b/src/commands/registration/quit.js new file mode 100644 index 00000000..d3350fa9 --- /dev/null +++ b/src/commands/registration/quit.js @@ -0,0 +1,11 @@ +module.exports = { + directive: 'QUIT', + handler: function () { + return this.close(221); + }, + syntax: '{{cmd}}', + description: 'Disconnect', + flags: { + no_auth: true + } +} diff --git a/src/commands/registration/retr.js b/src/commands/registration/retr.js new file mode 100644 index 00000000..e557b1c6 --- /dev/null +++ b/src/commands/registration/retr.js @@ -0,0 +1,41 @@ +const when = require('when'); + +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'); + + let dataSocket; + return this.connector.waitForConnection() + .then(socket => { + this.commandSocket.pause(); + dataSocket = socket; + }) + .then(() => when(this.fs.read(command._[1]))) + .then(stream => { + return when.promise((resolve, reject) => { + dataSocket.on('error', err => stream.emit('error', err)); + + stream.on('data', data => dataSocket.write(data, this.encoding)); + stream.on('end', () => resolve(this.reply(226))); + stream.on('error', err => reject(err)); + this.reply(150).then(() => dataSocket.resume()); + }); + }) + .catch(when.TimeoutError, err => { + log.error(err); + return this.reply(425, 'No connection established'); + }) + .catch(err => { + log.error(err); + return this.reply(551); + }) + .finally(() => { + this.connector.end(); + this.commandSocket.resume(); + }) + }, + syntax: '{{cmd}} [path]', + description: 'Retrieve a copy of the file' +} diff --git a/src/commands/registration/rmd.js b/src/commands/registration/rmd.js new file mode 100644 index 00000000..3bdaf447 --- /dev/null +++ b/src/commands/registration/rmd.js @@ -0,0 +1,10 @@ +const dele = require('./dele').handler; + +module.exports = { + directive: ['RMD', 'XRMD'], + handler: function (args) { + return dele.call(this, args); + }, + syntax: '{{cmd}} [path]', + description: 'Remove a directory' +} diff --git a/src/commands/registration/rnfr.js b/src/commands/registration/rnfr.js new file mode 100644 index 00000000..020b93a7 --- /dev/null +++ b/src/commands/registration/rnfr.js @@ -0,0 +1,22 @@ +const when = require('when'); + +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'); + + const fileName = command._[1]; + return when(this.fs.get(fileName)) + .then(() => { + this.renameFrom = fileName; + return this.reply(350); + }) + .catch(err => { + log.error(err); + return this.reply(550); + }); + }, + syntax: '{{cmd}} [name]', + description: 'Rename from' +}; diff --git a/src/commands/registration/rnto.js b/src/commands/registration/rnto.js new file mode 100644 index 00000000..dcfb4b3b --- /dev/null +++ b/src/commands/registration/rnto.js @@ -0,0 +1,28 @@ +const when = require('when'); + +module.exports = { + directive: 'RNTO', + handler: function ({log, command} = {}) { + if (!this.renameFrom) return this.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'); + + const from = this.renameFrom; + const to = command._[1]; + + return when(this.fs.rename(from, to)) + .then(() => { + return this.reply(250); + }) + .catch(err => { + log.error(err); + return this.reply(550); + }) + .finally(() => { + delete this.renameFrom; + }) + }, + syntax: '{{cmd}} [name]', + description: 'Rename to' +} diff --git a/src/commands/site/chmod.js b/src/commands/registration/site/chmod.js similarity index 100% rename from src/commands/site/chmod.js rename to src/commands/registration/site/chmod.js diff --git a/src/commands/registration/site/index.js b/src/commands/registration/site/index.js new file mode 100644 index 00000000..bc3b0be3 --- /dev/null +++ b/src/commands/registration/site/index.js @@ -0,0 +1,23 @@ +const _ = require('lodash'); +const when = require('when'); + +module.exports = { + directive: 'SITE', + handler: function ({log, command} = {}) { + const registry = require('./registry'); + let [, subverb, ...subparameters] = command._; + subverb = _.upperCase(subverb); + const subLog = log.child({subverb}); + + if (!registry.hasOwnProperty(subverb)) return this.reply(502); + + const subCommand = { + _: [subverb, ...subparameters], + directive: subverb + } + const handler = registry[subverb].handler.bind(this); + return when.try(handler, { log: subLog, command: subCommand }); + }, + syntax: '{{cmd}} [subVerb] [subParams]', + description: 'Sends site specific commands to remote server' +} diff --git a/src/commands/site/registry.js b/src/commands/registration/site/registry.js similarity index 100% rename from src/commands/site/registry.js rename to src/commands/registration/site/registry.js diff --git a/src/commands/registration/size.js b/src/commands/registration/size.js new file mode 100644 index 00000000..d9fc6609 --- /dev/null +++ b/src/commands/registration/size.js @@ -0,0 +1,23 @@ +const when = require('when'); + +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'); + + return when(this.fs.get(command._[1])) + .then(fileStat => { + return this.reply(213, {message: fileStat.size}); + }) + .catch(err => { + log.error(err); + return this.reply(550); + }); + }, + syntax: '{{cmd}} [path]', + description: 'Return the size of a file', + flags: { + feat: 'SIZE' + } +} diff --git a/src/commands/registration/stat.js b/src/commands/registration/stat.js new file mode 100644 index 00000000..ab236c5f --- /dev/null +++ b/src/commands/registration/stat.js @@ -0,0 +1,42 @@ +const _ = require('lodash'); +const when = require('when'); +const getFileStat = require('../../helpers/file-stat'); + +module.exports = { + directive: 'STAT', + handler: function (args = {}) { + const {log, command} = args; + const path = command._[1]; + 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'); + + return when(this.fs.get(path)) + .then(stat => { + if (stat.isDirectory()) { + return when(this.fs.list(path)) + .then(files => { + const fileList = files.map(file => { + const message = getFileStat(file, _.get(this, 'server.options.file_format', 'ls')); + return { + raw: true, + message + }; + }) + return this.reply(213, 'Status begin', ...fileList, 'Status end'); + }) + } else { + return this.reply(212, getFileStat(stat, _.get(this, 'server.options.file_format', 'ls'))) + } + }) + .catch(err => { + log.error(err); + return this.reply(450); + }) + } else { + return this.reply(211, 'Status OK'); + } + }, + syntax: '{{cmd}} [path(optional)]', + description: 'Returns the current status' +} diff --git a/src/commands/registration/stor.js b/src/commands/registration/stor.js new file mode 100644 index 00000000..8f6c36b7 --- /dev/null +++ b/src/commands/registration/stor.js @@ -0,0 +1,43 @@ +const when = require('when'); + +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'); + + const append = command.directive === 'APPE'; + + let dataSocket; + return this.connector.waitForConnection() + .then(socket => { + this.commandSocket.pause(); + dataSocket = socket; + }) + .then(() => when(this.fs.write(command._[1], {append}))) + .then(stream => { + return when.promise((resolve, reject) => { + stream.on('error', err => dataSocket.emit('error', err)); + + dataSocket.on('end', () => stream.end(() => resolve(this.reply(226)))); + dataSocket.on('error', err => reject(err)); + dataSocket.on('data', data => stream.write(data, this.encoding)); + this.reply(150).then(() => dataSocket.resume()); + }); + }) + .catch(when.TimeoutError, err => { + log.error(err); + return this.reply(425, 'No connection established'); + }) + .catch(err => { + log.error(err); + return this.reply(553); + }) + .finally(() => { + this.connector.end(); + this.commandSocket.resume(); + }); + }, + syntax: '{{cmd}} [path]', + description: 'Store data as a file at the server site' +} diff --git a/src/commands/registration/stru.js b/src/commands/registration/stru.js new file mode 100644 index 00000000..d025e129 --- /dev/null +++ b/src/commands/registration/stru.js @@ -0,0 +1,11 @@ +module.exports = { + directive: 'STRU', + handler: function ({command} = {}) { + return this.reply(command._[1] === 'F' ? 200 : 504); + }, + syntax: '{{cmd}} [structure]', + description: 'Set file transfer structure', + flags: { + obsolete: true + } +} diff --git a/src/commands/registration/syst.js b/src/commands/registration/syst.js new file mode 100644 index 00000000..1b1e1552 --- /dev/null +++ b/src/commands/registration/syst.js @@ -0,0 +1,11 @@ +module.exports = { + directive: 'SYST', + handler: function () { + return this.reply(215); + }, + syntax: '{{cmd}}', + description: 'Return system type', + flags: { + no_auth: true + } +} diff --git a/src/commands/registration/type.js b/src/commands/registration/type.js new file mode 100644 index 00000000..b139fbdd --- /dev/null +++ b/src/commands/registration/type.js @@ -0,0 +1,20 @@ +const _ = require('lodash'); + +module.exports = { + directive: 'TYPE', + handler: function ({command} = {}) { + const encoding = _.upperCase(command._[1]); + switch (encoding) { + case 'A': + this.encoding = 'utf-8'; + case 'I': + case 'L': + this.encoding = 'binary'; + return this.reply(200); + default: + return this.reply(501); + } + }, + syntax: '{{cmd}} [mode]', + description: 'Set the transfer mode, binary (I) or utf-8 (A)' +} diff --git a/src/commands/registration/user.js b/src/commands/registration/user.js new file mode 100644 index 00000000..2e7fc442 --- /dev/null +++ b/src/commands/registration/user.js @@ -0,0 +1,24 @@ +module.exports = { + directive: 'USER', + handler: function ({log, command} = {}) { + console.log('HANDLE USER') + if (this.username) return this.reply(530, 'Username already set'); + this.username = command._[1]; + if (this.server.options.anonymous === true) { + return this.login(this.username, '@anonymous') + .then(() => { + return this.reply(230); + }) + .catch(err => { + log.error(err); + return this.reply(530, err || 'Authentication failed'); + }); + } + return this.reply(331); + }, + syntax: '{{cmd}} [username]', + description: 'Authentication username', + flags: { + no_auth: true + } +} diff --git a/src/commands/registry.js b/src/commands/registry.js index 551729ba..25dd038e 100644 --- a/src/commands/registry.js +++ b/src/commands/registry.js @@ -1,200 +1,41 @@ -module.exports = { - AUTH: { - handler: require('./auth'), - syntax: 'AUTH [type]', - help: 'Not supported', - no_auth: true - }, - USER: { - handler: require('./user'), - syntax: 'USER [username]', - help: 'Authentication username', - no_auth: true - }, - PASS: { - handler: require('./pass'), - syntax: 'PASS [password]', - help: 'Authentication password', - no_auth: true - }, - SYST: { - handler: require('./syst'), - syntax: 'SYST', - help: 'Return system type', - no_auth: true - }, - FEAT: { - handler: require('./feat'), - syntax: 'FEAT', - help: 'Get the feature list implemented by the server', - no_auth: true - }, - PWD: { - handler: require('./pwd'), - syntax: 'PWD', - help: 'Print current working directory' - }, - XPWD: { - handler: require('./pwd'), - syntax: 'XPWD', - help: 'Print current working directory' - }, - TYPE: { - handler: require('./type'), - syntax: 'TYPE', - help: 'Set the transfer mode' - }, - PASV: { - handler: require('./pasv'), - syntax: 'PASV', - help: 'Initiate passive mode' - }, - PORT: { - handler: require('./port'), - syntax: 'PORT [x,x,x,x,y,y]', - help: 'Specifies an address and port to which the server should connect' - }, - LIST: { - handler: require('./list'), - syntax: 'LIST [path(optional)]', - help: 'Returns information of a file or directory if specified, else information of the current working directory is returned' - }, - NLST: { - handler: require('./list'), - syntax: 'NLST [path(optional)]', - help: 'Returns a list of file names in a specified directory' - }, - CWD: { - handler: require('./cwd'), - syntax: 'CWD [path]', - help: 'Change working directory' - }, - XCWD: { - handler: require('./cwd'), - syntax: 'XCWD [path]', - help: 'Change working directory' - }, - CDUP: { - handler: require('./cdup'), - syntax: 'CDUP', - help: 'Change to Parent Directory' - }, - XCUP: { - handler: require('./cdup'), - syntax: 'XCUP', - help: 'Change to Parent Directory' - }, - STOR: { - handler: require('./stor'), - syntax: 'STOR [path]', - help: 'Accept the data and to store the data as a file at the server site' - }, - APPE: { - handler: require('./stor'), - syntax: 'APPE [path]', - help: 'Append to file' - }, - RETR: { - handler: require('./retr'), - syntax: 'RETR [path]', - help: 'Retrieve a copy of the file' - }, - DELE: { - handler: require('./dele'), - syntax: 'DELE [path]', - help: 'Delete file' - }, - RMD: { - handler: require('./dele'), - syntax: 'RMD [path]', - help: 'Remove a directory' - }, - XRMD: { - handler: require('./dele'), - syntax: 'XRMD [path]', - help: 'Remove a directory' - }, - HELP: { - handler: require('./help'), - syntax: 'HELP [command(optional)]', - help: 'Returns usage documentation on a command if specified, else a general help document is returned' - }, - MDTM: { - handler: require('./mdtm'), - syntax: 'MDTM [path]', - help: 'Return the last-modified time of a specified file', - feat: 'MDTM' - }, - MKD: { - handler: require('./mkd'), - syntax: 'MKD [path]', - help: 'Make directory' - }, - XMKD: { - handler: require('./mkd'), - syntax: 'XMKD [path]', - help: 'Make directory' - }, - NOOP: { - handler: require('./noop'), - syntax: 'NOOP', - help: 'No operation', - no_auth: true - }, - QUIT: { - handler: require('./quit'), - syntax: 'QUIT', - help: 'Disconnect', - no_auth: true - }, - RNFR: { - handler: require('./rnfr'), - syntax: 'RNFR [name]', - help: 'Rename from' - }, - RNTO: { - handler: require('./rnto'), - syntax: 'RNTO [name]', - help: 'Rename to' - }, - SIZE: { - handler: require('./size'), - syntax: 'SIZE [path]', - help: 'Return the size of a file', - feat: 'SIZE' - }, - STAT: { - handler: require('./stat'), - syntax: 'SIZE [path(optional)]', - help: 'Returns the current status' - }, - SITE: { - handler: require('./site'), - syntax: 'SITE [subVerb] [subParams]', - help: 'Sends site specific commands to remote server' - }, - OPTS: { - handler: require('./opts'), - syntax: 'OPTS', - help: 'Select options for a feature' - }, +const commands = [ + require('./registration/allo'), + require('./registration/appe'), + require('./registration/auth'), + require('./registration/cdup'), + require('./registration/cwd'), + require('./registration/dele'), + require('./registration/feat'), + require('./registration/help'), + require('./registration/list'), + require('./registration/mdtm'), + require('./registration/mkd'), + require('./registration/mode'), + require('./registration/nlst'), + require('./registration/noop'), + require('./registration/opts'), + require('./registration/pass'), + require('./registration/pasv'), + require('./registration/port'), + require('./registration/pwd'), + require('./registration/retr'), + require('./registration/rmd'), + require('./registration/rnfr'), + require('./registration/rnto'), + require('./registration/site'), + require('./registration/size'), + require('./registration/stat'), + require('./registration/stor'), + require('./registration/stru'), + require('./registration/syst'), + require('./registration/type'), + require('./registration/user') +]; - STRU: { - handler: require('./stru'), - syntax: 'STRU [structure]', - help: 'Set file transfer structure', - obsolete: true - }, - ALLO: { - handler: require('./allo'), - syntax: 'ALLO', - help: 'Allocate sufficient disk space to receive a file', - obsolete: true - }, - MODE: { - handler: require('./mode'), - syntax: 'MODE [mode]', - help: 'Sets the transfer mode (Stream, Block, or Compressed)', - obsolete: true - } -}; +const registry = commands.reduce((result, cmd) => { + const aliases = Array.isArray(cmd.directive) ? cmd.directive : [cmd.directive]; + aliases.forEach(alias => result[alias] = cmd); + return result; +}, {}); + +module.exports = registry; diff --git a/src/commands/retr.js b/src/commands/retr.js deleted file mode 100644 index d03d046e..00000000 --- a/src/commands/retr.js +++ /dev/null @@ -1,36 +0,0 @@ -const when = require('when'); - -module.exports = 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'); - - let dataSocket; - return this.connector.waitForConnection() - .then(socket => { - this.commandSocket.pause(); - dataSocket = socket; - }) - .then(() => when(this.fs.read(command._[1]))) - .then(stream => { - return when.promise((resolve, reject) => { - dataSocket.on('error', err => stream.emit('error', err)); - - stream.on('data', data => dataSocket.write(data, this.encoding)); - stream.on('end', () => resolve(this.reply(226))); - stream.on('error', err => reject(err)); - this.reply(150).then(() => dataSocket.resume()); - }); - }) - .catch(when.TimeoutError, err => { - log.error(err); - return this.reply(425, 'No connection established'); - }) - .catch(err => { - log.error(err); - return this.reply(551); - }) - .finally(() => { - this.connector.end(); - this.commandSocket.resume(); - }); -} diff --git a/src/commands/rnfr.js b/src/commands/rnfr.js deleted file mode 100644 index 1eefb3a9..00000000 --- a/src/commands/rnfr.js +++ /dev/null @@ -1,17 +0,0 @@ -const when = require('when'); - -module.exports = 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'); - - const fileName = command._[1]; - return when(this.fs.get(fileName)) - .then(() => { - this.renameFrom = fileName; - return this.reply(350); - }) - .catch(err => { - log.error(err); - return this.reply(550); - }) -} diff --git a/src/commands/rnto.js b/src/commands/rnto.js deleted file mode 100644 index b5e7cd12..00000000 --- a/src/commands/rnto.js +++ /dev/null @@ -1,23 +0,0 @@ -const when = require('when'); - -module.exports = function ({log, command} = {}) { - if (!this.renameFrom) return this.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'); - - const from = this.renameFrom; - const to = command._[1]; - - return when(this.fs.rename(from, to)) - .then(() => { - return this.reply(250); - }) - .catch(err => { - log.error(err); - return this.reply(550); - }) - .finally(() => { - delete this.renameFrom; - }); -} diff --git a/src/commands/site/index.js b/src/commands/site/index.js deleted file mode 100644 index b0c68f99..00000000 --- a/src/commands/site/index.js +++ /dev/null @@ -1,18 +0,0 @@ -const _ = require('lodash'); -const when = require('when'); -const registry = require('./registry'); - -module.exports = function ({log, command} = {}) { - let [, subverb, ...subparameters] = command._; - subverb = _.upperCase(subverb); - const subLog = log.child({subverb}); - - if (!registry.hasOwnProperty(subverb)) return this.reply(502); - - const subCommand = { - _: [subverb, ...subparameters], - directive: subverb - } - const handler = registry[subverb].handler.bind(this); - return when.try(handler, { log: subLog, command: subCommand }); -} diff --git a/src/commands/size.js b/src/commands/size.js deleted file mode 100644 index f4370b1a..00000000 --- a/src/commands/size.js +++ /dev/null @@ -1,15 +0,0 @@ -const when = require('when'); - -module.exports = 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'); - - return when(this.fs.get(command._[1])) - .then(fileStat => { - return this.reply(213, {message: fileStat.size}); - }) - .catch(err => { - log.error(err); - return this.reply(550); - }); -} diff --git a/src/commands/stat.js b/src/commands/stat.js deleted file mode 100644 index 6d561f48..00000000 --- a/src/commands/stat.js +++ /dev/null @@ -1,37 +0,0 @@ -const _ = require('lodash'); -const when = require('when'); -const getFileStat = require('../helpers/file-stat'); - -module.exports = function (args = {}) { - const {log, command} = args; - const path = command._[1]; - 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'); - - return when(this.fs.get(path)) - .then(stat => { - if (stat.isDirectory()) { - return when(this.fs.list(path)) - .then(files => { - const fileList = files.map(file => { - const message = getFileStat(file, _.get(this, 'server.options.file_format', 'ls')); - return { - raw: true, - message - }; - }) - return this.reply(213, 'Status begin', ...fileList, 'Status end'); - }) - } else { - return this.reply(212, getFileStat(stat, _.get(this, 'server.options.file_format', 'ls'))) - } - }) - .catch(err => { - log.error(err); - return this.reply(450); - }) - } else { - return this.reply(211, 'Status OK'); - } -} diff --git a/src/commands/stor.js b/src/commands/stor.js deleted file mode 100644 index e0137218..00000000 --- a/src/commands/stor.js +++ /dev/null @@ -1,38 +0,0 @@ -const when = require('when'); - -module.exports = 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'); - - const append = command.directive === 'APPE'; - - let dataSocket; - return this.connector.waitForConnection() - .then(socket => { - this.commandSocket.pause(); - dataSocket = socket; - }) - .then(() => when(this.fs.write(command._[1], {append}))) - .then(stream => { - return when.promise((resolve, reject) => { - stream.on('error', err => dataSocket.emit('error', err)); - - dataSocket.on('end', () => resolve(this.reply(226))); - dataSocket.on('error', err => reject(err)); - dataSocket.on('data', data => stream.write(data, this.encoding)); - this.reply(150).then(() => dataSocket.resume()); - }); - }) - .catch(when.TimeoutError, err => { - log.error(err); - return this.reply(425, 'No connection established'); - }) - .catch(err => { - log.error(err); - return this.reply(553); - }) - .finally(() => { - this.connector.end(); - this.commandSocket.resume(); - }); -} diff --git a/src/commands/stru.js b/src/commands/stru.js deleted file mode 100644 index 865c1178..00000000 --- a/src/commands/stru.js +++ /dev/null @@ -1,3 +0,0 @@ -module.exports = function ({command} = {}) { - return this.reply(command._[1] === 'F' ? 200 : 504); -} diff --git a/src/commands/syst.js b/src/commands/syst.js deleted file mode 100644 index 63919a11..00000000 --- a/src/commands/syst.js +++ /dev/null @@ -1,3 +0,0 @@ -module.exports = function () { - return this.reply(215); -} diff --git a/src/commands/type.js b/src/commands/type.js deleted file mode 100644 index d128a03e..00000000 --- a/src/commands/type.js +++ /dev/null @@ -1,15 +0,0 @@ -const _ = require('lodash'); - -module.exports = function ({command} = {}) { - const encoding = _.upperCase(command._[1]); - switch (encoding) { - case 'A': - this.encoding = 'utf-8'; - case 'I': - case 'L': - this.encoding = 'binary'; - return this.reply(200); - default: - return this.reply(501); - } -} diff --git a/src/commands/user.js b/src/commands/user.js deleted file mode 100644 index 6cdb5c8a..00000000 --- a/src/commands/user.js +++ /dev/null @@ -1,15 +0,0 @@ -module.exports = function ({log, command} = {}) { - if (this.username) return this.reply(530, 'Username already set'); - this.username = command._[1]; - if (this.server.options.anonymous === true) { - return this.login(this.username, '@anonymous') - .then(() => { - return this.reply(230); - }) - .catch(err => { - log.error(err); - return this.reply(530, err || 'Authentication failed'); - }); - } - return this.reply(331); -}; diff --git a/test/commands/allo.spec.js b/test/commands/allo.spec.js index 8ca3f33b..5ee192a9 100644 --- a/test/commands/allo.spec.js +++ b/test/commands/allo.spec.js @@ -8,7 +8,7 @@ describe(CMD, done => { const mockClient = { reply: () => when.resolve() }; - const CMDFN = require(`../../src/commands/${CMD.toLowerCase()}`).bind(mockClient); + const CMDFN = require(`../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(mockClient); beforeEach(() => { sandbox = sinon.sandbox.create(); diff --git a/test/commands/auth.spec.js b/test/commands/auth.spec.js index 80cf1bd2..adcf6765 100644 --- a/test/commands/auth.spec.js +++ b/test/commands/auth.spec.js @@ -8,7 +8,7 @@ describe(CMD, done => { const mockClient = { reply: () => when.resolve() }; - const CMDFN = require(`../../src/commands/${CMD.toLowerCase()}`).bind(mockClient); + const CMDFN = require(`../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(mockClient); beforeEach(() => { sandbox = sinon.sandbox.create(); diff --git a/test/commands/cdup.spec.js b/test/commands/cdup.spec.js index d2452569..06f0a79f 100644 --- a/test/commands/cdup.spec.js +++ b/test/commands/cdup.spec.js @@ -13,7 +13,7 @@ describe(CMD, done => { chdir: () => when.resolve() } }; - const CMDFN = require(`../../src/commands/${CMD.toLowerCase()}`).bind(mockClient); + const CMDFN = require(`../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(mockClient); beforeEach(() => { sandbox = sinon.sandbox.create(); diff --git a/test/commands/cwd.spec.js b/test/commands/cwd.spec.js index 88c0e094..44fc9a5a 100644 --- a/test/commands/cwd.spec.js +++ b/test/commands/cwd.spec.js @@ -12,7 +12,7 @@ describe(CMD, done => { reply: () => {}, fs: { chdir: () => {} } }; - const CMDFN = require(`../../src/commands/${CMD.toLowerCase()}`).bind(mockClient); + const CMDFN = require(`../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(mockClient); beforeEach(() => { sandbox = sinon.sandbox.create(); @@ -27,7 +27,7 @@ describe(CMD, done => { describe('// check', function () { it('fails on no fs', done => { const badMockClient = { reply: () => {} }; - const BADCMDFN = require(`../../src/commands/${CMD.toLowerCase()}`).bind(badMockClient); + const BADCMDFN = require(`../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(badMockClient); sandbox.stub(badMockClient, 'reply').resolves(); BADCMDFN() .then(() => { @@ -39,7 +39,7 @@ describe(CMD, done => { it('fails on no fs chdir command', done => { const badMockClient = { reply: () => {}, fs: {} }; - const BADCMDFN = require(`../../src/commands/${CMD.toLowerCase()}`).bind(badMockClient); + const BADCMDFN = require(`../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(badMockClient); sandbox.stub(badMockClient, 'reply').resolves(); BADCMDFN() .then(() => { diff --git a/test/commands/dele.spec.js b/test/commands/dele.spec.js index f2cfc34f..92e32916 100644 --- a/test/commands/dele.spec.js +++ b/test/commands/dele.spec.js @@ -12,7 +12,7 @@ describe(CMD, done => { reply: () => {}, fs: { delete: () => {} } }; - const CMDFN = require(`../../src/commands/${CMD.toLowerCase()}`).bind(mockClient); + const CMDFN = require(`../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(mockClient); beforeEach(() => { sandbox = sinon.sandbox.create(); @@ -27,7 +27,7 @@ describe(CMD, done => { describe('// check', function () { it('fails on no fs', done => { const badMockClient = { reply: () => {} }; - const BADCMDFN = require(`../../src/commands/${CMD.toLowerCase()}`).bind(badMockClient); + const BADCMDFN = require(`../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(badMockClient); sandbox.stub(badMockClient, 'reply').resolves(); BADCMDFN() .then(() => { @@ -39,7 +39,7 @@ describe(CMD, done => { it('fails on no fs delete command', done => { const badMockClient = { reply: () => {}, fs: {} }; - const BADCMDFN = require(`../../src/commands/${CMD.toLowerCase()}`).bind(badMockClient); + const BADCMDFN = require(`../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(badMockClient); sandbox.stub(badMockClient, 'reply').resolves(); BADCMDFN() .then(() => { diff --git a/test/commands/help.spec.js b/test/commands/help.spec.js index 3a47db09..53095744 100644 --- a/test/commands/help.spec.js +++ b/test/commands/help.spec.js @@ -8,7 +8,7 @@ describe(CMD, done => { const mockClient = { reply: () => when.resolve() }; - const CMDFN = require(`../../src/commands/${CMD.toLowerCase()}`).bind(mockClient); + const CMDFN = require(`../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(mockClient); beforeEach(() => { sandbox = sinon.sandbox.create(); diff --git a/test/commands/list.spec.js b/test/commands/list.spec.js index 717ef7ce..b27e8da8 100644 --- a/test/commands/list.spec.js +++ b/test/commands/list.spec.js @@ -21,7 +21,7 @@ describe(CMD, done => { } }; const mockSocket = {}; - const CMDFN = require(`../../src/commands/${CMD.toLowerCase()}`).bind(mockClient); + const CMDFN = require(`../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(mockClient); beforeEach(() => { sandbox = sinon.sandbox.create(); @@ -54,7 +54,7 @@ describe(CMD, done => { describe('// check', function () { it('fails on no fs', done => { const badMockClient = { reply: () => {} }; - const BADCMDFN = require(`../../src/commands/${CMD.toLowerCase()}`).bind(badMockClient); + const BADCMDFN = require(`../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(badMockClient); sandbox.stub(badMockClient, 'reply').resolves(); BADCMDFN() .then(() => { @@ -66,7 +66,7 @@ describe(CMD, done => { it('fails on no fs list command', done => { const badMockClient = { reply: () => {}, fs: {} }; - const BADCMDFN = require(`../../src/commands/${CMD.toLowerCase()}`).bind(badMockClient); + const BADCMDFN = require(`../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(badMockClient); sandbox.stub(badMockClient, 'reply').resolves(); BADCMDFN() .then(() => { diff --git a/test/commands/mdtm.spec.js b/test/commands/mdtm.spec.js index c3648a8f..2e9c43e6 100644 --- a/test/commands/mdtm.spec.js +++ b/test/commands/mdtm.spec.js @@ -13,7 +13,7 @@ describe(CMD, done => { fs: { get: () => {} } }; const mockSocket = {}; - const CMDFN = require(`../../src/commands/${CMD.toLowerCase()}`).bind(mockClient); + const CMDFN = require(`../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(mockClient); beforeEach(() => { sandbox = sinon.sandbox.create(); @@ -28,7 +28,7 @@ describe(CMD, done => { describe('// check', function () { it('fails on no fs', done => { const badMockClient = { reply: () => {} }; - const BADCMDFN = require(`../../src/commands/${CMD.toLowerCase()}`).bind(badMockClient); + const BADCMDFN = require(`../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(badMockClient); sandbox.stub(badMockClient, 'reply').resolves(); BADCMDFN() .then(() => { @@ -40,7 +40,7 @@ describe(CMD, done => { it('fails on no fs get command', done => { const badMockClient = { reply: () => {}, fs: {} }; - const BADCMDFN = require(`../../src/commands/${CMD.toLowerCase()}`).bind(badMockClient); + const BADCMDFN = require(`../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(badMockClient); sandbox.stub(badMockClient, 'reply').resolves(); BADCMDFN() .then(() => { diff --git a/test/commands/mkd.spec.js b/test/commands/mkd.spec.js index aa51813a..1b91077e 100644 --- a/test/commands/mkd.spec.js +++ b/test/commands/mkd.spec.js @@ -12,7 +12,7 @@ describe(CMD, done => { reply: () => {}, fs: { mkdir: () => {} } }; - const CMDFN = require(`../../src/commands/${CMD.toLowerCase()}`).bind(mockClient); + const CMDFN = require(`../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(mockClient); beforeEach(() => { sandbox = sinon.sandbox.create(); @@ -27,7 +27,7 @@ describe(CMD, done => { describe('// check', function () { it('fails on no fs', done => { const badMockClient = { reply: () => {} }; - const BADCMDFN = require(`../../src/commands/${CMD.toLowerCase()}`).bind(badMockClient); + const BADCMDFN = require(`../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(badMockClient); sandbox.stub(badMockClient, 'reply').resolves(); BADCMDFN() .then(() => { @@ -39,7 +39,7 @@ describe(CMD, done => { it('fails on no fs mkdir command', done => { const badMockClient = { reply: () => {}, fs: {} }; - const BADCMDFN = require(`../../src/commands/${CMD.toLowerCase()}`).bind(badMockClient); + const BADCMDFN = require(`../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(badMockClient); sandbox.stub(badMockClient, 'reply').resolves(); BADCMDFN() .then(() => { diff --git a/test/commands/mode.spec.js b/test/commands/mode.spec.js index 3f027d79..87f7239b 100644 --- a/test/commands/mode.spec.js +++ b/test/commands/mode.spec.js @@ -8,7 +8,7 @@ describe(CMD, done => { const mockClient = { reply: () => when.resolve() }; - const CMDFN = require(`../../src/commands/${CMD.toLowerCase()}`).bind(mockClient); + const CMDFN = require(`../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(mockClient); beforeEach(() => { sandbox = sinon.sandbox.create(); diff --git a/test/commands/nlst.spec.js b/test/commands/nlst.spec.js index 1d5fcd7f..9f8be7d6 100644 --- a/test/commands/nlst.spec.js +++ b/test/commands/nlst.spec.js @@ -21,7 +21,7 @@ describe(CMD, done => { } }; const mockSocket = {}; - const CMDFN = require(`../../src/commands/list`).bind(mockClient); + const CMDFN = require(`../../src/commands/registration/list`).handler.bind(mockClient); beforeEach(() => { sandbox = sinon.sandbox.create(); diff --git a/test/commands/noop.spec.js b/test/commands/noop.spec.js index ff66abd3..bb34ae5d 100644 --- a/test/commands/noop.spec.js +++ b/test/commands/noop.spec.js @@ -8,7 +8,7 @@ describe(CMD, done => { const mockClient = { reply: () => when.resolve() }; - const CMDFN = require(`../../src/commands/${CMD.toLowerCase()}`).bind(mockClient); + const CMDFN = require(`../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(mockClient); beforeEach(() => { sandbox = sinon.sandbox.create(); diff --git a/test/commands/opts.spec.js b/test/commands/opts.spec.js index c8a11f34..548cc2dc 100644 --- a/test/commands/opts.spec.js +++ b/test/commands/opts.spec.js @@ -8,7 +8,7 @@ describe(CMD, done => { const mockClient = { reply: () => when.resolve() }; - const CMDFN = require(`../../src/commands/${CMD.toLowerCase()}`).bind(mockClient); + const CMDFN = require(`../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(mockClient); beforeEach(() => { sandbox = sinon.sandbox.create(); diff --git a/test/commands/pass.spec.js b/test/commands/pass.spec.js index 60c85227..6ccecbd6 100644 --- a/test/commands/pass.spec.js +++ b/test/commands/pass.spec.js @@ -14,7 +14,7 @@ describe(CMD, done => { server: { options: { anonymous: false } }, username: 'user' }; - const CMDFN = require(`../../src/commands/${CMD.toLowerCase()}`).bind(mockClient); + const CMDFN = require(`../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(mockClient); beforeEach(() => { sandbox = sinon.sandbox.create(); diff --git a/test/commands/pwd.spec.js b/test/commands/pwd.spec.js index b06b34c7..59291d10 100644 --- a/test/commands/pwd.spec.js +++ b/test/commands/pwd.spec.js @@ -12,7 +12,7 @@ describe(CMD, done => { reply: () => {}, fs: { currentDirectory: () => {} } }; - const CMDFN = require(`../../src/commands/${CMD.toLowerCase()}`).bind(mockClient); + const CMDFN = require(`../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(mockClient); beforeEach(() => { sandbox = sinon.sandbox.create(); @@ -27,7 +27,7 @@ describe(CMD, done => { describe('// check', function () { it('fails on no fs', done => { const badMockClient = { reply: () => {} }; - const BADCMDFN = require(`../../src/commands/${CMD.toLowerCase()}`).bind(badMockClient); + const BADCMDFN = require(`../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(badMockClient); sandbox.stub(badMockClient, 'reply').resolves(); BADCMDFN() .then(() => { @@ -39,7 +39,7 @@ describe(CMD, done => { it('fails on no fs currentDirectory command', done => { const badMockClient = { reply: () => {}, fs: {} }; - const BADCMDFN = require(`../../src/commands/${CMD.toLowerCase()}`).bind(badMockClient); + const BADCMDFN = require(`../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(badMockClient); sandbox.stub(badMockClient, 'reply').resolves(); BADCMDFN() .then(() => { diff --git a/test/commands/quit.spec.js b/test/commands/quit.spec.js index 1116ec24..dbf0ae3e 100644 --- a/test/commands/quit.spec.js +++ b/test/commands/quit.spec.js @@ -11,7 +11,7 @@ describe(CMD, done => { const mockClient = { close: () => {} }; - const CMDFN = require(`../../src/commands/${CMD.toLowerCase()}`).bind(mockClient); + const CMDFN = require(`../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(mockClient); beforeEach(() => { sandbox = sinon.sandbox.create(); diff --git a/test/index.spec.js b/test/index.spec.js index 0d70d676..388e4f35 100644 --- a/test/index.spec.js +++ b/test/index.spec.js @@ -6,7 +6,6 @@ const fs = require('fs'); const sinon = require('sinon'); const FtpServer = require('../src'); -const FtpCommands = require('../src/commands'); const FtpClient = require('ftp'); describe('FtpServer', function () {