diff --git a/README.md b/README.md index 8aac47fd..91baebb2 100644 --- a/README.md +++ b/README.md @@ -141,6 +141,11 @@ Options: > Used in `SITE CHMOD` +`getUniqueName()` +> Return a unique file name to write to + +> Used in `STOU` + ## Contributing diff --git a/src/commands/index.js b/src/commands/index.js index 5d5c10b7..a5a0edd2 100644 --- a/src/commands/index.js +++ b/src/commands/index.js @@ -5,7 +5,6 @@ const REGISTRY = require('./registry'); class FtpCommands { constructor(connection) { - console.log(REGISTRY) this.connection = connection; this.previousCommand = {}; this.blacklist = _.get(this.connection, 'server.options.blacklist', []).map(cmd => _.upperCase(cmd)); @@ -31,7 +30,7 @@ class FtpCommands { const commandRegister = REGISTRY[command.directive]; const commandFlags = _.get(commandRegister, 'flags', {}); if (!commandFlags.no_auth && !this.connection.authenticated) { - return this.connection.reply(530); + return this.connection.reply(530, 'Command requires authentication'); } if (!commandRegister.handler) { diff --git a/src/commands/registration/abor.js b/src/commands/registration/abor.js new file mode 100644 index 00000000..2ec2664b --- /dev/null +++ b/src/commands/registration/abor.js @@ -0,0 +1,14 @@ +module.exports = { + directive: 'ABOR', + handler: function () { + return this.connector.waitForConnection() + .then(socket => { + return this.reply(426, {socket}) + .then(() => this.connector.end()); + }) + .catch(() => {}) + .then(() => this.reply(226)); + }, + syntax: '{{cmd}}', + description: 'Abort an active file transfer' +}; diff --git a/src/commands/registration/mode.js b/src/commands/registration/mode.js index 31ba113c..994a6ad0 100644 --- a/src/commands/registration/mode.js +++ b/src/commands/registration/mode.js @@ -1,7 +1,7 @@ module.exports = { directive: 'MODE', handler: function ({command} = {}) { - return this.reply(command._[1] === 'S' ? 200 : 504); + return this.reply(/^S$/i.test(command._[1]) ? 200 : 504); }, syntax: '{{cmd}} [mode]', description: 'Sets the transfer mode (Stream, Block, or Compressed)', diff --git a/src/commands/registration/stor.js b/src/commands/registration/stor.js index 8f6c36b7..cc8d910b 100644 --- a/src/commands/registration/stor.js +++ b/src/commands/registration/stor.js @@ -7,6 +7,7 @@ module.exports = { if (!this.fs.write) return this.reply(402, 'Not supported by file system'); const append = command.directive === 'APPE'; + const fileName = command._[1]; let dataSocket; return this.connector.waitForConnection() @@ -14,12 +15,12 @@ module.exports = { this.commandSocket.pause(); dataSocket = socket; }) - .then(() => when(this.fs.write(command._[1], {append}))) + .then(() => when(this.fs.write(fileName, {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('end', () => stream.end(() => resolve(this.reply(226, fileName)))); dataSocket.on('error', err => reject(err)); dataSocket.on('data', data => stream.write(data, this.encoding)); this.reply(150).then(() => dataSocket.resume()); diff --git a/src/commands/registration/stou.js b/src/commands/registration/stou.js new file mode 100644 index 00000000..9582315f --- /dev/null +++ b/src/commands/registration/stou.js @@ -0,0 +1,20 @@ +const stor = require('./stor').handler; + +module.exports = { + directive: 'STOU', + handler: function (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._[1]; + return this.fs.get(fileName) + .catch(() => fileName) // does not exist, name is unique + .then(() => this.fs.getUniqueName()) // exists, must create new unique name + .then(name => { + args.command._[1] = name; + return stor.call(this, args); + }); + }, + syntax: '{{cmd}}', + description: 'Store file uniquely' +}; diff --git a/src/commands/registration/stru.js b/src/commands/registration/stru.js index d025e129..68470c15 100644 --- a/src/commands/registration/stru.js +++ b/src/commands/registration/stru.js @@ -1,7 +1,7 @@ module.exports = { directive: 'STRU', handler: function ({command} = {}) { - return this.reply(command._[1] === 'F' ? 200 : 504); + return this.reply(/^F$/i.test(command._[1]) ? 200 : 504); }, syntax: '{{cmd}} [structure]', description: 'Set file transfer structure', diff --git a/src/commands/registration/user.js b/src/commands/registration/user.js index 2e7fc442..b8a26081 100644 --- a/src/commands/registration/user.js +++ b/src/commands/registration/user.js @@ -1,7 +1,6 @@ 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) { diff --git a/src/commands/registry.js b/src/commands/registry.js index 25dd038e..12ab5726 100644 --- a/src/commands/registry.js +++ b/src/commands/registry.js @@ -1,4 +1,5 @@ const commands = [ + require('./registration/abor'), require('./registration/allo'), require('./registration/appe'), require('./registration/auth'), @@ -26,6 +27,7 @@ const commands = [ require('./registration/size'), require('./registration/stat'), require('./registration/stor'), + require('./registration/stou'), require('./registration/stru'), require('./registration/syst'), require('./registration/type'), diff --git a/src/connection.js b/src/connection.js index fec36908..3fa7fdc1 100644 --- a/src/connection.js +++ b/src/connection.js @@ -2,7 +2,7 @@ const _ = require('lodash'); const uuid = require('uuid'); const when = require('when'); const sequence = require('when/sequence'); -const parseSentence = require('minimist-string'); +const parseCommandString = require('minimist-string'); const net = require('net'); const BaseConnector = require('./connector/base'); @@ -16,28 +16,26 @@ class FtpConnection { this.server = server; this.commandSocket = options.socket; this.id = uuid.v4(); - this.log = options.log.child({ftp_session_id: this.commandSocket.ftp_session_id}); + this.log = options.log.child({id: this.id}); this.commands = new Commands(this); this.encoding = 'utf-8'; this.connector = new BaseConnector(this); this.commandSocket.on('error', err => { - console.log('error', err) + this.server.server.emit('error', {connection: this, error: err}); }); this.commandSocket.on('data', data => { const messages = _.compact(data.toString('utf-8').split('\r\n')); const handleMessage = (message) => { - const command = parseSentence(message); + const command = parseCommandString(message); command.directive = _.upperCase(command._[0]); return this.commands.handle(command); }; return sequence(messages.map(message => handleMessage.bind(this, message))); }); - this.commandSocket.on('timeout', () => { - console.log('timeout') - }); + this.commandSocket.on('timeout', () => {}); this.commandSocket.on('close', () => { if (this.connector) this.connector.end(); if (this.commandSocket && !this.commandSocket.destroyed) this.commandSocket.destroy(); diff --git a/src/connector/active.js b/src/connector/active.js index 68f02c98..b4dde230 100644 --- a/src/connector/active.js +++ b/src/connector/active.js @@ -8,12 +8,12 @@ class Active extends Connector { this.type = 'active'; } - waitForConnection() { + waitForConnection({timeout = 5000, delay = 250} = {}) { return when.iterate( () => {}, () => this.dataSocket && this.dataSocket.connected, - () => when().delay(250) - ).timeout(5000) + () => when().delay(delay) + ).timeout(timeout) .then(() => this.dataSocket); } diff --git a/src/connector/base.js b/src/connector/base.js index 922ae54c..4e7a61ad 100644 --- a/src/connector/base.js +++ b/src/connector/base.js @@ -21,8 +21,7 @@ class Connector { if (this.dataServer) this.dataServer.close(); this.dataSocket = null; this.dataServer = null; - - this.connection.connector = new Connector(this.connection); + this.type = false; } } module.exports = Connector; diff --git a/src/connector/passive.js b/src/connector/passive.js index 535c51a0..8ed2662a 100644 --- a/src/connector/passive.js +++ b/src/connector/passive.js @@ -10,15 +10,15 @@ class Passive extends Connector { this.type = 'passive'; } - waitForConnection() { + waitForConnection({timeout = 5000, delay = 250} = {}) { if (!this.dataServer) { return when.reject(new errors.ConnectorError('Passive server not setup')); } return when.iterate( () => {}, - () => this.dataServer && this.dataServer.listening && this.dataSocket, - () => when().delay(250) - ).timeout(5000) + () => this.dataServer && this.dataServer.listening && this.dataSocket && this.dataSocket.connected, + () => when().delay(delay) + ).timeout(timeout) .then(() => this.dataSocket); } @@ -44,18 +44,18 @@ class Passive extends Connector { return this.connection.reply(550, 'Remote addresses do not match') .finally(() => this.connection.close()); } - this.log.info({port}, 'Passive connection fulfilled.'); + this.log.debug({port}, 'Passive connection fulfilled.'); this.dataSocket = socket; + this.dataSocket.connected = true; this.dataSocket.setEncoding(this.connection.encoding); - this.dataSocket.on('data', data => { - - }); this.dataSocket.on('close', () => { + this.log.debug('Passive connection closed'); this.end(); }); }); this.dataServer.on('close', () => { + this.log.debug('Passive server closed'); this.dataServer = null; }); diff --git a/src/fs.js b/src/fs.js index 94abab2a..c0a6a881 100644 --- a/src/fs.js +++ b/src/fs.js @@ -1,5 +1,6 @@ const _ = require('lodash'); const nodePath = require('path'); +const uuid = require('uuid'); const when = require('when'); const whenNode = require('when/node'); const syncFs = require('fs'); @@ -97,5 +98,9 @@ class FileSystem { path = nodePath.resolve(this.cwd, path); return fs.chmod(path, mode); } + + getUniqueName() { + return uuid.v4().replace(/\W/g, ''); + } } module.exports = FileSystem; diff --git a/test/start.js b/test/start.js index 3071ef3c..be638a67 100644 --- a/test/start.js +++ b/test/start.js @@ -3,7 +3,8 @@ const bunyan = require('bunyan'); const FtpServer = require('../src'); -const log = bunyan.createLogger({name: 'test', level: 10}); +const log = bunyan.createLogger({name: 'test'}); +log.level(process.env.LOG_LEVEL || 'trace'); const server = new FtpServer(process.env.FTP_URL, { log, pasv_range: process.env.PASV_RANGE