From 89af5670e94b37ebe5967e7950bc0aa5d722ce20 Mon Sep 17 00:00:00 2001 From: Rian Wouters Date: Thu, 19 May 2016 12:38:46 +0000 Subject: [PATCH 01/36] Patch stack size problem for large number of file --- lib/FtpConnection.js | 39 +++++++++++++++------------------------ 1 file changed, 15 insertions(+), 24 deletions(-) diff --git a/lib/FtpConnection.js b/lib/FtpConnection.js index 828e1a8..1736e8a 100644 --- a/lib/FtpConnection.js +++ b/lib/FtpConnection.js @@ -494,33 +494,24 @@ FtpConnection.prototype._LIST = function(commandArg, detailed, cmd) { var CONC = self.server.options.maxStatsAtOnce; var j = 0; - for (var i = 0; i < files.length && i < CONC; ++i) { - handleFile(i); - } - j = --i; - - function handleFile(ii) { - if (i >= files.length) { - return i === files.length + j ? finished() : null; - } + for (var i = 0; i < files.length; ++i) + handleFile(files[i]); - self.server.getUsernameFromUid(files[ii].stats.uid, function(e1, uname) { - self.server.getGroupFromGid(files[ii].stats.gid, function(e2, gname) { + function handleFile(file) { + self.server.getUsernameFromUid(file.stats.uid, function(e1, uname) { + self.server.getGroupFromGid(file.stats.gid, function(e2, gname) { if (e1 || e2) { - self._logIf(LOG.WARN, 'Error getting user/group name for file: ' + util.inspect(e1 || e2)); - fileInfos.push({ - file: files[ii], - uname: null, - gname: null, - }); - } else { - fileInfos.push({ - file: files[ii], - uname: uname, - gname: gname, - }); + self._logIf(3, "Error getting user/group name for file: " + util.inspect(e1 || e2)); + } + fileInfos.push({ + file: file, + uname: uname, + gname: gname + }); + j++; + if (j === files.length) { + finished() } - handleFile(++i); }); }); } From 309f501d34a080b5986451a1457efbb67fa4a5c5 Mon Sep 17 00:00:00 2001 From: Rian Wouters Date: Tue, 31 May 2016 09:37:35 +0000 Subject: [PATCH 02/36] Abstract normal case into part range probing --- lib/FtpConnection.js | 44 ++++++++++++++++++++------------------------ 1 file changed, 20 insertions(+), 24 deletions(-) diff --git a/lib/FtpConnection.js b/lib/FtpConnection.js index 1736e8a..fa03f9e 100644 --- a/lib/FtpConnection.js +++ b/lib/FtpConnection.js @@ -775,35 +775,31 @@ FtpConnection.prototype._setupNewPASV = function(commandArg, command) { self.pasv = null; } - if (self.server.options.pasvPortRangeStart != null && self.server.options.pasvPortRangeEnd != null) { - // Keep trying ports in the range supplied until either: - // (i) It works - // (ii) We get an error that's not just EADDRINUSE - // (iii) We run out of ports to try. - var i = self.server.options.pasvPortRangeStart; - pasv.listen(i); - portRangeErrorHandler = function(e) { - if (e.code === 'EADDRINUSE' && i < self.server.options.pasvPortRangeEnd) { - pasv.listen(++i); - } else { - self._logIf(LOG.DEBUG, 'Passing on error from portRangeErrorHandler to normalErrorHandler:' + JSON.stringify(e)); - normalErrorHandler(e); - } - }; - pasv.on('error', portRangeErrorHandler); - } else { - pasv.listen(0); - pasv.on('error', normalErrorHandler); - } + var startPort = this.server.options.pasvPortRangeStart; + var endPort = this.server.options.pasvPortRangeEnd; + + // Keep trying ports in the range supplied until either: + // (i) It works + // (ii) We get an error that's not just EADDRINUSE + // (iii) We run out of ports to try. + var i = startPort || endPort || 0; + pasv.listen(i); + portRangeErrorHandler = function(e) { + if (e.code === 'EADDRINUSE' && i < (endPort || startPort || 0)) { + pasv.listen(++i); + } else { + self._logIf(LOG.DEBUG, 'Passing on error from portRangeErrorHandler to normalErrorHandler:' + JSON.stringify(e)); + normalErrorHandler(e); + } + }; + pasv.on('error', portRangeErrorHandler); // Once we're successfully listening, tell the client pasv.on('listening', function() { self.pasv = pasv; - if (portRangeErrorHandler) { - pasv.removeListener('error', portRangeErrorHandler); - pasv.addListener('error', normalErrorHandler); - } + pasv.removeListener('error', portRangeErrorHandler); + pasv.addListener('error', normalErrorHandler); self._logIf(LOG.DEBUG, 'Passive data connection beginning to listen'); From 2a66128735b336766f57011351be3ab82f09eada Mon Sep 17 00:00:00 2001 From: Rian Wouters Date: Tue, 31 May 2016 09:39:01 +0000 Subject: [PATCH 03/36] Rename: i->port --- lib/FtpConnection.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/FtpConnection.js b/lib/FtpConnection.js index fa03f9e..a9d2479 100644 --- a/lib/FtpConnection.js +++ b/lib/FtpConnection.js @@ -782,11 +782,11 @@ FtpConnection.prototype._setupNewPASV = function(commandArg, command) { // (i) It works // (ii) We get an error that's not just EADDRINUSE // (iii) We run out of ports to try. - var i = startPort || endPort || 0; - pasv.listen(i); + var port = startPort || endPort || 0; + pasv.listen(port); portRangeErrorHandler = function(e) { - if (e.code === 'EADDRINUSE' && i < (endPort || startPort || 0)) { - pasv.listen(++i); + if (e.code === 'EADDRINUSE' && port < (endPort || startPort || 0)) { + pasv.listen(++port); } else { self._logIf(LOG.DEBUG, 'Passing on error from portRangeErrorHandler to normalErrorHandler:' + JSON.stringify(e)); normalErrorHandler(e); From ca3e13c770a0b63473b103c83ea28ef1c3da43b9 Mon Sep 17 00:00:00 2001 From: Rian Wouters Date: Tue, 31 May 2016 10:33:37 +0000 Subject: [PATCH 04/36] Fix eslint errors --- lib/FtpConnection.js | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/lib/FtpConnection.js b/lib/FtpConnection.js index a9d2479..c2096f9 100644 --- a/lib/FtpConnection.js +++ b/lib/FtpConnection.js @@ -492,25 +492,25 @@ FtpConnection.prototype._LIST = function(commandArg, detailed, cmd) { // Now we need to get username and group name for each file from user/group ids. fileInfos = []; - var CONC = self.server.options.maxStatsAtOnce; var j = 0; - for (var i = 0; i < files.length; ++i) + for (var i = 0; i < files.length; ++i) { handleFile(files[i]); + } function handleFile(file) { self.server.getUsernameFromUid(file.stats.uid, function(e1, uname) { self.server.getGroupFromGid(file.stats.gid, function(e2, gname) { if (e1 || e2) { - self._logIf(3, "Error getting user/group name for file: " + util.inspect(e1 || e2)); + self._logIf(3, 'Error getting user/group name for file: ' + util.inspect(e1 || e2)); } fileInfos.push({ file: file, uname: uname, - gname: gname + gname: gname, }); j++; if (j === files.length) { - finished() + finished(); } }); }); @@ -782,11 +782,10 @@ FtpConnection.prototype._setupNewPASV = function(commandArg, command) { // (i) It works // (ii) We get an error that's not just EADDRINUSE // (iii) We run out of ports to try. - var port = startPort || endPort || 0; - pasv.listen(port); + pasv.listen(startPort || endPort || 0); portRangeErrorHandler = function(e) { - if (e.code === 'EADDRINUSE' && port < (endPort || startPort || 0)) { - pasv.listen(++port); + if (e.code === 'EADDRINUSE' && e.port < (endPort || startPort || 0)) { + pasv.listen(++e.port); } else { self._logIf(LOG.DEBUG, 'Passing on error from portRangeErrorHandler to normalErrorHandler:' + JSON.stringify(e)); normalErrorHandler(e); From 3377c37dd4d036951923fa4e8c5620c4fb452bd1 Mon Sep 17 00:00:00 2001 From: Rian Wouters Date: Tue, 31 May 2016 10:56:20 +0000 Subject: [PATCH 05/36] Start listening only after even registration --- lib/FtpConnection.js | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/lib/FtpConnection.js b/lib/FtpConnection.js index c2096f9..68cc77d 100644 --- a/lib/FtpConnection.js +++ b/lib/FtpConnection.js @@ -764,9 +764,11 @@ FtpConnection.prototype._setupNewPASV = function(commandArg, command) { var self = this; var pasv = self._createPassiveServer(); - var portRangeErrorHandler; - function normalErrorHandler(e) { + var startPort = this.server.options.pasvPortRangeStart; + var endPort = this.server.options.pasvPortRangeEnd; + + var normalErrorHandler = function(e) { self._logIf(LOG.WARN, 'Error with passive data listener: ' + util.inspect(e)); self.respond('421 Server was unable to open passive connection listener'); self.dataConfigured = false; @@ -775,15 +777,11 @@ FtpConnection.prototype._setupNewPASV = function(commandArg, command) { self.pasv = null; } - var startPort = this.server.options.pasvPortRangeStart; - var endPort = this.server.options.pasvPortRangeEnd; - // Keep trying ports in the range supplied until either: // (i) It works // (ii) We get an error that's not just EADDRINUSE // (iii) We run out of ports to try. - pasv.listen(startPort || endPort || 0); - portRangeErrorHandler = function(e) { + var portRangeErrorHandler = function(e) { if (e.code === 'EADDRINUSE' && e.port < (endPort || startPort || 0)) { pasv.listen(++e.port); } else { @@ -798,7 +796,7 @@ FtpConnection.prototype._setupNewPASV = function(commandArg, command) { self.pasv = pasv; pasv.removeListener('error', portRangeErrorHandler); - pasv.addListener('error', normalErrorHandler); + pasv.on('error', normalErrorHandler); self._logIf(LOG.DEBUG, 'Passive data connection beginning to listen'); @@ -807,11 +805,14 @@ FtpConnection.prototype._setupNewPASV = function(commandArg, command) { self._logIf(LOG.DEBUG, 'Passive data connection listening on port ' + port); self._writePASVReady(command); }); + pasv.on('close', function() { self.pasv = null; self.dataListener = null; self._logIf(LOG.DEBUG, 'Passive data listener closed'); }); + + pasv.listen(startPort || endPort || 0); }; FtpConnection.prototype._command_PBSZ = function(commandArg) { From 5ebe0c3354c7a28bea02e7dfebac6085ce73e96b Mon Sep 17 00:00:00 2001 From: Rian Wouters Date: Tue, 31 May 2016 12:30:25 +0000 Subject: [PATCH 06/36] Fix eslint error --- lib/FtpConnection.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/FtpConnection.js b/lib/FtpConnection.js index 68cc77d..59ca4f0 100644 --- a/lib/FtpConnection.js +++ b/lib/FtpConnection.js @@ -775,7 +775,7 @@ FtpConnection.prototype._setupNewPASV = function(commandArg, command) { self.dataListener = null; self.dataSocket = null; self.pasv = null; - } + }; // Keep trying ports in the range supplied until either: // (i) It works From 08d38eebfa031ab220c01b2302f1ad7ba3df1f88 Mon Sep 17 00:00:00 2001 From: Rian Wouters Date: Tue, 31 May 2016 15:47:20 +0000 Subject: [PATCH 07/36] Refactor --- lib/FtpConnection.js | 71 ++++++++++++++++++++------------------------ 1 file changed, 33 insertions(+), 38 deletions(-) diff --git a/lib/FtpConnection.js b/lib/FtpConnection.js index 59ca4f0..d33b305 100644 --- a/lib/FtpConnection.js +++ b/lib/FtpConnection.js @@ -760,13 +760,15 @@ FtpConnection.prototype._writePASVReady = function(command) { } }; +// Keep trying ports in the range supplied until either: +// (i) It works +// (ii) We get an error that's not just EADDRINUSE +// (iii) We run out of ports to try. FtpConnection.prototype._setupNewPASV = function(commandArg, command) { var self = this; - var pasv = self._createPassiveServer(); - - var startPort = this.server.options.pasvPortRangeStart; - var endPort = this.server.options.pasvPortRangeEnd; + var firstPort = this.server.options.pasvPortRangeStart; + var lastPort = this.server.options.pasvPortRangeEnd; var normalErrorHandler = function(e) { self._logIf(LOG.WARN, 'Error with passive data listener: ' + util.inspect(e)); @@ -777,42 +779,35 @@ FtpConnection.prototype._setupNewPASV = function(commandArg, command) { self.pasv = null; }; - // Keep trying ports in the range supplied until either: - // (i) It works - // (ii) We get an error that's not just EADDRINUSE - // (iii) We run out of ports to try. - var portRangeErrorHandler = function(e) { - if (e.code === 'EADDRINUSE' && e.port < (endPort || startPort || 0)) { - pasv.listen(++e.port); - } else { + this._createPassiveServer() + .on('error', function(e) { + if (e.code === 'EADDRINUSE' && e.port < (lastPort || firstPort || 0)) { + this.listen(++e.port); + return; + } + self._logIf(LOG.DEBUG, 'Passing on error from portRangeErrorHandler to normalErrorHandler:' + JSON.stringify(e)); normalErrorHandler(e); - } - }; - pasv.on('error', portRangeErrorHandler); - - // Once we're successfully listening, tell the client - pasv.on('listening', function() { - self.pasv = pasv; - - pasv.removeListener('error', portRangeErrorHandler); - pasv.on('error', normalErrorHandler); - - self._logIf(LOG.DEBUG, 'Passive data connection beginning to listen'); - - var port = pasv.address().port; - self.dataListener = new PassiveListener(); - self._logIf(LOG.DEBUG, 'Passive data connection listening on port ' + port); - self._writePASVReady(command); - }); - - pasv.on('close', function() { - self.pasv = null; - self.dataListener = null; - self._logIf(LOG.DEBUG, 'Passive data listener closed'); - }); - - pasv.listen(startPort || endPort || 0); + }) + + // Once we're successfully listening, tell the client + .on('listening', function() { + this + .removeAllListeners('error') + .on('error', normalErrorHandler); + self.pasv = this; + self.dataListener = new PassiveListener(); + self._logIf(LOG.DEBUG, 'Passive data connection listening on port ' + this.address().port); + self._writePASVReady(command); + }) + + .on('close', function() { + self.pasv = null; + self.dataListener = null; + self._logIf(LOG.DEBUG, 'Passive data listener closed'); + }) + + .listen(firstPort || lastPort || 0); }; FtpConnection.prototype._command_PBSZ = function(commandArg) { From cac027ff78ce05147911eeef489333bb8b0c1ef4 Mon Sep 17 00:00:00 2001 From: Rian Wouters Date: Fri, 3 Jun 2016 12:26:03 +0000 Subject: [PATCH 08/36] Eliminate dataListener --- lib/FtpConnection.js | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/lib/FtpConnection.js b/lib/FtpConnection.js index d33b305..4f2d8cb 100644 --- a/lib/FtpConnection.js +++ b/lib/FtpConnection.js @@ -11,7 +11,6 @@ var dateformat = require('dateformat'); var glob = require('./glob'); var starttls = require('./starttls'); var Constants = require('./Constants'); -var PassiveListener = require('./PassiveListener'); var pathEscape = require('./helpers/pathEscape'); var withCwd = require('./helpers/withCwd'); @@ -113,8 +112,8 @@ FtpConnection.prototype._createPassiveServer = function() { } function setupPassiveListener() { - if (self.dataListener) { - self.dataListener.emit('ready'); + if (self.passv) { + self.passv.emit('ready'); } else { self._logIf(LOG.WARN, 'Passive connection initiated, but no data listener'); } @@ -147,7 +146,7 @@ FtpConnection.prototype._createPassiveServer = function() { FtpConnection.prototype._whenDataReady = function(callback) { var self = this; - if (self.dataListener) { + if (self.passv) { // how many data connections are allowed? // should still be listening since we created a server, right? if (self.dataSocket) { @@ -155,7 +154,7 @@ FtpConnection.prototype._whenDataReady = function(callback) { callback(self.dataSocket); } else { self._logIf(LOG.DEBUG, 'Currently no data connection; expecting client to connect to pasv server shortly...'); - self.dataListener.once('ready', function() { + self.passv.once('ready', function() { self._logIf(LOG.DEBUG, '...client has connected now'); callback(self.dataSocket); }); @@ -734,7 +733,7 @@ FtpConnection.prototype._PASV = function(commandArg, command) { self._closeSocket(self.dataSocket, true); } - if (self.dataListener) { + if (self.passv) { self._logIf(LOG.DEBUG, 'Telling client that they can connect now'); self._writePASVReady(command); } else { @@ -774,7 +773,6 @@ FtpConnection.prototype._setupNewPASV = function(commandArg, command) { self._logIf(LOG.WARN, 'Error with passive data listener: ' + util.inspect(e)); self.respond('421 Server was unable to open passive connection listener'); self.dataConfigured = false; - self.dataListener = null; self.dataSocket = null; self.pasv = null; }; @@ -796,14 +794,12 @@ FtpConnection.prototype._setupNewPASV = function(commandArg, command) { .removeAllListeners('error') .on('error', normalErrorHandler); self.pasv = this; - self.dataListener = new PassiveListener(); self._logIf(LOG.DEBUG, 'Passive data connection listening on port ' + this.address().port); self._writePASVReady(command); }) .on('close', function() { self.pasv = null; - self.dataListener = null; self._logIf(LOG.DEBUG, 'Passive data listener closed'); }) From 2d33f74eab057ba9bb576c91e3689967896e98b5 Mon Sep 17 00:00:00 2001 From: Rian Wouters Date: Fri, 3 Jun 2016 12:54:40 +0000 Subject: [PATCH 09/36] Refactor closing data sockets --- lib/FtpConnection.js | 21 +++++++-------------- 1 file changed, 7 insertions(+), 14 deletions(-) diff --git a/lib/FtpConnection.js b/lib/FtpConnection.js index 4f2d8cb..b59df78 100644 --- a/lib/FtpConnection.js +++ b/lib/FtpConnection.js @@ -58,10 +58,9 @@ FtpConnection.prototype._authenticated = function() { return !!this.username; }; -FtpConnection.prototype._closeDataConnections = function() { +FtpConnection.prototype._closeDataConnections = function(destroy) { if (this.dataSocket) { - // TODO: should the second arg be false here? - this._closeSocket(this.dataSocket, true); + this._closeSocket(this.dataSocket, destroy); this.dataSocket = null; } if (this.pasv) { @@ -215,18 +214,11 @@ FtpConnection.prototype._onClose = function(hadError) { // I feel like some of this might be redundant since we probably close some // of these sockets elsewhere, but it is fine to call _closeSocket more than // once. - if (this.dataSocket) { - this._closeSocket(this.dataSocket, hadError); - this.dataSocket = null; - } + this._closeDataConnections(hadError); if (this.socket) { this._closeSocket(this.socket, hadError); this.socket = null; } - if (this.pasv) { - this.pasv.close(); - this.pasv = null; - } // TODO: LOG.DEBUG? this._logIf(LOG.INFO, 'Client connection closed'); }; @@ -659,7 +651,8 @@ FtpConnection.prototype._PORT = function(commandArg, command) { var port; self.dataConfigured = false; - self._closeDataConnections(); + // TODO: should the arg be false here? + self._closeDataConnections(true); if (command === 'PORT') { m = commandArg.match(/^([0-9]{1,3}),([0-9]{1,3}),([0-9]{1,3}),([0-9]{1,3}),([0-9]{1,3}),([0-9]{1,3})$/); @@ -867,8 +860,8 @@ FtpConnection.prototype._command_QUIT = function() { if (err) { self._logIf(LOG.ERROR, "Error writing 'Goodbye' message following QUIT"); } - self._closeSocket(self.socket, true); - self._closeDataConnections(); + // TODO: should the arg be false here? + self._onClose(true); }); }; From 60800afd17c443c5c8d286e18b672e6bf4d52e37 Mon Sep 17 00:00:00 2001 From: Rian Wouters Date: Fri, 3 Jun 2016 13:08:30 +0000 Subject: [PATCH 10/36] Fix typo --- lib/FtpConnection.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/FtpConnection.js b/lib/FtpConnection.js index b59df78..4a4fcd9 100644 --- a/lib/FtpConnection.js +++ b/lib/FtpConnection.js @@ -111,8 +111,8 @@ FtpConnection.prototype._createPassiveServer = function() { } function setupPassiveListener() { - if (self.passv) { - self.passv.emit('ready'); + if (self.pasv) { + self.pasv.emit('ready'); } else { self._logIf(LOG.WARN, 'Passive connection initiated, but no data listener'); } From 0933d4f9d79302a954eeaade930b973341e75126 Mon Sep 17 00:00:00 2001 From: Rian Wouters Date: Fri, 3 Jun 2016 13:18:38 +0000 Subject: [PATCH 11/36] Fix same typo --- lib/FtpConnection.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/FtpConnection.js b/lib/FtpConnection.js index 4a4fcd9..14c22fc 100644 --- a/lib/FtpConnection.js +++ b/lib/FtpConnection.js @@ -145,7 +145,7 @@ FtpConnection.prototype._createPassiveServer = function() { FtpConnection.prototype._whenDataReady = function(callback) { var self = this; - if (self.passv) { + if (self.pasv) { // how many data connections are allowed? // should still be listening since we created a server, right? if (self.dataSocket) { @@ -153,7 +153,7 @@ FtpConnection.prototype._whenDataReady = function(callback) { callback(self.dataSocket); } else { self._logIf(LOG.DEBUG, 'Currently no data connection; expecting client to connect to pasv server shortly...'); - self.passv.once('ready', function() { + self.pasv.once('ready', function() { self._logIf(LOG.DEBUG, '...client has connected now'); callback(self.dataSocket); }); @@ -726,7 +726,7 @@ FtpConnection.prototype._PASV = function(commandArg, command) { self._closeSocket(self.dataSocket, true); } - if (self.passv) { + if (self.pasv) { self._logIf(LOG.DEBUG, 'Telling client that they can connect now'); self._writePASVReady(command); } else { From 9d16ba8e8ffb882590313671ef783e48c798c748 Mon Sep 17 00:00:00 2001 From: Rian Wouters Date: Mon, 6 Jun 2016 12:20:38 +0000 Subject: [PATCH 12/36] Refactor --- lib/FtpConnection.js | 113 ++++++++++++++++++------------------------- 1 file changed, 47 insertions(+), 66 deletions(-) diff --git a/lib/FtpConnection.js b/lib/FtpConnection.js index 14c22fc..ff6c610 100644 --- a/lib/FtpConnection.js +++ b/lib/FtpConnection.js @@ -86,33 +86,30 @@ FtpConnection.prototype._createPassiveServer = function() { self.dataConfigured = false; } else if (!cleartext.authorized) { if (self.server.options.allowUnauthorizedTls) { - self._logIf(LOG.INFO, 'Allowing unauthorized passive connection (allowUnauthorizedTls is on)'); - switchToSecure(); + self._logIf(LOG.INFO, 'Allowed unauthorized secure passive connection started (allowUnauthorizedTls is on)'); + // TODO: Check for existing dataSocket. + setupPassiveListener(cleartext); } else { - self._logIf(LOG.INFO, 'Closing unauthorized passive connection (allowUnauthorizedTls is off)'); + self._logIf(LOG.INFO, 'Closing disallowed unauthorized secure passive connection (allowUnauthorizedTls is off)'); self._closeSocket(self.socket, true); self.dataConfigured = false; } } else { - switchToSecure(); - } - - function switchToSecure() { - self._logIf(LOG.INFO, 'Secure passive connection started'); + self._logIf(LOG.INFO, 'Authorized secure passive connection started'); // TODO: Check for existing dataSocket. - self.dataSocket = cleartext; - setupPassiveListener(); + setupPassiveListener(cleartext); } }); } else { // TODO: Check for existing dataSocket. - self.dataSocket = psocket; - setupPassiveListener(); + setupPassiveListener(psocket); } - function setupPassiveListener() { + function setupPassiveListener(socket) { + self.dataSocket = socket; + if (self.pasv) { - self.pasv.emit('ready'); + self.pasv.emit('ready', socket); } else { self._logIf(LOG.WARN, 'Passive connection initiated, but no data listener'); } @@ -120,8 +117,8 @@ FtpConnection.prototype._createPassiveServer = function() { // Responses are not guaranteed to have an 'end' event // (https://github.com/joyent/node/issues/728), but we want to set // dataSocket to null as soon as possible, so we handle both events. - self.dataSocket.on('close', allOver('close')); - self.dataSocket.on('end', allOver('end')); + socket.on('close', allOver('close')); + socket.on('end', allOver('end')); function allOver(ename) { return function(err) { self._logIf( @@ -132,7 +129,7 @@ FtpConnection.prototype._createPassiveServer = function() { }; } - self.dataSocket.on('error', function(err) { + socket.on('error', function(err) { self._logIf(LOG.ERROR, 'Passive data event: error: ' + err); // TODO: Can we can rely on self.dataSocket having been closed? self.dataSocket = null; @@ -144,61 +141,45 @@ FtpConnection.prototype._createPassiveServer = function() { FtpConnection.prototype._whenDataReady = function(callback) { var self = this; - - if (self.pasv) { - // how many data connections are allowed? - // should still be listening since we created a server, right? - if (self.dataSocket) { - self._logIf(LOG.DEBUG, 'A data connection exists'); - callback(self.dataSocket); - } else { - self._logIf(LOG.DEBUG, 'Currently no data connection; expecting client to connect to pasv server shortly...'); - self.pasv.once('ready', function() { - self._logIf(LOG.DEBUG, '...client has connected now'); - callback(self.dataSocket); - }); - } - } else { - // Do we need to open the data connection? - if (self.dataSocket) { // There really shouldn't be an existing connection - self._logIf(LOG.DEBUG, 'Using existing non-passive dataSocket'); - callback(self.dataSocket); - } else { - self._initiateData(function(sock) { - callback(sock); - }); - } + var socket = self.dataSocket; + if (socket) { + self._logIf(LOG.DEBUG, 'Using existing ' + (self.pasv ? '' : 'non-') + 'passive connection'); + callback(socket); + return; } -}; -FtpConnection.prototype._initiateData = function(callback) { - var self = this; + function setupDataConnection(callback) { - if (self.dataSocket) { - return callback(self.dataSocket); + function allOver(err) { + self.dataSocket = null; + self._logIf( + err ? LOG.ERROR : LOG.DEBUG, + 'Non-passive data connection ended' + (err ? 'due to error: ' + util.inspect(err) : '') + ); + } + net.connect(self.dataPort, self.dataHost || self.socket.remoteAddress) + .on('connect', function() { + self.dataSocket = this; + callback(this); + }) + .on('end', allOver) + .on('close', allOver) + .on('error', function(err) { + self._closeSocket(this, true); + self._logIf(LOG.ERROR, 'Data connection error: ' + util.inspect(err)); + self.dataSocket = null; + self.dataConfigured = false; + }); } - var sock = net.connect(self.dataPort, self.dataHost || self.socket.remoteAddress); - sock.on('connect', function() { - self.dataSocket = sock; - callback(sock); - }); - sock.on('end', allOver); - sock.on('close', allOver); - function allOver(err) { - self.dataSocket = null; - self._logIf( - err ? LOG.ERROR : LOG.DEBUG, - 'Non-passive data connection ended' + (err ? 'due to error: ' + util.inspect(err) : '') - ); + self._logIf(LOG.DEBUG, 'Currently no data connection; setting up ' + (self.pasv ? '' : 'non-') + 'passive connection to client'); + if (self.pasv) { + // how many data connections are allowed? + // should still be listening since we created a server, right? + this.pasv.once('ready', callback); + } else { + setupDataConnection(callback); } - - sock.on('error', function(err) { - self._closeSocket(sock, true); - self._logIf(LOG.ERROR, 'Data connection error: ' + util.inspect(err)); - self.dataSocket = null; - self.dataConfigured = false; - }); }; FtpConnection.prototype._onError = function(err) { From 0e3b0299dcb5e3bc0b058f1c59cd679babce76ab Mon Sep 17 00:00:00 2001 From: Rian Wouters Date: Mon, 6 Jun 2016 13:35:37 +0000 Subject: [PATCH 13/36] Refactor even more --- lib/FtpConnection.js | 117 ++++++++++++++++--------------------------- 1 file changed, 43 insertions(+), 74 deletions(-) diff --git a/lib/FtpConnection.js b/lib/FtpConnection.js index ff6c610..6e054a4 100644 --- a/lib/FtpConnection.js +++ b/lib/FtpConnection.js @@ -84,63 +84,59 @@ FtpConnection.prototype._createPassiveServer = function() { self._logIf(LOG.ERROR, 'Error upgrading passive connection to TLS:' + util.inspect(err)); self._closeSocket(psocket, true); self.dataConfigured = false; - } else if (!cleartext.authorized) { - if (self.server.options.allowUnauthorizedTls) { - self._logIf(LOG.INFO, 'Allowed unauthorized secure passive connection started (allowUnauthorizedTls is on)'); - // TODO: Check for existing dataSocket. - setupPassiveListener(cleartext); - } else { - self._logIf(LOG.INFO, 'Closing disallowed unauthorized secure passive connection (allowUnauthorizedTls is off)'); - self._closeSocket(self.socket, true); - self.dataConfigured = false; - } - } else { - self._logIf(LOG.INFO, 'Authorized secure passive connection started'); + return; + } + + if (cleartext.authorized || self.server.options.allowUnauthorizedTls) { + self._logIf(LOG.INFO, 'Secure passive connection started'); // TODO: Check for existing dataSocket. - setupPassiveListener(cleartext); + self._setupDataSocket(cleartext); + return; } + + self._logIf(LOG.INFO, 'Closing disallowed unauthorized secure passive connection (allowUnauthorizedTls is off)'); + self._closeSocket(self.socket, true); + self.dataConfigured = false; }); } else { // TODO: Check for existing dataSocket. - setupPassiveListener(psocket); + self._setupDataSocket(psocket); } + }); +}; - function setupPassiveListener(socket) { - self.dataSocket = socket; +FtpConnection.prototype._setupDataSocket = function(socket) { + var self = this; - if (self.pasv) { - self.pasv.emit('ready', socket); - } else { - self._logIf(LOG.WARN, 'Passive connection initiated, but no data listener'); - } + function allOver(ename) { + return function(err) { + self._logIf( + (err ? LOG.ERROR : LOG.DEBUG), + 'Passive data event: ' + ename + (err ? ' due to error: ' + util.inspect(err) : '') + ); + self.dataSocket = null; + }; + } - // Responses are not guaranteed to have an 'end' event - // (https://github.com/joyent/node/issues/728), but we want to set - // dataSocket to null as soon as possible, so we handle both events. - socket.on('close', allOver('close')); - socket.on('end', allOver('end')); - function allOver(ename) { - return function(err) { - self._logIf( - (err ? LOG.ERROR : LOG.DEBUG), - 'Passive data event: ' + ename + (err ? ' due to error' : '') - ); - self.dataSocket = null; - }; - } + // Responses are not guaranteed to have an 'end' event + // (https://github.com/joyent/node/issues/728), but we want to set + // dataSocket to null as soon as possible, so we handle both events. + self.dataSocket = socket + .on('close', allOver('close')) + .on('end', allOver('end')) + .on('error', function(err) { + self._logIf(LOG.ERROR, 'Data connection error: ' + err); + self._closeSocket(this, true); + self.dataSocket = null; + self.dataConfigured = false; + }); - socket.on('error', function(err) { - self._logIf(LOG.ERROR, 'Passive data event: error: ' + err); - // TODO: Can we can rely on self.dataSocket having been closed? - self.dataSocket = null; - self.dataConfigured = false; - }); - } - }); + (self.pasv || socket).emit('ready', socket); }; FtpConnection.prototype._whenDataReady = function(callback) { var self = this; + var socket = self.dataSocket; if (socket) { self._logIf(LOG.DEBUG, 'Using existing ' + (self.pasv ? '' : 'non-') + 'passive connection'); @@ -148,38 +144,11 @@ FtpConnection.prototype._whenDataReady = function(callback) { return; } - function setupDataConnection(callback) { - - function allOver(err) { - self.dataSocket = null; - self._logIf( - err ? LOG.ERROR : LOG.DEBUG, - 'Non-passive data connection ended' + (err ? 'due to error: ' + util.inspect(err) : '') - ); - } - net.connect(self.dataPort, self.dataHost || self.socket.remoteAddress) - .on('connect', function() { - self.dataSocket = this; - callback(this); - }) - .on('end', allOver) - .on('close', allOver) - .on('error', function(err) { - self._closeSocket(this, true); - self._logIf(LOG.ERROR, 'Data connection error: ' + util.inspect(err)); - self.dataSocket = null; - self.dataConfigured = false; - }); - } - self._logIf(LOG.DEBUG, 'Currently no data connection; setting up ' + (self.pasv ? '' : 'non-') + 'passive connection to client'); - if (self.pasv) { - // how many data connections are allowed? - // should still be listening since we created a server, right? - this.pasv.once('ready', callback); - } else { - setupDataConnection(callback); - } + + (self.pasv || net.connect(self.dataPort, self.dataHost || self.socket.remoteAddress, function() { + self._setupDataSocket(this); + })).once('ready', callback); }; FtpConnection.prototype._onError = function(err) { From eed0937b182fb00d3a186a76286fe55f35a68c52 Mon Sep 17 00:00:00 2001 From: Rian Wouters Date: Thu, 9 Jun 2016 07:56:00 +0000 Subject: [PATCH 14/36] Improve logging --- lib/FtpConnection.js | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/lib/FtpConnection.js b/lib/FtpConnection.js index 6e054a4..16ea269 100644 --- a/lib/FtpConnection.js +++ b/lib/FtpConnection.js @@ -112,7 +112,7 @@ FtpConnection.prototype._setupDataSocket = function(socket) { return function(err) { self._logIf( (err ? LOG.ERROR : LOG.DEBUG), - 'Passive data event: ' + ename + (err ? ' due to error: ' + util.inspect(err) : '') + 'Data event: ' + ename + (err ? ' due to error: ' + util.inspect(err) : '') ); self.dataSocket = null; }; @@ -677,10 +677,8 @@ FtpConnection.prototype._PASV = function(commandArg, command) { } if (self.pasv) { - self._logIf(LOG.DEBUG, 'Telling client that they can connect now'); self._writePASVReady(command); } else { - self._logIf(LOG.DEBUG, 'Setting up listener for passive connections'); self._setupNewPASV(commandArg, command); } @@ -689,6 +687,8 @@ FtpConnection.prototype._PASV = function(commandArg, command) { FtpConnection.prototype._writePASVReady = function(command) { var self = this; + + self._logIf(LOG.DEBUG, 'Telling client that it can connect now'); var a = self.pasv.address(); var host = self.server.host; @@ -709,6 +709,8 @@ FtpConnection.prototype._writePASVReady = function(command) { FtpConnection.prototype._setupNewPASV = function(commandArg, command) { var self = this; + self._logIf(LOG.DEBUG, 'Setting up listener for passive connections'); + var firstPort = this.server.options.pasvPortRangeStart; var lastPort = this.server.options.pasvPortRangeEnd; @@ -733,11 +735,11 @@ FtpConnection.prototype._setupNewPASV = function(commandArg, command) { // Once we're successfully listening, tell the client .on('listening', function() { + self._logIf(LOG.DEBUG, 'Passive data connection listening on port ' + this.address().port); this .removeAllListeners('error') .on('error', normalErrorHandler); self.pasv = this; - self._logIf(LOG.DEBUG, 'Passive data connection listening on port ' + this.address().port); self._writePASVReady(command); }) From 9f6a8be83c53f3753ab5ec3fe6ff12a9ee5b775d Mon Sep 17 00:00:00 2001 From: Rian Wouters Date: Thu, 9 Jun 2016 08:37:35 +0000 Subject: [PATCH 15/36] Sanitize data ready callbacks --- lib/FtpConnection.js | 44 ++++++++++++++++++++------------------------ 1 file changed, 20 insertions(+), 24 deletions(-) diff --git a/lib/FtpConnection.js b/lib/FtpConnection.js index 16ea269..03df816 100644 --- a/lib/FtpConnection.js +++ b/lib/FtpConnection.js @@ -687,7 +687,7 @@ FtpConnection.prototype._PASV = function(commandArg, command) { FtpConnection.prototype._writePASVReady = function(command) { var self = this; - + self._logIf(LOG.DEBUG, 'Telling client that it can connect now'); var a = self.pasv.address(); @@ -862,7 +862,7 @@ FtpConnection.prototype._RETR_usingCreateReadStream = function(commandArg, filen } } else { afterOk(function() { - self._whenDataReady(function(pasvconn) { + self._whenDataReady(function(socket) { var readLength = 0; var now = new Date(); var rs = self.fs.createReadStream(null, {fd: fd}); @@ -902,7 +902,7 @@ FtpConnection.prototype._RETR_usingCreateReadStream = function(commandArg, filen self.respond('226 Closing data connection, sent ' + readLength + ' bytes'); }); - rs.pipe(pasvconn); + rs.pipe(socket); rs.resume(); }); }); @@ -946,11 +946,9 @@ FtpConnection.prototype._RETR_usingReadFile = function(commandArg, filename) { } } else { afterOk(function() { - self._whenDataReady(function(pasvconn) { - contents = {filename: filename, data: contents}; - self.emit('file:retr:contents', contents); - contents = contents.data; - pasvconn.write(contents); + self._whenDataReady(function(socket) { + self.emit('file:retr:contents', {filename: filename, data: contents}); + socket.write(contents); var contentLength = contents.length; self.respond('226 Closing data connection, sent ' + contentLength + ' bytes'); self.emit('file:retr', 'close', { @@ -964,7 +962,7 @@ FtpConnection.prototype._RETR_usingReadFile = function(commandArg, filename) { duration: new Date() - startTime, errorState: false, }); - self._closeSocket(pasvconn); + self._closeSocket(socket); }); }); } @@ -1059,7 +1057,13 @@ FtpConnection.prototype._STOR_usingCreateWriteStream = function(filename, initia }); } - self._whenDataReady(handleUpload); + self._whenDataReady(function(socket) { + socket.on('error', function(err) { + notErr = false; // TODO RWO should we not emit an error here and close the socket? + self._logIf(LOG.ERROR, 'Data connection error: ' + util.inspect(err)); + }); + socket.pipe(storeStream); + }); storeStream.on('open', function() { self._logIf(LOG.DEBUG, 'File opened/created: ' + filename); @@ -1113,13 +1117,6 @@ FtpConnection.prototype._STOR_usingCreateWriteStream = function(filename, initia } }); - function handleUpload(dataSocket) { - dataSocket.pipe(storeStream); - dataSocket.on('error', function(err) { - notErr = false; - self._logIf(LOG.ERROR, 'Data connection error: ' + util.inspect(err)); - }); - } }; FtpConnection.prototype._STOR_usingWriteFile = function(filename, flag) { @@ -1137,14 +1134,13 @@ FtpConnection.prototype._STOR_usingWriteFile = function(filename, flag) { }); self.respond('150 Ok to send data', function() { - self._whenDataReady(handleUpload); + self._whenDataReady(function(socket) { + socket.on('data', dataHandler); + socket.once('close', closeHandler); + socket.once('error', errorHandler); + }); }); - function handleUpload() { - self.dataSocket.on('data', dataHandler); - self.dataSocket.once('close', closeHandler); - self.dataSocket.once('error', errorHandler); - } function dataHandler(buf) { if (self.server.options.uploadMaxSlurpSize != null && @@ -1222,7 +1218,7 @@ FtpConnection.prototype._STOR_usingWriteFile = function(filename, flag) { } function errorHandler() { - erroredOut = true; + erroredOut = true; // TODO RWO: should we not log the error and emit error and close the connection? } }; From d23cb7c3c30ec0662cfed418ae3f4c1552697c6f Mon Sep 17 00:00:00 2001 From: Rian Wouters Date: Thu, 9 Jun 2016 12:17:29 +0000 Subject: [PATCH 16/36] Refactor --- lib/helpers/withCwd.js | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/lib/helpers/withCwd.js b/lib/helpers/withCwd.js index 3eed41c..1942198 100644 --- a/lib/helpers/withCwd.js +++ b/lib/helpers/withCwd.js @@ -1,14 +1,11 @@ -var pathModule = require('path'); +var path = require('path'); -function withCwd(cwd, path) { - var firstChar = (path || '').charAt(0); - cwd = cwd || pathModule.sep; - path = path || ''; - if (firstChar === '/' || firstChar === pathModule.sep) { - cwd = pathModule.sep; +function withCwd(cwd, p) { + p = p || ''; + if (!cwd || p.charAt(0) in ['/', path.sep]) { + cwd = path.sep; } - path = pathModule.join(pathModule.sep, cwd, path); - return path; + return path.join(path.sep, cwd, p); } module.exports = withCwd; From 43cb39c94e595902ace5b83d0d48cb15326bb674 Mon Sep 17 00:00:00 2001 From: Rian Wouters Date: Thu, 9 Jun 2016 12:17:49 +0000 Subject: [PATCH 17/36] Nullify socket --- lib/FtpConnection.js | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/FtpConnection.js b/lib/FtpConnection.js index 03df816..df560c3 100644 --- a/lib/FtpConnection.js +++ b/lib/FtpConnection.js @@ -154,6 +154,7 @@ FtpConnection.prototype._whenDataReady = function(callback) { FtpConnection.prototype._onError = function(err) { this._logIf(LOG.ERROR, 'Client connection error: ' + util.inspect(err)); this._closeSocket(this.socket, true); + this.socket = null; }; FtpConnection.prototype._onEnd = function() { From 9ceaa2f646019ddbd3cf553a6c9144816cc33aa3 Mon Sep 17 00:00:00 2001 From: Rian Wouters Date: Fri, 10 Jun 2016 11:34:04 +0000 Subject: [PATCH 18/36] Improve logging --- lib/FtpConnection.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/lib/FtpConnection.js b/lib/FtpConnection.js index df560c3..de6ce5a 100644 --- a/lib/FtpConnection.js +++ b/lib/FtpConnection.js @@ -75,7 +75,7 @@ FtpConnection.prototype._createPassiveServer = function() { return net.createServer(function(psocket) { // This is simply a connection listener. // TODO: Should we keep track of *all* connections, or enforce just one? - self._logIf(LOG.INFO, 'Passive data event: connect'); + self._logIf(LOG.INFO, 'Passive data event: connect on port ' + this.address().port); if (self.secure) { self._logIf(LOG.INFO, 'Upgrading passive connection to TLS'); @@ -112,7 +112,7 @@ FtpConnection.prototype._setupDataSocket = function(socket) { return function(err) { self._logIf( (err ? LOG.ERROR : LOG.DEBUG), - 'Data event: ' + ename + (err ? ' due to error: ' + util.inspect(err) : '') + 'Data event: ' + ename + (err ? ' due to error: ' + util.inspect(err) : '') + ' on port ' + this.address().port ); self.dataSocket = null; }; @@ -1318,6 +1318,7 @@ FtpConnection.prototype._closeSocket = function(socket, shouldDestroy) { if (shouldDestroy || this.server.options.destroySockets) { // Don't call destroy() more than once. if (!socket.destroyed) { + this._logIf(LOG.DEBUG, 'Closing socket on port ' + socket.address().port); socket.destroy(); } } else { From e2f7c6bee05271b68c4607ffb4117acda08419b1 Mon Sep 17 00:00:00 2001 From: Rian Wouters Date: Fri, 10 Jun 2016 12:55:14 +0000 Subject: [PATCH 19/36] use dataSocket again --- lib/FtpConnection.js | 60 +++++++++++++++++++++++--------------------- 1 file changed, 32 insertions(+), 28 deletions(-) diff --git a/lib/FtpConnection.js b/lib/FtpConnection.js index de6ce5a..7332de4 100644 --- a/lib/FtpConnection.js +++ b/lib/FtpConnection.js @@ -72,35 +72,35 @@ FtpConnection.prototype._closeDataConnections = function(destroy) { FtpConnection.prototype._createPassiveServer = function() { var self = this; - return net.createServer(function(psocket) { + return net.createServer(function(socket) { // This is simply a connection listener. // TODO: Should we keep track of *all* connections, or enforce just one? self._logIf(LOG.INFO, 'Passive data event: connect on port ' + this.address().port); if (self.secure) { self._logIf(LOG.INFO, 'Upgrading passive connection to TLS'); - starttls.starttlsServer(psocket, self.server.options.tlsOptions, function(err, cleartext) { + starttls.starttlsServer(socket, self.server.options.tlsOptions, function(err, secureSocket) { if (err) { self._logIf(LOG.ERROR, 'Error upgrading passive connection to TLS:' + util.inspect(err)); - self._closeSocket(psocket, true); + self._closeSocket(socket, true); self.dataConfigured = false; return; } - if (cleartext.authorized || self.server.options.allowUnauthorizedTls) { + if (secureSocket.authorized || self.server.options.allowUnauthorizedTls) { self._logIf(LOG.INFO, 'Secure passive connection started'); // TODO: Check for existing dataSocket. - self._setupDataSocket(cleartext); + self._setupDataSocket(secureSocket); return; } self._logIf(LOG.INFO, 'Closing disallowed unauthorized secure passive connection (allowUnauthorizedTls is off)'); - self._closeSocket(self.socket, true); + self._closeSocket(socket, true); self.dataConfigured = false; }); } else { // TODO: Check for existing dataSocket. - self._setupDataSocket(psocket); + self._setupDataSocket(socket); } }); }; @@ -110,10 +110,11 @@ FtpConnection.prototype._setupDataSocket = function(socket) { function allOver(ename) { return function(err) { - self._logIf( - (err ? LOG.ERROR : LOG.DEBUG), - 'Data event: ' + ename + (err ? ' due to error: ' + util.inspect(err) : '') + ' on port ' + this.address().port - ); + if (err) { + self._logIf(LOG.ERROR, 'Data event: ' + ename + ' due to error: ' + util.inspect(err) + ' on port ' + this.address().port); + } else { + self._logIf(LOG.DEBUG, 'Data event: ' + ename + ' on port ' + this.address().port); + } self.dataSocket = null; }; } @@ -126,8 +127,10 @@ FtpConnection.prototype._setupDataSocket = function(socket) { .on('end', allOver('end')) .on('error', function(err) { self._logIf(LOG.ERROR, 'Data connection error: ' + err); - self._closeSocket(this, true); - self.dataSocket = null; + if (self.dataSocket) { + self._closeSocket(self.dataSocket, true); + self.dataSocket = null; + } self.dataConfigured = false; }); @@ -253,31 +256,25 @@ FtpConnection.prototype._command_AUTH = function(commandArg) { self.respond('234 Honored', function() { self._logIf(LOG.INFO, 'Establishing secure connection...'); - starttls.starttlsServer(self.socket, self.server.options.tlsOptions, function(err, cleartext) { + starttls.starttlsServer(self.socket, self.server.options.tlsOptions, function(err, secureSocket) { if (err) { self._logIf(LOG.ERROR, 'Error upgrading connection to TLS: ' + util.inspect(err)); self._closeSocket(self.socket, true); - } else if (!cleartext.authorized) { - self._logIf(LOG.INFO, 'Secure socket not authorized: ' + util.inspect(cleartext.authorizationError)); - if (self.server.options.allowUnauthorizedTls) { - self._logIf(LOG.INFO, 'Allowing unauthorized connection (allowUnauthorizedTls is on)'); - switchToSecure(); - } else { - self._logIf(LOG.INFO, 'Closing unauthorized connection (allowUnauthorizedTls is off)'); - self._closeSocket(self.socket, true); - } - } else { - switchToSecure(); + return; } - function switchToSecure() { + if (secureSocket.authorized || self.server.options.allowUnauthorizedTls) { self._logIf(LOG.INFO, 'Secure connection started'); - self.socket = cleartext; + self.socket = secureSocket; self.socket.on('data', function(data) { self._onData(data); }); self.secure = true; + return; } + + self._logIf(LOG.INFO, 'Closing unauthorized connection (allowUnauthorizedTls is off)'); + self._closeSocket(self.socket, true); }); }); }; @@ -675,6 +672,7 @@ FtpConnection.prototype._PASV = function(commandArg, command) { // not sure whether the spec limits to 1 data connection at a time ... if (self.dataSocket) { self._closeSocket(self.dataSocket, true); + self.dataSocket = null; } if (self.pasv) { @@ -963,7 +961,10 @@ FtpConnection.prototype._RETR_usingReadFile = function(commandArg, filename) { duration: new Date() - startTime, errorState: false, }); - self._closeSocket(socket); + if (self.dataSocket) { + self._closeSocket(self.dataSocket); + self.dataSocket = null; + } }); }); } @@ -1095,6 +1096,7 @@ FtpConnection.prototype._STOR_usingCreateWriteStream = function(filename, initia notErr = false; if (self.dataSocket) { self._closeSocket(self.dataSocket, true); + self.dataSocket = null; } self.respond('426 Connection closed; transfer aborted'); }); @@ -1115,6 +1117,7 @@ FtpConnection.prototype._STOR_usingCreateWriteStream = function(filename, initia notErr ? self.respond('226 Closing data connection') : true; if (self.dataSocket) { self._closeSocket(self.dataSocket); + self.dataSocket = null; } }); @@ -1153,6 +1156,7 @@ FtpConnection.prototype._STOR_usingWriteFile = function(filename, flag) { if (!self.fs.createWriteStream) { if (self.dataSocket) { self._closeSocket(self.dataSocket, true); + self.dataSocket = null; } self.respond('552 Requested file action aborted; file too big'); return; From 74737053eb659019efd8599fe416f59ee3c0b0f7 Mon Sep 17 00:00:00 2001 From: Rian Wouters Date: Fri, 10 Jun 2016 16:20:49 +0000 Subject: [PATCH 20/36] Log port on data events --- lib/FtpConnection.js | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/lib/FtpConnection.js b/lib/FtpConnection.js index 7332de4..a432f80 100644 --- a/lib/FtpConnection.js +++ b/lib/FtpConnection.js @@ -108,12 +108,13 @@ FtpConnection.prototype._createPassiveServer = function() { FtpConnection.prototype._setupDataSocket = function(socket) { var self = this; - function allOver(ename) { + function allOver(socket, ename) { + var port = socket.address().port; return function(err) { if (err) { - self._logIf(LOG.ERROR, 'Data event: ' + ename + ' due to error: ' + util.inspect(err) + ' on port ' + this.address().port); + self._logIf(LOG.ERROR, 'Data event: ' + ename + ' due to error: ' + util.inspect(err) + ' on port ' + port); } else { - self._logIf(LOG.DEBUG, 'Data event: ' + ename + ' on port ' + this.address().port); + self._logIf(LOG.DEBUG, 'Data event: ' + ename + ' on port ' + port); } self.dataSocket = null; }; @@ -123,8 +124,8 @@ FtpConnection.prototype._setupDataSocket = function(socket) { // (https://github.com/joyent/node/issues/728), but we want to set // dataSocket to null as soon as possible, so we handle both events. self.dataSocket = socket - .on('close', allOver('close')) - .on('end', allOver('end')) + .on('close', allOver(socket, 'close')) + .on('end', allOver(socket, 'end')) .on('error', function(err) { self._logIf(LOG.ERROR, 'Data connection error: ' + err); if (self.dataSocket) { From 5e191151cdc27755d4dcb634d5ee0bde91bd1593 Mon Sep 17 00:00:00 2001 From: Rian Wouters Date: Tue, 14 Jun 2016 10:37:05 +0000 Subject: [PATCH 21/36] Refactor and code cleanup --- lib/FtpConnection.js | 213 ++++++++++++++++++++----------------------- lib/FtpServer.js | 20 ---- 2 files changed, 99 insertions(+), 134 deletions(-) diff --git a/lib/FtpConnection.js b/lib/FtpConnection.js index a432f80..cd6f9fc 100644 --- a/lib/FtpConnection.js +++ b/lib/FtpConnection.js @@ -3,7 +3,7 @@ var net = require('net'); var util = require('util'); var events = require('events'); -var pathModule = require('path'); +var path = require('path'); var fsModule = require('fs'); var StatMode = require('stat-mode'); var dateformat = require('dateformat'); @@ -22,24 +22,49 @@ var EventEmitter = events.EventEmitter; // Use LOG for brevity. var LOG = Constants.LOG_LEVELS; var DOES_NOT_REQUIRE_AUTH = Constants.DOES_NOT_REQUIRE_AUTH; + var REQUIRES_CONFIGURED_DATA = Constants.REQUIRES_CONFIGURED_DATA; function FtpConnection(properties) { - EventEmitter.call(this); var self = this; + + EventEmitter.call(this); + Object.keys(properties).forEach(function(key) { self[key] = properties[key]; }); + + self.socket.setTimeout(0); + self.socket.setNoDelay(); + + self.socket.on('data', self._onData.bind(self)); + self.socket.on('end', endHandler); + self.socket.on('error', errorHandler); + self.socket.on('close', self._onClose.bind(self)); + + self.respond('220 FTP server (nodeftpd) ready'); + self._logIf(LOG.INFO, 'Accepted a new client connection'); + + function endHandler() { + self._logIf(LOG.DEBUG, 'Client connection ended'); + } + + function errorHandler(err) { + self._logIf(LOG.ERROR, 'Client connection error: ' + util.inspect(err)); + self._closeCommandConnection(true); + } + } + util.inherits(FtpConnection, EventEmitter); // TODO: rename this to writeLine? FtpConnection.prototype.respond = function(message, callback) { - return this._writeText(this.socket, message + '\r\n', callback); + this._writeText(this.socket, message + '\r\n', callback); }; FtpConnection.prototype._logIf = function(verbosity, message) { - return this.server._logIf(verbosity, message, this); + this.server._logIf(verbosity, message, this); }; // We don't want to use setEncoding because it screws up TLS, but we @@ -54,10 +79,6 @@ FtpConnection.prototype._writeText = function(socket, data, callback) { return socket.write(data, 'utf8', callback); }; -FtpConnection.prototype._authenticated = function() { - return !!this.username; -}; - FtpConnection.prototype._closeDataConnections = function(destroy) { if (this.dataSocket) { this._closeSocket(this.dataSocket, destroy); @@ -67,6 +88,14 @@ FtpConnection.prototype._closeDataConnections = function(destroy) { this.pasv.close(); this.pasv = null; } + this.dataConfigured = false; +}; + +FtpConnection.prototype._closeCommandConnection = function(destroy) { + if (this.socket) { + this._closeSocket(this.socket, destroy); + this.socket = null; + } }; FtpConnection.prototype._createPassiveServer = function() { @@ -155,25 +184,12 @@ FtpConnection.prototype._whenDataReady = function(callback) { })).once('ready', callback); }; -FtpConnection.prototype._onError = function(err) { - this._logIf(LOG.ERROR, 'Client connection error: ' + util.inspect(err)); - this._closeSocket(this.socket, true); - this.socket = null; -}; - -FtpConnection.prototype._onEnd = function() { - this._logIf(LOG.DEBUG, 'Client connection ended'); -}; - FtpConnection.prototype._onClose = function(hadError) { // I feel like some of this might be redundant since we probably close some // of these sockets elsewhere, but it is fine to call _closeSocket more than // once. this._closeDataConnections(hadError); - if (this.socket) { - this._closeSocket(this.socket, hadError); - this.socket = null; - } + this._closeCommandConnection(hadError); // TODO: LOG.DEBUG? this._logIf(LOG.INFO, 'Client connection closed'); }; @@ -215,7 +231,7 @@ FtpConnection.prototype._onData = function(data) { // be permitted over a secure connection. See RFC4217 regarding error code. if (!self.secure && self.server.options.tlsOnly) { self.respond('522 Protection level not sufficient; send AUTH TLS'); - } else if (self._authenticated()) { + } else if (self.username) { checkData(); } else { self.respond('530 Not logged in.'); @@ -239,13 +255,11 @@ FtpConnection.prototype._onData = function(data) { // Specify the user's account (superfluous) FtpConnection.prototype._command_ACCT = function() { this.respond('202 Command not implemented, superfluous at this site.'); - return this; }; // Allocate storage space (superfluous) FtpConnection.prototype._command_ALLO = function() { this.respond('202 Command not implemented, superfluous at this site.'); - return this; }; FtpConnection.prototype._command_AUTH = function(commandArg) { @@ -282,17 +296,16 @@ FtpConnection.prototype._command_AUTH = function(commandArg) { // Change working directory to parent directory FtpConnection.prototype._command_CDUP = function() { - var pathServer = pathModule.dirname(this.cwd); + var pathServer = path.dirname(this.cwd); var pathEscaped = pathEscape(pathServer); this.cwd = pathServer; this.respond('250 Directory changed to "' + pathEscaped + '"'); - return this; }; // Change working directory FtpConnection.prototype._command_CWD = function(pathRequest) { var pathServer = withCwd(this.cwd, pathRequest); - var pathFs = pathModule.join(this.root, pathServer); + var pathFs = path.join(this.root, pathServer); var pathEscaped = pathEscape(pathServer); this.fs.stat(pathFs, function(err, stats) { if (err) { @@ -306,14 +319,13 @@ FtpConnection.prototype._command_CWD = function(pathRequest) { this.respond('250 CWD successful. "' + pathEscaped + '" is current directory'); } }.bind(this)); - return this; }; FtpConnection.prototype._command_DELE = function(commandArg) { var self = this; var filename = withCwd(self.cwd, commandArg); - self.fs.unlink(pathModule.join(self.root, filename), function(err) { + self.fs.unlink(path.join(self.root, filename), function(err) { if (err) { self._logIf(LOG.ERROR, 'Error deleting file: ' + filename + ', ' + err); // write error to socket @@ -352,16 +364,16 @@ FtpConnection.prototype._command_OPTS = function(commandArg) { // Print the file modification time FtpConnection.prototype._command_MDTM = function(file) { + var self = this; file = withCwd(this.cwd, file); - file = pathModule.join(this.root, file); + file = path.join(this.root, file); this.fs.stat(file, function(err, stats) { if (err) { - this.respond('550 File unavailable'); + self.respond('550 File unavailable'); } else { - this.respond('213 ' + dateformat(stats.mtime, 'yyyymmddhhMMss')); + self.respond('213 ' + dateformat(stats.mtime, 'yyyymmddhhMMss')); } - }.bind(this)); - return this; + }); }; FtpConnection.prototype._command_LIST = function(commandArg) { @@ -400,7 +412,7 @@ FtpConnection.prototype._LIST = function(commandArg, detailed, cmd) { var dir = withCwd(self.cwd, dirname); glob.setMaxStatsAtOnce(self.server.options.maxStatsAtOnce); - glob.glob(pathModule.join(self.root, dir), self.fs, function(err, files) { + glob.glob(path.join(self.root, dir), self.fs, function(err, files) { if (err) { self._logIf(LOG.ERROR, 'Error sending file list, reading directory: ' + err); self.respond('550 Not a directory'); @@ -500,29 +512,26 @@ FtpConnection.prototype._listFiles = function(fileInfos, detailed, cmd) { }; m = '226 Transfer OK'; var END_MSGS = { - LIST: m, NLST: m, STAT: '213 End of status', + LIST: m, NLST: m, STAT: '213 End of status', ERROR: '550 Error listing files', }; self.respond(BEGIN_MSGS[cmd], function() { if (cmd === 'STAT') { - whenReady(self.socket); + writeFileList(self.socket); } else { - self._whenDataReady(whenReady); + self._whenDataReady(writeFileList); } - function whenReady(listconn) { + function writeFileList(socket) { if (fileInfos.length === 0) { return success(); } function success(err) { - if (err) { - self.respond('550 Error listing files'); - } else { - self.respond(END_MSGS[cmd]); - } + self.respond(END_MSGS[err && 'ERROR' || cmd]); + if (cmd !== 'STAT') { - self._closeSocket(listconn); + self._closeSocket(socket); } } @@ -536,7 +545,6 @@ FtpConnection.prototype._listFiles = function(fileInfos, detailed, cmd) { if (!detailed) { file = fileInfo; - line += file.name + '\r\n'; } else { file = fileInfo.file; var s = file.stats; @@ -550,11 +558,10 @@ FtpConnection.prototype._listFiles = function(fileInfos, detailed, cmd) { line += leftPad(s.size.toString(), 12) + ' '; var d = new Date(s.mtime); line += leftPad(dateformat(d, 'mmm dd HH:MM'), 12) + ' '; - line += file.name; - line += '\r\n'; } + line += file.name + '\r\n'; self._writeText( - listconn, + socket, line, (i === fileInfos.length - 1 ? success : undefined) ); @@ -567,7 +574,7 @@ FtpConnection.prototype._listFiles = function(fileInfos, detailed, cmd) { FtpConnection.prototype._command_MKD = function(pathRequest) { var pathServer = withCwd(this.cwd, pathRequest); var pathEscaped = pathEscape(pathServer); - var pathFs = pathModule.join(this.root, pathServer); + var pathFs = path.join(this.root, pathServer); this.fs.mkdir(pathFs, 0755, function(err) { if (err) { this._logIf(LOG.ERROR, 'MKD ' + pathRequest + ': ' + err); @@ -576,13 +583,11 @@ FtpConnection.prototype._command_MKD = function(pathRequest) { this.respond('257 "' + pathEscaped + '" directory created'); } }.bind(this)); - return this; }; // Perform a no-op (used to keep-alive connection) FtpConnection.prototype._command_NOOP = function() { this.respond('200 OK'); - return this; }; FtpConnection.prototype._command_PORT = function(x, y) { @@ -599,7 +604,6 @@ FtpConnection.prototype._PORT = function(commandArg, command) { var host; var port; - self.dataConfigured = false; // TODO: should the arg be false here? self._closeDataConnections(true); @@ -676,18 +680,14 @@ FtpConnection.prototype._PASV = function(commandArg, command) { self.dataSocket = null; } - if (self.pasv) { - self._writePASVReady(command); - } else { - self._setupNewPASV(commandArg, command); - } - - self.dataConfigured = true; + self._setupPASV(commandArg, command); }; FtpConnection.prototype._writePASVReady = function(command) { var self = this; + self.dataConfigured = true; + self._logIf(LOG.DEBUG, 'Telling client that it can connect now'); var a = self.pasv.address(); @@ -706,9 +706,14 @@ FtpConnection.prototype._writePASVReady = function(command) { // (i) It works // (ii) We get an error that's not just EADDRINUSE // (iii) We run out of ports to try. -FtpConnection.prototype._setupNewPASV = function(commandArg, command) { +FtpConnection.prototype._setupPASV = function(commandArg, command) { var self = this; + if (self.pasv) { + self._writePASVReady(command); + return; + } + self._logIf(LOG.DEBUG, 'Setting up listener for passive connections'); var firstPort = this.server.options.pasvPortRangeStart; @@ -752,56 +757,39 @@ FtpConnection.prototype._setupNewPASV = function(commandArg, command) { }; FtpConnection.prototype._command_PBSZ = function(commandArg) { - var self = this; - - if (!self.server.options.tlsOptions) { - return self.respond('202 Not supported'); + if (this.secure) { + this.pbszReceived = true; } - // Protection Buffer Size (RFC 2228) - if (!self.secure) { - self.respond('503 Secure connection not established'); - } else if (parseInt(commandArg, 10) !== 0) { + this.respond( + !this.server.options.tlsOptions ? '202 Not supported' : + !this.secure ? '503 Secure connection not established' /* Protection Buffer Size (RFC 2228) */ : + parseInt(commandArg, 10) !== 0 ? '200 buffer too big, PBSZ=0' : // RFC 2228 specifies that a 200 reply must be sent specifying a more // satisfactory PBSZ size (0 in our case, since we're using TLS). // Doubt that this will do any good if the client was already confused // enough to send a non-zero value, but ok... - self.pbszReceived = true; - self.respond('200 buffer too big, PBSZ=0'); - } else { - self.pbszReceived = true; - self.respond('200 OK'); - } + '200 OK' + ); }; FtpConnection.prototype._command_PROT = function(commandArg) { - var self = this; - - if (!self.server.options.tlsOptions) { - return self.respond('202 Not supported'); - } - - if (!self.pbszReceived) { - self.respond('503 No PBSZ command received'); - } else if (commandArg === 'S' || commandArg === 'E' || commandArg === 'C') { - self.respond('536 Not supported'); - } else if (commandArg === 'P') { - self.respond('200 OK'); - } else { - // Don't even recognize this one... - self.respond('504 Not recognized'); - } + this.respond( + !this.server.options.tlsOptions ? '202 Not supported' : + !this.pbszReceived ? '503 No PBSZ command received' : + (commandArg === 'S' || commandArg === 'E' || commandArg === 'C') ? '536 Not supported' : + commandArg !== 'P' ? '504 Not recognized' /* Don't even recognize this one... */ : + '200 OK' + ); }; // Print the current working directory. FtpConnection.prototype._command_PWD = function(commandArg) { - var pathEscaped = pathEscape(this.cwd); - if (commandArg === '') { - this.respond('257 "' + pathEscaped + '" is current directory'); - } else { - this.respond('501 Syntax error in parameters or arguments.'); - } - return this; + this.respond( + commandArg === '' + ? '257 "' + pathEscape(this.cwd) + '" is current directory' + : '501 Syntax error in parameters or arguments.' + ); }; FtpConnection.prototype._command_QUIT = function() { @@ -818,7 +806,7 @@ FtpConnection.prototype._command_QUIT = function() { }; FtpConnection.prototype._command_RETR = function(commandArg) { - var filename = pathModule.join(this.root, withCwd(this.cwd, commandArg)); + var filename = path.join(this.root, withCwd(this.cwd, commandArg)); if (this.server.options.useReadFile) { this._RETR_usingReadFile(commandArg, filename); @@ -975,7 +963,7 @@ FtpConnection.prototype._RETR_usingReadFile = function(commandArg, filename) { // Remove a directory FtpConnection.prototype._command_RMD = function(pathRequest) { var pathServer = withCwd(this.cwd, pathRequest); - var pathFs = pathModule.join(this.root, pathServer); + var pathFs = path.join(this.root, pathServer); this.fs.rmdir(pathFs, function(err) { if (err) { this._logIf(LOG.ERROR, 'RMD ' + pathRequest + ': ' + err); @@ -984,7 +972,6 @@ FtpConnection.prototype._command_RMD = function(pathRequest) { this.respond('250 "' + pathServer + '" directory removed'); } }.bind(this)); - return this; }; FtpConnection.prototype._command_RNFR = function(commandArg) { @@ -997,7 +984,7 @@ FtpConnection.prototype._command_RNFR = function(commandArg) { FtpConnection.prototype._command_RNTO = function(commandArg) { var self = this; var fileto = withCwd(self.cwd, commandArg); - self.fs.rename(pathModule.join(self.root, self.filefrom), pathModule.join(self.root, fileto), function(err) { + self.fs.rename(path.join(self.root, self.filefrom), path.join(self.root, fileto), function(err) { if (err) { self._logIf(LOG.ERROR, 'Error renaming file from ' + self.filefrom + ' to ' + fileto); self.respond('550 Rename failed' + (err.code === 'ENOENT' ? '; file does not exist' : '')); @@ -1011,7 +998,7 @@ FtpConnection.prototype._command_SIZE = function(commandArg) { var self = this; var filename = withCwd(self.cwd, commandArg); - self.fs.stat(pathModule.join(self.root, filename), function(err, s) { + self.fs.stat(path.join(self.root, filename), function(err, s) { if (err) { self._logIf(LOG.ERROR, "Error getting size of file '" + filename + "' "); self.respond('450 Failed to get size of file'); @@ -1022,11 +1009,11 @@ FtpConnection.prototype._command_SIZE = function(commandArg) { }; FtpConnection.prototype._command_TYPE = function(commandArg) { - if (commandArg === 'I' || commandArg === 'A') { - this.respond('200 OK'); - } else { - this.respond('202 Not supported'); - } + this.respond( + commandArg === 'I' || commandArg === 'A' + ? '200 OK' + : '202 Not supported' + ); }; FtpConnection.prototype._command_SYST = function() { @@ -1048,7 +1035,7 @@ FtpConnection.prototype._STOR_usingCreateWriteStream = function(filename, initia var self = this; var wStreamFlags = {flags: flag || 'w', mode: 0644}; - var storeStream = self.fs.createWriteStream(pathModule.join(self.root, filename), wStreamFlags); + var storeStream = self.fs.createWriteStream(path.join(self.root, filename), wStreamFlags); var notErr = true; // Adding for event metadata for file upload (STOR) var startTime = new Date(); @@ -1062,7 +1049,7 @@ FtpConnection.prototype._STOR_usingCreateWriteStream = function(filename, initia self._whenDataReady(function(socket) { socket.on('error', function(err) { - notErr = false; // TODO RWO should we not emit an error here and close the socket? + notErr = false; self._logIf(LOG.ERROR, 'Data connection error: ' + util.inspect(err)); }); socket.pipe(storeStream); @@ -1194,7 +1181,7 @@ FtpConnection.prototype._STOR_usingWriteFile = function(filename, flag) { var wOptions = {flag: flag || 'w', mode: 0644}; var contents = {filename: filename, data: slurpBuf.slice(0, totalBytes)}; self.emit('file:stor:contents', contents); - self.fs.writeFile(pathModule.join(self.root, filename), contents.data, wOptions, function(err) { + self.fs.writeFile(path.join(self.root, filename), contents.data, wOptions, function(err) { self.emit('file:stor', 'close', { user: self.username, file: filename, @@ -1257,7 +1244,6 @@ FtpConnection.prototype._command_USER = function(username) { } ); } - return this; }; // Specify a password for login @@ -1314,7 +1300,6 @@ FtpConnection.prototype._command_PASS = function(password) { } ); } - return this; }; FtpConnection.prototype._closeSocket = function(socket, shouldDestroy) { diff --git a/lib/FtpServer.js b/lib/FtpServer.js index 2ff206e..c1565b6 100644 --- a/lib/FtpServer.js +++ b/lib/FtpServer.js @@ -71,7 +71,6 @@ FtpServer.prototype._onConnection = function(socket) { allowedCommands: allowedCommands, // subset of allowed commands for this server dataPort: 20, dataHost: null, - dataListener: null, // for incoming passive connections dataSocket: null, // the actual data socket // True if the client has sent a PORT/PASV command, and // we haven't experienced a problem with the configuration @@ -94,25 +93,6 @@ FtpServer.prototype._onConnection = function(socket) { this.emit('client:connected', conn); // pass client info so they can listen for client-specific events - socket.setTimeout(0); - socket.setNoDelay(); - - this._logIf(LOG.INFO, 'Accepted a new client connection'); - conn.respond('220 FTP server (nodeftpd) ready'); - - socket.on('data', function(buf) { - conn._onData(buf); - }); - socket.on('end', function() { - conn._onEnd(); - }); - socket.on('error', function(err) { - conn._onError(err); - }); - // `close` will always be called once (directly after `end` or `error`) - socket.on('close', function(hadError) { - conn._onClose(hadError); - }); }; ['listen', 'close'].forEach(function(fname) { From cd566bb36e0548ce826ce17a7d784750177467a9 Mon Sep 17 00:00:00 2001 From: Rian Wouters Date: Wed, 15 Jun 2016 09:25:17 +0000 Subject: [PATCH 22/36] Simplify error handling --- lib/FtpConnection.js | 20 ++++++-------------- 1 file changed, 6 insertions(+), 14 deletions(-) diff --git a/lib/FtpConnection.js b/lib/FtpConnection.js index cd6f9fc..5269854 100644 --- a/lib/FtpConnection.js +++ b/lib/FtpConnection.js @@ -719,31 +719,23 @@ FtpConnection.prototype._setupPASV = function(commandArg, command) { var firstPort = this.server.options.pasvPortRangeStart; var lastPort = this.server.options.pasvPortRangeEnd; - var normalErrorHandler = function(e) { - self._logIf(LOG.WARN, 'Error with passive data listener: ' + util.inspect(e)); - self.respond('421 Server was unable to open passive connection listener'); - self.dataConfigured = false; - self.dataSocket = null; - self.pasv = null; - }; - this._createPassiveServer() .on('error', function(e) { - if (e.code === 'EADDRINUSE' && e.port < (lastPort || firstPort || 0)) { + if (self.pasv === null && e.code === 'EADDRINUSE' && e.port < (lastPort || firstPort || 0)) { this.listen(++e.port); return; } - self._logIf(LOG.DEBUG, 'Passing on error from portRangeErrorHandler to normalErrorHandler:' + JSON.stringify(e)); - normalErrorHandler(e); + self._logIf(LOG.WARN, 'Passive Error with passive data listener: ' + util.inspect(e)); + self.respond('421 Server was unable to open passive connection listener'); + self.dataConfigured = false; + self.dataSocket = null; + self.pasv = null; }) // Once we're successfully listening, tell the client .on('listening', function() { self._logIf(LOG.DEBUG, 'Passive data connection listening on port ' + this.address().port); - this - .removeAllListeners('error') - .on('error', normalErrorHandler); self.pasv = this; self._writePASVReady(command); }) From bf8b8deb21fd5d4174e580586715b438220d98d2 Mon Sep 17 00:00:00 2001 From: Rian Wouters Date: Wed, 15 Jun 2016 09:25:47 +0000 Subject: [PATCH 23/36] Refactor cloning and context creation --- lib/starttls.js | 23 ++++++++++++----------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/lib/starttls.js b/lib/starttls.js index ede6b39..77f53bb 100644 --- a/lib/starttls.js +++ b/lib/starttls.js @@ -32,23 +32,24 @@ function starttlsClient(socket, options, callback) { return starttls(socket, options, callback, false); } -function starttls(socket, options, callback, isServer) { - var sslcontext; - - var opts = {}; - Object.keys(options).forEach(function(key) { - opts[key] = options[key]; +function clone(o) { + var c = {}; + Object.keys(o).forEach(function(key) { + c[key] = o[key]; }); + return c; +} + +function starttls(socket, options, callback, isServer) { + var opts = clone(options); if (!opts.ciphers) { opts.ciphers = RECOMMENDED_CIPHERS; } socket.removeAllListeners('data'); - if (tls.createSecureContext) { - sslcontext = tls.createSecureContext(opts); - } else { - sslcontext = crypto.createCredentials(opts); - } + var sslcontext = tls.createSecureContext + ? tls.createSecureContext(opts) + : crypto.createCredentials(opts); var pair = tls.createSecurePair(sslcontext, isServer); var cleartext = pipe(pair, socket); From bcdb344421109813ca62ef024faf6d471a57fdc4 Mon Sep 17 00:00:00 2001 From: Rian Wouters Date: Wed, 15 Jun 2016 09:26:31 +0000 Subject: [PATCH 24/36] Defactor port handling --- lib/FtpConnection.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/lib/FtpConnection.js b/lib/FtpConnection.js index 5269854..8385a27 100644 --- a/lib/FtpConnection.js +++ b/lib/FtpConnection.js @@ -690,9 +690,8 @@ FtpConnection.prototype._writePASVReady = function(command) { self._logIf(LOG.DEBUG, 'Telling client that it can connect now'); - var a = self.pasv.address(); var host = self.server.host; - var port = a.port; + var port = self.pasv.address().port; if (command === 'PASV') { var i1 = (port / 256) | 0; var i2 = port % 256; From c8fc071c6f31bd146baf456d28a1d6b4c2166768 Mon Sep 17 00:00:00 2001 From: Rian Wouters Date: Fri, 17 Jun 2016 10:42:09 +0000 Subject: [PATCH 25/36] Add test for directory with 1 file --- test/lib/common.js | 2 +- test/list.js | 176 ++++++++++++++++++++++++++++----------------- 2 files changed, 113 insertions(+), 65 deletions(-) diff --git a/test/lib/common.js b/test/lib/common.js index e41847b..3edb0b2 100644 --- a/test/lib/common.js +++ b/test/lib/common.js @@ -73,7 +73,7 @@ var common = module.exports = { }); connection.on('command:pass', function(pass, success, failure) { if (pass === customOptions.pass) { - success(username); + success(username, customOptions.fs); } else { failure(); } diff --git a/test/list.js b/test/list.js index 31e0773..1764bf3 100644 --- a/test/list.js +++ b/test/list.js @@ -1,4 +1,5 @@ var common = require('./lib/common'); +var fs = require('fs'); describe('LIST command', function() { 'use strict'; @@ -6,81 +7,128 @@ describe('LIST command', function() { var client; var server; - beforeEach(function(done) { - server = common.server(); - client = common.client(done); - }); + describe('regular cases', function() { - function unslashRgx(rgx) { - return String(rgx).replace(/^\/|\/$/g, ''); - } - - it('should return "-" as first character for files', function(done) { - client.list('/', function(error, listing) { - error.should.equal(false); - listing = common.splitResponseLines(listing, / data\d*\.txt$/); - listing.should.have.lengthOf(6); - listing[0].should.startWith('-'); - done(); + beforeEach(function(done) { + server = common.server(); + client = common.client(done); }); - }); - it('should return "d" as first character for directories', function(done) { - client.list('/', function(error, listing) { - error.should.equal(false); - listing = common.splitResponseLines(listing, / usr$/); - listing.should.have.lengthOf(1); - listing[0].should.startWith('d'); - done(); + function unslashRgx(rgx) { + return String(rgx).replace(/^\/|\/$/g, ''); + } + + it('should return "-" as first character for files', function(done) { + client.list('/', function(error, listing) { + error.should.equal(false); + listing = common.splitResponseLines(listing, / data\d*\.txt$/); + listing.should.have.lengthOf(6); + listing[0].should.startWith('-'); + done(); + }); }); - }); - it('should list files similar to ls -l', function(done) { - client.list('/usr', function(error, listing) { - error.should.equal(false); - listing = common.splitResponseLines(listing); - listing.should.have.lengthOf(1); - var lsLongRgx = [ - /($# file modes: ___|)[d-]([r-][w-][x-]){3}/, - /($# ?¿?¿? inodes?: |)\d+/, - /($# owner name: ___|)\S+/, - /($# owner group: __|)\S+/, - /($# size in bytes: |)\d+/, - /($# month: ________|)[A-Z][a-z]{2}/, - /($# day of month: _|)\d{1,2}/, - /($# time or year: _|)([\d ]\d:|19|[2-9]\d)\d{2}/, - /($# file name: ____|)[\S\s]+/, - ].map(unslashRgx).join('\\s+'); - lsLongRgx = new RegExp(lsLongRgx, ''); - var match = (lsLongRgx.exec(listing[0]) || [false]); - match[0].should.equal(listing[0]); - done(); + it('should return "d" as first character for directories', function(done) { + client.list('/', function(error, listing) { + error.should.equal(false); + listing = common.splitResponseLines(listing, / usr$/); + listing.should.have.lengthOf(1); + listing[0].should.startWith('d'); + done(); + }); }); - }); - it('should list a single file', function(done) { - var filename = 'data.txt'; - client.list('/' + filename, function(error, listing) { - error.should.equal(false); - listing = common.splitResponseLines(listing, ' ' + filename); - listing.should.have.lengthOf(1); - listing[0].should.startWith('-'); - done(); + it('should list files similar to ls -l', function(done) { + client.list('/usr', function(error, listing) { + error.should.equal(false); + listing = common.splitResponseLines(listing); + listing.should.have.lengthOf(1); + var lsLongRgx = [ + /($# file modes: ___|)[d-]([r-][w-][x-]){3}/, + /($# ?¿?¿? inodes?: |)\d+/, + /($# owner name: ___|)\S+/, + /($# owner group: __|)\S+/, + /($# size in bytes: |)\d+/, + /($# month: ________|)[A-Z][a-z]{2}/, + /($# day of month: _|)\d{1,2}/, + /($# time or year: _|)([\d ]\d:|19|[2-9]\d)\d{2}/, + /($# file name: ____|)[\S\s]+/, + ].map(unslashRgx).join('\\s+'); + lsLongRgx = new RegExp(lsLongRgx, ''); + var match = (lsLongRgx.exec(listing[0]) || [false]); + match[0].should.equal(listing[0]); + done(); + }); }); - }); - it('should list a subdirectory', function(done) { - client.list('/usr', function(error, listing) { - error.should.equal(false); - listing = common.splitResponseLines(listing); - listing.should.have.lengthOf(1); - listing[0].should.startWith('d'); - listing[0].should.endWith(' local'); - done(); + it('should list a single file', function(done) { + var filename = 'data.txt'; + client.list('/' + filename, function(error, listing) { + error.should.equal(false); + listing = common.splitResponseLines(listing, ' ' + filename); + listing.should.have.lengthOf(1); + listing[0].should.startWith('-'); + done(); + }); + }); + + it('should list a subdirectory', function(done) { + client.list('/usr', function(error, listing) { + error.should.equal(false); + listing = common.splitResponseLines(listing); + listing.should.have.lengthOf(1); + listing[0].should.startWith('d'); + listing[0].should.endWith(' local'); + done(); + }); + }); + + afterEach(function() { + server.close(); }); }); - afterEach(function() { - server.close(); + describe('corner cases', function() { + 'use strict'; + + var files; + + beforeEach(function(done) { + server = common.server({ + fs: { + stat: function(path, callback) { + console.log('STAT', path); + callback( + undefined /* err */, + new fs.Stats(0,32768 /* file mode */,0,0,0,0,0,0,0,43 /* size */,0,0,0,0) + ); + }, + readdir: function(path, callback) { + console.log('READDIR', path); + callback(undefined, files); + }, + }, + }); + client = common.client(done); + }); + + + it.only('supports directories with only a few files', function(done) { + files = ['a']; + client.list('/', function(error, listing) { + error.should.equal(false); + console.log(listing); + listing = common.splitResponseLines(listing); + listing.should.have.lengthOf(1); + done(); + }); + }); + + afterEach(function() { + server.close(); + }); }); + + }); + From f90a4004d3b42385edd37998b8a8f178389914d4 Mon Sep 17 00:00:00 2001 From: Rian Wouters Date: Fri, 17 Jun 2016 10:42:45 +0000 Subject: [PATCH 26/36] Fix for #files < CONC --- lib/glob.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/glob.js b/lib/glob.js index e6805aa..0a8becf 100644 --- a/lib/glob.js +++ b/lib/glob.js @@ -16,7 +16,7 @@ function statList(fsm, list, callback) { var stats = []; var total = list.length; - for (var i = 0; i < CONC; ++i) { + for (var i = 0; i < list.length && i < CONC; ++i) { handleFile(); } From 922907803e502fc32b041d78234ad24b447cad12 Mon Sep 17 00:00:00 2001 From: Rian Wouters Date: Fri, 17 Jun 2016 12:48:40 +0000 Subject: [PATCH 27/36] Add failing test for many files --- test/list.js | 20 +++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/test/list.js b/test/list.js index 1764bf3..73ef3ca 100644 --- a/test/list.js +++ b/test/list.js @@ -88,7 +88,7 @@ describe('LIST command', function() { }); }); - describe('corner cases', function() { + describe('corner case', function() { 'use strict'; var files; @@ -97,7 +97,6 @@ describe('LIST command', function() { server = common.server({ fs: { stat: function(path, callback) { - console.log('STAT', path); callback( undefined /* err */, new fs.Stats(0,32768 /* file mode */,0,0,0,0,0,0,0,43 /* size */,0,0,0,0) @@ -113,7 +112,7 @@ describe('LIST command', function() { }); - it.only('supports directories with only a few files', function(done) { + it('supports directories with only a few files', function(done) { files = ['a']; client.list('/', function(error, listing) { error.should.equal(false); @@ -124,6 +123,21 @@ describe('LIST command', function() { }); }); + it.only('supports directories with many files', function(done) { + function ArrayWithStrings(n) { + return Array.apply(null, Array(n)).map(function(x, i) { + return i.toString(); + }); + } + files = ArrayWithStrings(3000); + client.list('/', function(error, listing) { + error.should.equal(false); + listing = common.splitResponseLines(listing); + listing.should.have.lengthOf(files.length); + done(); + }); + }); + afterEach(function() { server.close(); }); From 6fd194f6b46d27cfc4382d4c033a3f224663a6c8 Mon Sep 17 00:00:00 2001 From: Rian Wouters Date: Mon, 20 Jun 2016 09:55:09 +0000 Subject: [PATCH 28/36] Make fs.stats asynchronous as specified --- test/list.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/test/list.js b/test/list.js index 73ef3ca..873742f 100644 --- a/test/list.js +++ b/test/list.js @@ -97,7 +97,8 @@ describe('LIST command', function() { server = common.server({ fs: { stat: function(path, callback) { - callback( + process.nextTick( + callback, undefined /* err */, new fs.Stats(0,32768 /* file mode */,0,0,0,0,0,0,0,43 /* size */,0,0,0,0) ); From 04f5b377aba68f2e8411f40be383b238ca5dc573 Mon Sep 17 00:00:00 2001 From: Rian Wouters Date: Mon, 20 Jun 2016 09:55:39 +0000 Subject: [PATCH 29/36] Refactor statList --- lib/glob.js | 42 +++++++++++++++++++----------------------- 1 file changed, 19 insertions(+), 23 deletions(-) diff --git a/lib/glob.js b/lib/glob.js index 0a8becf..230f11d 100644 --- a/lib/glob.js +++ b/lib/glob.js @@ -10,46 +10,42 @@ function setMaxStatsAtOnce(n) { // unique directory to be listed. So this can be pretty simple. function statList(fsm, list, callback) { - if (list.length === 0) { - return callback(null, []); - } - - var stats = []; var total = list.length; - for (var i = 0; i < list.length && i < CONC; ++i) { + var finished = false; + var stats = []; + + for (var i = 0; i < CONC; ++i) { handleFile(); } - var erroredOut = false; - function handleFile() { - if (erroredOut) { - return; + if (stats.length === total) { + return finish(null); } + if (list.length === 0) { - if (stats.length === total) { - finished(); - } return; } var path = list.shift(); fsm.stat(path, function(err, st) { if (err) { - erroredOut = true; - callback(err); - } else { - stats.push({ - name: PathModule.basename(path), - stats: st, - }); - handleFile(); + return finish(err); } + + stats.push({ + name: PathModule.basename(path), + stats: st, + }); + handleFile(); }); } - function finished() { - callback(null, stats); + function finish(err) { + if (!finished) { + callback(err, stats); + finished = true; + } } } From ecb17180e306e09a3ff30c7dee48d914a4f55d57 Mon Sep 17 00:00:00 2001 From: Rian Wouters Date: Mon, 20 Jun 2016 09:56:29 +0000 Subject: [PATCH 30/36] Stengthen text to reproduce the other recursion issue --- test/list.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/list.js b/test/list.js index 873742f..5f1205c 100644 --- a/test/list.js +++ b/test/list.js @@ -130,7 +130,7 @@ describe('LIST command', function() { return i.toString(); }); } - files = ArrayWithStrings(3000); + files = ArrayWithStrings(6000); client.list('/', function(error, listing) { error.should.equal(false); listing = common.splitResponseLines(listing); From 2920df6edfff0603cb26f498889865da399173a0 Mon Sep 17 00:00:00 2001 From: Rian Wouters Date: Mon, 20 Jun 2016 11:00:38 +0000 Subject: [PATCH 31/36] Make getUserNameFromUid and getGroupFromGid asynchronous --- lib/FtpServer.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/FtpServer.js b/lib/FtpServer.js index 2ff206e..aa21455 100644 --- a/lib/FtpServer.js +++ b/lib/FtpServer.js @@ -31,10 +31,10 @@ function FtpServer(host, options) { self.getRoot = options.getRoot; self.getUsernameFromUid = options.getUsernameFromUid || function(uid, c) { - c(null, 'ftp'); + process.nextTick(c, null, 'ftp'); }; self.getGroupFromGid = options.getGroupFromGid || function(gid, c) { - c(null, 'ftp'); + process.nextTick(c, null, 'ftp'); }; self.debugging = options.logLevel || 0; self.useWriteFile = options.useWriteFile; From 087f01296c67c58227a1f89bc75a2fe6b9ce25b7 Mon Sep 17 00:00:00 2001 From: Rian Wouters Date: Mon, 20 Jun 2016 14:29:54 +0000 Subject: [PATCH 32/36] linearize code --- lib/glob.js | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/lib/glob.js b/lib/glob.js index 230f11d..3fb6fdb 100644 --- a/lib/glob.js +++ b/lib/glob.js @@ -42,10 +42,11 @@ function statList(fsm, list, callback) { } function finish(err) { - if (!finished) { - callback(err, stats); - finished = true; + if (finished) { + return; } + finished = true; + callback(err, stats); } } From 41e67abe17e362d53956728d3a7e4b47dc05111a Mon Sep 17 00:00:00 2001 From: Rian Wouters Date: Mon, 20 Jun 2016 14:32:58 +0000 Subject: [PATCH 33/36] Make readdir asynchronous and add empty dir test --- test/list.js | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/test/list.js b/test/list.js index 5f1205c..0a624b9 100644 --- a/test/list.js +++ b/test/list.js @@ -104,27 +104,33 @@ describe('LIST command', function() { ); }, readdir: function(path, callback) { - console.log('READDIR', path); - callback(undefined, files); + process.nextTick(callback, undefined, files); }, }, }); client = common.client(done); }); + it('supports directories without files', function(done) { + files = []; + client.list('/', function(error, listing) { + error.should.equal(false); + listing.should.equal(''); + done(); + }); + }); it('supports directories with only a few files', function(done) { files = ['a']; client.list('/', function(error, listing) { error.should.equal(false); - console.log(listing); listing = common.splitResponseLines(listing); listing.should.have.lengthOf(1); done(); }); }); - it.only('supports directories with many files', function(done) { + it('supports directories with many files', function(done) { function ArrayWithStrings(n) { return Array.apply(null, Array(n)).map(function(x, i) { return i.toString(); From 6d273e4f19ea38a2be1b82a984bafcaa5efe023e Mon Sep 17 00:00:00 2001 From: Rian Wouters Date: Mon, 20 Jun 2016 14:36:45 +0000 Subject: [PATCH 34/36] Harmonize concurrent implementation --- lib/FtpConnection.js | 46 ++++++++++++++++++++++---------------------- 1 file changed, 23 insertions(+), 23 deletions(-) diff --git a/lib/FtpConnection.js b/lib/FtpConnection.js index 828e1a8..e373d3a 100644 --- a/lib/FtpConnection.js +++ b/lib/FtpConnection.js @@ -486,46 +486,46 @@ FtpConnection.prototype._LIST = function(commandArg, detailed, cmd) { // We're not doing a detailed listing, so we don't need to get username // and group name. fileInfos = files; - return finished(); + return finish(); } // Now we need to get username and group name for each file from user/group ids. fileInfos = []; var CONC = self.server.options.maxStatsAtOnce; - var j = 0; - for (var i = 0; i < files.length && i < CONC; ++i) { - handleFile(i); + var total = files.length; + for (var i = 0; i < CONC; ++i) { + handleFile(); } - j = --i; - function handleFile(ii) { - if (i >= files.length) { - return i === files.length + j ? finished() : null; + function handleFile() { + if (fileInfos.length === total) { + return finish(); } - self.server.getUsernameFromUid(files[ii].stats.uid, function(e1, uname) { - self.server.getGroupFromGid(files[ii].stats.gid, function(e2, gname) { + if (files.length === 0) { + return; + } + + var file = files.shift(); + self.server.getUsernameFromUid(file.stats.uid, function(e1, uname) { + self.server.getGroupFromGid(file.stats.gid, function(e2, gname) { if (e1 || e2) { self._logIf(LOG.WARN, 'Error getting user/group name for file: ' + util.inspect(e1 || e2)); - fileInfos.push({ - file: files[ii], - uname: null, - gname: null, - }); - } else { - fileInfos.push({ - file: files[ii], - uname: uname, - gname: gname, - }); + uname = null; + gname = null; } - handleFile(++i); + fileInfos.push({ + file: file, + uname: uname, + gname: gname, + }); + handleFile(); }); }); } - function finished() { + function finish() { // Sort file names. if (!self.server.options.dontSortFilenames) { if (self.server.options.filenameSortMap !== false) { From 15428591c1f626f2095ce5b05954d42b5bde0a7c Mon Sep 17 00:00:00 2001 From: Rian Wouters Date: Mon, 20 Jun 2016 14:38:50 +0000 Subject: [PATCH 35/36] Simplify --- lib/glob.js | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/lib/glob.js b/lib/glob.js index 3fb6fdb..afae84f 100644 --- a/lib/glob.js +++ b/lib/glob.js @@ -207,13 +207,7 @@ function glob(path, fsm, callback, noWildcards) { matches.map(function(p) { return PathModule.join(base, p); }), - function(err, list) { - if (err) { - callback(err); - } else { - callback(null, list); - } - } + callback ); } } From e8f81a7c10c217252dcdc996a74e2301f4a6518c Mon Sep 17 00:00:00 2001 From: Rian Wouters Date: Tue, 21 Jun 2016 09:45:50 +0000 Subject: [PATCH 36/36] Simplify --- lib/glob.js | 202 ++++++++++++++++++++++++---------------------------- 1 file changed, 95 insertions(+), 107 deletions(-) diff --git a/lib/glob.js b/lib/glob.js index afae84f..05781c5 100644 --- a/lib/glob.js +++ b/lib/glob.js @@ -89,130 +89,118 @@ function glob(path, fsm, callback, noWildcards) { if (w === path.length) { // There are no wildcards. fsm.readdir(path, function(err, contents) { if (err) { - if (err.code === 'ENOTDIR') { - statList(fsm, [path], function(err, list) { - if (err) { - return callback(err); - } - if (list.length !== 1) { - throw new Error('Internal error in glob.js'); - } - callback(null, list); - }); - } else if (err.code === 'ENOENT') { + if (err.code === 'ENOENT') { callback(null, []); - } else { + return; + } + + if (err.code !== 'ENOTDIR') { callback(err); + return; } - } else { - statList( - fsm, - contents.map(function(p) { - return PathModule.join(path, p); - }), - function(err, list) { - if (err) { - callback(err); - } else { - callback(null, list); - } - } - ); + + contents = ['']; } + + var list = contents.map(function(p) { + return PathModule.join(path, p); + }); + + statList(fsm, list, callback); }); return; - } else { - // Check that there is no '/' after the first wildcard. + } - var i; - for (i = w; i < path.length; ++i) { - if (path.charAt(i) === '/') { - return callback(null, []); - } + var i; + + // Check that there is no '/' after the first wildcard. + for (i = w; i < path.length; ++i) { + if (path.charAt(i) === '/') { + return callback(null, []); } + } - var base = ''; - var pattern; - for (i = w; i >= 0; --i) { - if (path.charAt(i) === '/') { - base = path.substr(0, i + 1); - break; - } + var base = ''; + var pattern; + for (i = w; i >= 0; --i) { + if (path.charAt(i) === '/') { + base = path.substr(0, i + 1); + break; } - pattern = path.substr(i === 0 ? 0 : i + 1); + } + pattern = path.substr(i === 0 ? 0 : i + 1); - // Remove any leading/trailing slashes which might still - // be present if the path contains multiple slashes. - for (i = 0; i < pattern.length && pattern.charAt(i) === '/'; ++i) { + // Remove any leading/trailing slashes which might still + // be present if the path contains multiple slashes. + for (i = 0; i < pattern.length && pattern.charAt(i) === '/'; ++i) { - } - if (i > 0) { - pattern = pattern.substr(i); - } - for (i = base.length - 1; i > 0 && base.charAt(i) === '/'; --i) { + } + if (i > 0) { + pattern = pattern.substr(i); + } + for (i = base.length - 1; i > 0 && base.charAt(i) === '/'; --i) { - } - if (i !== base.length - 1) { - base = base.substr(0, i + 1); - } + } + if (i !== base.length - 1) { + base = base.substr(0, i + 1); + } + + // We now have the base path in 'base' (possibly the empty string) + // and the wildcard filename pattern in 'pattern'. - // We now have the base path in 'base' (possibly the empty string) - // and the wildcard filename pattern in 'pattern'. - - readTheDir(false); - function readTheDir(listingSingleDir) { - fsm.readdir(base, function(err, contents) { - if (err) { - if (err.code === 'ENOTDIR' || err.code === 'ENOENT') { - callback(null, []); - } else { - callback(err); - } + readTheDir(false); + function readTheDir(listingSingleDir) { + fsm.readdir(base, function(err, contents) { + if (err) { + if (err.code === 'ENOTDIR' || err.code === 'ENOENT') { + callback(null, []); + } else { + callback(err); + } + } else { + var matches; + if (!listingSingleDir) { + matches = contents.filter(function(n) { + return matchPattern(pattern, n); + }); } else { - var matches; - if (!listingSingleDir) { - matches = contents.filter(function(n) { - return matchPattern(pattern, n); - }); - } else { - matches = contents; - } - - // Special case. If we have exactly one match, and it's a directory, then list - // the contents of that directory. (There's no reason why anyone should want - // to identify mutliple directories using wildcards and then list all of their - // contents over FTP!) - if (!listingSingleDir && matches.length === 1) { - var dir = PathModule.join(base, matches[0]); - fsm.stat(dir, function(err, st) { - if (err) { - return callback(err); - } - - if (!st.isDirectory()) { - doTheNormalThing(); - } else { - base = dir; - readTheDir(/*listingSingleDir=*/true); - } - }); - } else { - doTheNormalThing(); - } - - function doTheNormalThing() { - statList( - fsm, - matches.map(function(p) { - return PathModule.join(base, p); - }), - callback - ); - } + matches = contents; } - }); - } + + // Special case. If we have exactly one match, and it's a directory, then list + // the contents of that directory. (There's no reason why anyone should want + // to identify mutliple directories using wildcards and then list all of their + // contents over FTP!) + if (!listingSingleDir && matches.length === 1) { + var dir = PathModule.join(base, matches[0]); + fsm.stat(dir, function(err, st) { + if (err) { + return callback(err); + } + + if (!st.isDirectory()) { + doTheNormalThing(); + } else { + base = dir; + readTheDir(/*listingSingleDir=*/true); + } + }); + } else { + doTheNormalThing(); + } + + function doTheNormalThing() { + statList( + fsm, + matches.map(function(p) { + return PathModule.join(base, p); + }), + callback + ); + } + } + }); } }