From 2c4e121871bf5c9e51329b77e2d2bac380c61cde Mon Sep 17 00:00:00 2001 From: Simon Sturmer Date: Sun, 17 Jan 2016 07:58:09 +0700 Subject: [PATCH 01/55] cleanup .eslintrc --- .eslintrc | 213 +++++++++++++++++++++++++++--------------------------- 1 file changed, 107 insertions(+), 106 deletions(-) diff --git a/.eslintrc b/.eslintrc index 544521f..b89f710 100644 --- a/.eslintrc +++ b/.eslintrc @@ -3,118 +3,116 @@ "node": true }, "rules": { - "no-cond-assign": 1, // disallow assignment in conditional expressions - "no-console": 0, // disallow use of console: should use nuclide-logging instead - "no-constant-condition": 1, // disallow use of constant expressions in conditions - "comma-dangle": [ // disallow trailing commas in object and array literals + "no-cond-assign": 1, // disallow assignment in conditional expressions + "no-console": 0, // disallow use of console: should use nuclide-logging instead + "no-constant-condition": 1, // disallow use of constant expressions in conditions + "comma-dangle": [ // disallow trailing commas in object and array literals 1, "always-multiline" ], - "no-control-regex": 1, // disallow control characters in regular expressions - "no-debugger": 1, // disallow use of debugger - "no-dupe-keys": 1, // disallow duplicate keys when creating object literals - "no-dupe-args": 1, // disallow duplicate arguments in functions - "no-duplicate-case": 1, // disallow a duplicate case label - "no-empty": 0, // disallow empty statements - "no-empty-character-class": 1, // disallow the use of empty character classes in regular expressions - "no-ex-assign": 1, // disallow assigning to the exception in a catch block - "no-extra-boolean-cast": 1, // disallow double-negation boolean casts in a boolean context - "no-extra-semi": 1, // disallow unnecessary semicolons - "no-func-assign": 1, // disallow overwriting functions written as function declarations - "no-inner-declarations": 0, // disallow function or variable declarations in nested blocks - "no-invalid-regexp": 1, // disallow invalid regular expression strings in the RegExp constructor - "no-negated-in-lhs": 1, // disallow negation of the left operand of an in expression - "no-obj-calls": 1, // disallow the use of object properties of the global object (Math and JSON) as functions - "no-regex-spaces": 1, // disallow multiple spaces in a regular expression literal - "no-reserved-keys": 0, // disallow reserved words being used as object literal keys - "no-sparse-arrays": 1, // disallow sparse arrays - "no-unreachable": 1, // disallow unreachable statements after a return, throw, continue, or break statement - "use-isnan": 1, // disallow comparisons with the value NaN - "valid-jsdoc": 0, // Ensure JSDoc comments are valid - "valid-typeof": 1, // Ensure that the results of typeof are compared against a valid string + "no-control-regex": 1, // disallow control characters in regular expressions + "no-debugger": 1, // disallow use of debugger + "no-dupe-keys": 1, // disallow duplicate keys when creating object literals + "no-dupe-args": 1, // disallow duplicate arguments in functions + "no-duplicate-case": 1, // disallow a duplicate case label + "no-empty": 0, // disallow empty statements + "no-empty-character-class": 1, // disallow the use of empty character classes in regular expressions + "no-ex-assign": 1, // disallow assigning to the exception in a catch block + "no-extra-boolean-cast": 1, // disallow double-negation boolean casts in a boolean context + "no-extra-semi": 1, // disallow unnecessary semicolons + "no-func-assign": 1, // disallow overwriting functions written as function declarations + "no-inner-declarations": 0, // disallow function or variable declarations in nested blocks + "no-invalid-regexp": 1, // disallow invalid regular expression strings in the RegExp constructor + "no-negated-in-lhs": 1, // disallow negation of the left operand of an in expression + "no-obj-calls": 1, // disallow the use of object properties of the global object (Math and JSON) as functions + "no-regex-spaces": 1, // disallow multiple spaces in a regular expression literal + "no-reserved-keys": 0, // disallow reserved words being used as object literal keys + "no-sparse-arrays": 1, // disallow sparse arrays + "no-unreachable": 1, // disallow unreachable statements after a return, throw, continue, or break statement + "use-isnan": 1, // disallow comparisons with the value NaN + "valid-jsdoc": 0, // Ensure JSDoc comments are valid + "valid-typeof": 1, // Ensure that the results of typeof are compared against a valid string - // Best Practices - // These are rules designed to prevent you from making mistakes. They either prescribe a better way of doing something or help you avoid footguns. + // Best Practices (designed to prevent you from making mistakes) - "block-scoped-var": 0, // treat var statements as if they were block scoped - "complexity": 0, // specify the maximum cyclomatic complexity allowed in a program - "consistent-return": 0, // require return statements to either always or never specify values - "curly": 1, // specify curly brace conventions for all control statements - "default-case": 0, // require default case in switch statements - "dot-notation": 0, // dot notation encouraged except for foreign properties that cannot be renamed (i.e., Closure Compiler rules) - "eqeqeq": [1, "allow-null"], // require the use of === and !== - "guard-for-in": 1, // make sure for-in loops have an if statement - "no-alert": 1, // disallow the use of alert, confirm, and prompt - "no-caller": 1, // disallow use of arguments.caller or arguments.callee - "no-div-regex": 1, // disallow division operators explicitly at beginning of regular expression - "no-else-return": 0, // disallow else after a return in an if - "no-empty-label": 1, // disallow use of labels for anything other then loops and switches - "no-eq-null": 0, // disallow comparisons to null without a type-checking operator - "no-eval": 1, // disallow use of eval() - "no-extend-native": 1, // disallow adding to native types - "no-extra-bind": 1, // disallow unnecessary function binding - "no-fallthrough": 1, // disallow fallthrough of case statements - "no-floating-decimal": 1, // disallow the use of leading or trailing decimal points in numeric literals - "no-implied-eval": 1, // disallow use of eval()-like methods - "no-labels": 1, // disallow use of labeled statements - "no-iterator": 1, // disallow usage of __iterator__ property - "no-lone-blocks": 1, // disallow unnecessary nested blocks - "no-loop-func": 0, // disallow creation of functions within loops - "no-multi-str": 0, // disallow use of multiline strings - "no-native-reassign": 0, // disallow reassignments of native objects - "no-new": 1, // disallow use of new operator when not part of the assignment or comparison - "no-new-func": 1, // disallow use of new operator for Function object - "no-new-wrappers": 1, // disallows creating new instances of String,Number, and Boolean - "no-octal": 1, // disallow use of octal literals - "no-octal-escape": 1, // disallow use of octal escape sequences in string literals, such as var foo = "Copyright \251"; - "no-proto": 1, // disallow usage of __proto__ property - "no-redeclare": 1, // disallow declaring the same variable more then once - "no-return-assign": 1, // disallow use of assignment in return statement - "no-script-url": 1, // disallow use of javascript: urls. - "no-self-compare": 1, // disallow comparisons where both sides are exactly the same - "no-sequences": 1, // disallow use of comma operator - "no-unused-expressions": 0, // disallow usage of expressions in statement position - "no-void": 1, // disallow use of void operator - "no-warning-comments": 0, // disallow usage of configurable warning terms in comments - "no-with": 1, // disallow use of the with statement - "radix": 1, // require use of the second argument for parseInt() - "vars-on-top": 0, // requires to declare all vars on top of their containing scope - "wrap-iife": 0, // require immediate function invocation to be wrapped in parentheses - "yoda": 1, // require or disallow Yoda conditions - "strict": 0, // this rule conflicts with 'use-babel' so we'll just disable it + "block-scoped-var": 0, // treat var statements as if they were block scoped + "complexity": 0, // specify the maximum cyclomatic complexity allowed in a program + "consistent-return": 0, // require return statements to either always or never specify values + "curly": 1, // specify curly brace conventions for all control statements + "default-case": 0, // require default case in switch statements + "dot-notation": 0, // dot notation encouraged except for foreign properties that cannot be renamed (i.e., Closure Compiler rules) + "eqeqeq": [1, "allow-null"], // require the use of === and !== + "guard-for-in": 1, // make sure for-in loops have an if statement + "no-alert": 1, // disallow the use of alert, confirm, and prompt + "no-caller": 1, // disallow use of arguments.caller or arguments.callee + "no-div-regex": 1, // disallow division operators explicitly at beginning of regular expression + "no-else-return": 0, // disallow else after a return in an if + "no-empty-label": 1, // disallow use of labels for anything other then loops and switches + "no-eq-null": 0, // disallow comparisons to null without a type-checking operator + "no-eval": 1, // disallow use of eval() + "no-extend-native": 1, // disallow adding to native types + "no-extra-bind": 1, // disallow unnecessary function binding + "no-fallthrough": 1, // disallow fallthrough of case statements + "no-floating-decimal": 1, // disallow the use of leading or trailing decimal points in numeric literals + "no-implied-eval": 1, // disallow use of eval()-like methods + "no-labels": 1, // disallow use of labeled statements + "no-iterator": 1, // disallow usage of __iterator__ property + "no-lone-blocks": 1, // disallow unnecessary nested blocks + "no-loop-func": 0, // disallow creation of functions within loops + "no-multi-str": 0, // disallow use of multiline strings + "no-native-reassign": 0, // disallow reassignments of native objects + "no-new": 1, // disallow use of new operator when not part of the assignment or comparison + "no-new-func": 1, // disallow use of new operator for Function object + "no-new-wrappers": 1, // disallows creating new instances of String,Number, and Boolean + "no-octal": 1, // disallow use of octal literals + "no-octal-escape": 1, // disallow use of octal escape sequences in string literals, such as var foo = "Copyright \251"; + "no-proto": 1, // disallow usage of __proto__ property + "no-redeclare": 1, // disallow declaring the same variable more then once + "no-return-assign": 1, // disallow use of assignment in return statement + "no-script-url": 1, // disallow use of javascript: urls. + "no-self-compare": 1, // disallow comparisons where both sides are exactly the same + "no-sequences": 1, // disallow use of comma operator + "no-unused-expressions": 0, // disallow usage of expressions in statement position + "no-void": 1, // disallow use of void operator + "no-warning-comments": 0, // disallow usage of configurable warning terms in comments e.g. TODO or FIXME + "no-with": 1, // disallow use of the with statement + "radix": 1, // require use of the second argument for parseInt() + "vars-on-top": 0, // requires to declare all vars on top of their containing scope + "wrap-iife": 0, // require immediate function invocation to be wrapped in parentheses + "yoda": 1, // require or disallow Yoda conditions + "strict": 0, // this rule conflicts with 'use-babel' so we'll just disable it // Variables - // These rules have to do with variable declarations. - "no-catch-shadow": 1, // disallow the catch clause parameter name being the same as a variable in the outer scope (off by default in the node environment) - "no-delete-var": 1, // disallow deletion of variables - "no-label-var": 1, // disallow labels that share a name with a variable - "no-shadow": 0, // disallow declaration of variables already declared in the outer scope - "no-shadow-restricted-names": 1, // disallow shadowing of names such as arguments - "no-undef": 1, // disallow undeclared variables - "no-undefined": 0, // disallow use of undefined variable - "no-undef-init": 0, // disallow use of undefined when initializing variables - "no-unused-vars": 1, // disallow declaration of variables that are not used in the code - "no-use-before-define": 0, // disallow use of variables before they are defined + "no-catch-shadow": 1, // disallow the catch clause parameter name being the same as a variable in the outer scope (off by default in the node environment) + "no-delete-var": 1, // disallow deletion of variables + "no-label-var": 1, // disallow labels that share a name with a variable + "no-shadow": 0, // disallow declaration of variables already declared in the outer scope + "no-shadow-restricted-names": 1, // disallow shadowing of names such as arguments + "no-undef": 1, // disallow undeclared variables + "no-undefined": 0, // disallow use of undefined variable + "no-undef-init": 0, // disallow use of undefined when initializing variables + "no-unused-vars": 1, // disallow declaration of variables that are not used in the code + "no-use-before-define": 0, // disallow use of variables before they are defined // Node.js - // These rules are specific to JavaScript running on Node.js. - "handle-callback-err": 1, // enforces error handling in callbacks - "no-mixed-requires": 1, // disallow mixing regular variable and require declarations - "no-new-require": 1, // disallow use of new operator with the require function - "no-path-concat": 1, // disallow string concatenation with __dirname and __filename - "no-process-exit": 0, // disallow process.exit() (on by default in the node environment) - "no-restricted-modules": 1, // restrict usage of specified node modules - "no-sync": 0, // disallow use of synchronous methods + "handle-callback-err": 1, // enforces error handling in callbacks + "no-mixed-requires": 1, // disallow mixing regular variable and require declarations + "no-new-require": 1, // disallow use of new operator with the require function + "no-path-concat": 1, // disallow string concatenation with __dirname and __filename + "no-process-exit": 0, // disallow process.exit() + "no-restricted-modules": 1, // restrict usage of specified node modules + "no-sync": 0, // disallow use of synchronous methods - // Stylistic Issues - // These rules are purely matters of style and are quite subjective. + // Stylistic (these rules are purely matters of style and are quite subjective) "key-spacing": 0, "comma-spacing": 0, "no-multi-spaces": 0, "brace-style": [ // enforce one true brace style - 1, "1tbs", {"allowSingleLine": false} + 1, "1tbs", { + "allowSingleLine": false + } ], "camelcase": [ // require camel case names 1, {"properties": "never"} @@ -148,8 +146,12 @@ "space-before-function-paren": [ // disallow a space before function parenthesis 1, "never" ], - "object-curly-spacing": [1, "never"], // disallow spaces inside of curly braces in object literals - "array-bracket-spacing": [1, "never"],// disallow spaces inside of curly braces in object literals + "object-curly-spacing": [ // disallow spaces inside of curly braces in object literals + 1, "never" + ], + "array-bracket-spacing": [ // disallow spaces inside of curly braces in array literals + 1, "never" + ], "space-in-parens": 1, // require or disallow spaces inside parentheses "space-infix-ops": 1, // require spaces around operators "space-return-throw-case": 1, // require a space after return, throw, and case @@ -158,20 +160,19 @@ "one-var": [1, "never"], // allow just one var statement per function "wrap-regex": 0, // require regex literals to be wrapped in parentheses - // Legacy - // The following rules are included for compatibility with JSHint and JSLint. While the names of the rules may not match up with the JSHint/JSLint counterpart, the functionality is the same. + // Legacy (included for compatibility with JSHint and JSLint. While the names of the rules may not match up with the JSHint/JSLint counterpart, the functionality is the same) - "max-depth": 0, // specify the maximum depth that blocks can be nested - //"max-len": [ // specify the maximum length of a line in your program [warning level, max line length, number of characters to treat a tab as] + "max-depth": 0, // specify the maximum depth that blocks can be nested + //"max-len": [ // specify the maximum length of a line in your program [warning level, max line length, number of characters to treat a tab as] // 1, 100, 2, { // "ignoreUrls": true, // "ignorePattern": "^\\s*(import\\s[^{]+from|(var|const|let)\\s[^{]+=\\s*require\\s*\\()" // } //], - "max-params": 0, // limits the number of parameters that can be used in the function declaration. - "max-statements": 0, // specify the maximum number of statement allowed in a function - "no-bitwise": 0, // disallow use of bitwise operators - "no-plusplus": 0 // disallow use of unary operators, ++ and -- + "max-params": 0, // limits the number of parameters that can be used in the function declaration. + "max-statements": 0, // specify the maximum number of statement allowed in a function + "no-bitwise": 0, // disallow use of bitwise operators + "no-plusplus": 0 // disallow use of unary operators, ++ and -- } } From f54c6d1d38a84668d03ed38f99c2e6a9d92a2663 Mon Sep 17 00:00:00 2001 From: Simon Sturmer Date: Sat, 16 Jan 2016 18:42:28 +0800 Subject: [PATCH 02/55] split into smaller modules --- lib/Constants.js | 31 + lib/FtpConnection.js | 1415 +++++++++++++++++++++++++++++ lib/FtpServer.js | 145 +++ lib/PassiveListener.js | 10 + lib/ftpd.js | 1673 +---------------------------------- lib/helpers/leftPad.js | 10 + lib/helpers/pathEscape.js | 9 + lib/helpers/stripOptions.js | 23 + lib/helpers/withCwd.js | 14 + 9 files changed, 1660 insertions(+), 1670 deletions(-) create mode 100644 lib/Constants.js create mode 100644 lib/FtpConnection.js create mode 100644 lib/FtpServer.js create mode 100644 lib/PassiveListener.js create mode 100644 lib/helpers/leftPad.js create mode 100644 lib/helpers/pathEscape.js create mode 100644 lib/helpers/stripOptions.js create mode 100644 lib/helpers/withCwd.js diff --git a/lib/Constants.js b/lib/Constants.js new file mode 100644 index 0000000..9d0f028 --- /dev/null +++ b/lib/Constants.js @@ -0,0 +1,31 @@ +module.exports = { + // Whitelist of commands which don't require authentication. + // All other commands sent by unauthorized users will be rejected by default. + DOES_NOT_REQUIRE_AUTH: { + AUTH: true, + FEAT: true, + NOOP: true, + PASS: true, + PBSZ: true, + PROT: true, + QUIT: true, + TYPE: true, + SYST: true, + USER: true, + }, + // Commands which can't be issued until a PASV/PORT command has been sent + // without an intervening data connection error. + REQUIRES_CONFIGURED_DATA: { + LIST: true, + NLST: true, + RETR: true, + STOR: true, + }, + LOG_LEVELS: { + ERROR: 0, + WARN: 1, + INFO: 2, + DEBUG: 3, + TRACE: 4, + }, +}; diff --git a/lib/FtpConnection.js b/lib/FtpConnection.js new file mode 100644 index 0000000..9156e8b --- /dev/null +++ b/lib/FtpConnection.js @@ -0,0 +1,1415 @@ +/* eslint-disable no-octal */ + +var net = require('net'); +var util = require('util'); +var events = require('events'); +var pathModule = require('path'); +var fsModule = require('fs'); +var StatMode = require('stat-mode'); +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'); +var stripOptions = require('./helpers/stripOptions'); +var leftPad = require('./helpers/leftPad'); + +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; + Object.keys(properties).forEach(function(key) { + self[key] = properties[key]; + }); +} +util.inherits(FtpConnection, EventEmitter); + +// TODO: rename this to writeLine? +FtpConnection.prototype.respond = function(message, callback) { + return this._writeText(this.socket, message + '\r\n', callback); +}; + +FtpConnection.prototype._logIf = function(verbosity, message) { + return this.server._logIf(verbosity, message, this); +}; + +// We don't want to use setEncoding because it screws up TLS, but we +// also don't want to explicitly specify ASCII encoding for every call to 'write' +// with a string argument. +FtpConnection.prototype._writeText = function(socket, data, callback) { + if (!socket.writable) { + this._logIf(LOG.DEBUG, 'Attempted writing to a closed socket:\n>> ' + data.trim()); + return; + } + this._logIf(LOG.TRACE, '>> ' + data.trim()); + return socket.write(data, 'utf8', callback); +}; + +FtpConnection.prototype._authenticated = function() { + return !!this.username; +}; + +FtpConnection.prototype._closeDataConnections = function() { + if (this.dataSocket) { + // TODO: should the second arg be false here? + this._closeSocket(this.dataSocket, true); + this.dataSocket = null; + } + if (this.pasv) { + this.pasv.close(); + this.pasv = null; + } +}; + +FtpConnection.prototype._createPassiveServer = function() { + var self = this; + + 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'); + + if (self.secure) { + self._logIf(LOG.INFO, 'Upgrading passive connection to TLS'); + starttls.starttlsServer(psocket, self.server.options.tlsOptions, function(err, cleartext) { + if (err) { + 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, 'Allowing unauthorized passive connection (allowUnauthorizedTls is on)'); + switchToSecure(); + } else { + self._logIf(LOG.INFO, 'Closing unauthorized 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'); + // TODO: Check for existing dataSocket. + self.dataSocket = cleartext; + setupPassiveListener(); + } + }); + } else { + // TODO: Check for existing dataSocket. + self.dataSocket = psocket; + setupPassiveListener(); + } + + function setupPassiveListener() { + if (self.dataListener) { + self.dataListener.emit('ready'); + } else { + self._logIf(LOG.WARN, 'Passive connection initiated, but no data listener'); + } + + // 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')); + function allOver(ename) { + return function(err) { + self._logIf( + (err ? LOG.ERROR : LOG.DEBUG), + 'Passive data event: ' + ename + (err ? ' due to error' : '') + ); + self.dataSocket = null; + }; + } + + self.dataSocket.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; + }); + } + }); +}; + +FtpConnection.prototype._whenDataReady = function(callback) { + var self = this; + + if (self.dataListener) { + // 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.dataListener.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); + }); + } + } +}; + +FtpConnection.prototype._initiateData = function(callback) { + var self = this; + + if (self.dataSocket) { + return callback(self.dataSocket); + } + + 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) : '') + ); + } + + 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) { + this._logIf(LOG.ERROR, 'Client connection error: ' + util.inspect(err)); + this._closeSocket(this.socket, true); +}; + +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. + if (this.dataSocket) { + this._closeSocket(this.dataSocket, hadError); + this.dataSocket = null; + } + 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'); +}; + +FtpConnection.prototype._onData = function(data) { + var self = this; + + if (self.hasQuit) { + return; + } + + data = data.toString('utf-8').trim(); + self._logIf(LOG.TRACE, '<< ' + data); + // Don't want to include passwords in logs. + self._logIf(LOG.INFO, 'FTP command: ' + + data.replace(/^PASS [\s\S]*$/i, 'PASS ***') + ); + + var command; + var commandArg; + var index = data.indexOf(' '); + if (index !== -1) { + var parts = data.split(' '); + command = parts.shift().toUpperCase(); + commandArg = parts.join(' ').trim(); + } else { + command = data.toUpperCase(); + commandArg = ''; + } + + var m = '_command_' + command; + if (self[m]) { + if (self.allowedCommands != null && self.allowedCommands[command] !== true) { + self.respond('502 ' + command + ' not implemented.'); + } else if (DOES_NOT_REQUIRE_AUTH[command]) { + self[m](commandArg, command); + } else { + // If 'tlsOnly' option is set, all commands which require user authentication will only + // 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()) { + checkData(); + } else { + self.respond('530 Not logged in.'); + } + } + + function checkData() { + if (REQUIRES_CONFIGURED_DATA[command] && !self.dataConfigured) { + self.respond('425 Data connection not configured; send PASV or PORT'); + return; + } + + self[m](commandArg, command); + } + } else { + self.respond('502 Command not implemented.'); + } + self.previousCommand = command; +}; + +// 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) { + var self = this; + + if (!self.server.options.tlsOptions || commandArg !== 'TLS') { + return self.respond('502 Command not implemented'); + } + + self.respond('234 Honored', function() { + self._logIf(LOG.INFO, 'Establishing secure connection...'); + starttls.starttlsServer(self.socket, self.server.options.tlsOptions, function(err, cleartext) { + 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(); + } + + function switchToSecure() { + self._logIf(LOG.INFO, 'Secure connection started'); + self.socket = cleartext; + self.socket.on('data', function(data) { + self._onData(data); + }); + self.secure = true; + } + }); + }); +}; + +// Change working directory to parent directory +FtpConnection.prototype._command_CDUP = function() { + var pathServer = pathModule.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 pathEscaped = pathEscape(pathServer); + this.fs.stat(pathFs, function(err, stats) { + if (err) { + this._logIf(LOG.ERROR, 'CWD ' + pathRequest + ': ' + err); + this.respond('550 Directory not found.'); + } else if (!stats.isDirectory()) { + this._logIf(LOG.WARN, 'Attempt to CWD to non-directory'); + this.respond('550 Not a directory'); + } else { + this.cwd = pathServer; + 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) { + if (err) { + self._logIf(LOG.ERROR, 'Error deleting file: ' + filename + ', ' + err); + // write error to socket + self.respond('550 Permission denied'); + } else { + self.respond('250 File deleted'); + } + }); +}; + +FtpConnection.prototype._command_FEAT = function() { + // Get the feature list implemented by the server. (RFC 2389) + this.respond( + '211-Features\r\n' + + ' SIZE\r\n' + + ' UTF8\r\n' + + ' MDTM\r\n' + + (!this.server.options.tlsOptions ? '' : + ' AUTH TLS\r\n' + + ' PBSZ\r\n' + + ' UTF8\r\n' + + ' PROT\r\n' + ) + + '211 end' + ); +}; + +FtpConnection.prototype._command_OPTS = function(commandArg) { + // http://tools.ietf.org/html/rfc2389#section-4 + if (commandArg.toUpperCase() === 'UTF8 ON') { + this.respond('200 OK'); + } else { + this.respond('451 Not supported'); + } +}; + +// Print the file modification time +FtpConnection.prototype._command_MDTM = function(file) { + file = withCwd(this.cwd, file); + file = pathModule.join(this.root, file); + this.fs.stat(file, function(err, stats) { + if (err) { + this.respond('550 File unavailable'); + } else { + this.respond('213 ' + dateformat(stats.mtime, 'yyyymmddhhMMss')); + } + }.bind(this)); + return this; +}; + +FtpConnection.prototype._command_LIST = function(commandArg) { + this._LIST(commandArg, true/*detailed*/, 'LIST'); +}; + +FtpConnection.prototype._command_NLST = function(commandArg) { + this._LIST(commandArg, false/*!detailed*/, 'NLST'); +}; + +FtpConnection.prototype._command_STAT = function(commandArg) { + if (commandArg) { + this._LIST(commandArg, true/*detailed*/, 'STAT'); + } else { + this.respond('211 FTP Server Status OK'); + } +}; + +FtpConnection.prototype._LIST = function(commandArg, detailed, cmd) { + /* + Normally the server responds with a mark using code 150. It then stops accepting new connections, attempts to send the contents of the directory over the data connection, and closes the data connection. Finally it + + accepts the LIST or NLST request with code 226 if the entire directory was successfully transmitted; + rejects the LIST or NLST request with code 425 if no TCP connection was established; + rejects the LIST or NLST request with code 426 if the TCP connection was established but then broken by the client or by network failure; or + rejects the LIST or NLST request with code 451 if the server had trouble reading the directory from disk. + + The server may reject the LIST or NLST request (with code 450 or 550) without first responding with a mark. In this case the server does not touch the data connection. + */ + + var self = this; + + // LIST may be passed options (-a in particular). We just ignore any of these. + // (In the particular case of -a, we show hidden files anyway.) + var dirname = stripOptions(commandArg); + var dir = withCwd(self.cwd, dirname); + + glob.setMaxStatsAtOnce(self.server.options.maxStatsAtOnce); + glob.glob(pathModule.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'); + return; + } + + if (self.server.options.hideDotFiles) { + files = files.filter(function(file) { + if (file.name && file.name[0] !== '.') { + return true; + } + }); + } + + self._logIf(LOG.INFO, 'Directory has ' + files.length + ' files'); + if (files.length === 0) { + return self._listFiles([], detailed, cmd); + } + + var fileInfos; // To contain list of files with info for each. + + if (!detailed) { + // We're not doing a detailed listing, so we don't need to get username + // and group name. + fileInfos = files; + return finished(); + } + + // 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); + } + j = --i; + + function handleFile(ii) { + if (i >= files.length) { + return i === files.length + j ? finished() : null; + } + + self.server.getUsernameFromUid(files[ii].stats.uid, function(e1, uname) { + self.server.getGroupFromGid(files[ii].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, + }); + } + handleFile(++i); + }); + }); + } + + function finished() { + // Sort file names. + if (!self.server.options.dontSortFilenames) { + if (self.server.options.filenameSortMap !== false) { + var sm = ( + self.server.options.filenameSortMap || + function(x) { + return x.toUpperCase(); + } + ); + for (var i = 0; i < fileInfos.length; ++i) { + fileInfos[i]._s = sm(detailed ? fileInfos[i].file.name : fileInfos[i].name); + } + } + + var sf = (self.server.options.filenameSortFunc || + function(x, y) { + return x.localeCompare(y); + }); + fileInfos = fileInfos.sort(function(x, y) { + if (self.server.options.filenameSortMap !== false) { + return sf(x._s, y._s); + } else if (detailed) { + return sf(x.file.name, y.file.name); + } else { + return sf(x.name, y.name); + } + }); + } + + self._listFiles(fileInfos, detailed, cmd); + } + }, self.server.options.noWildcards); +}; + +FtpConnection.prototype._listFiles = function(fileInfos, detailed, cmd) { + var self = this; + + var m = '150 Here comes the directory listing'; + var BEGIN_MSGS = { + LIST: m, NLST: m, STAT: '213-Status follows', + }; + m = '226 Transfer OK'; + var END_MSGS = { + LIST: m, NLST: m, STAT: '213 End of status', + }; + + self.respond(BEGIN_MSGS[cmd], function() { + if (cmd === 'STAT') { + whenReady(self.socket); + } else { + self._whenDataReady(whenReady); + } + + function whenReady(listconn) { + if (fileInfos.length === 0) { + return success(); + } + + function success(err) { + if (err) { + self.respond('550 Error listing files'); + } else { + self.respond(END_MSGS[cmd]); + } + if (cmd !== 'STAT') { + self._closeSocket(listconn); + } + } + + self._logIf(LOG.DEBUG, 'Sending file list'); + + for (var i = 0; i < fileInfos.length; ++i) { + var fileInfo = fileInfos[i]; + + var line = ''; + var file; + + if (!detailed) { + file = fileInfo; + line += file.name + '\r\n'; + } else { + file = fileInfo.file; + var s = file.stats; + var allModes = (new StatMode({mode: s.mode})).toString(); + var rwxModes = allModes.substr(1, 9); + line += (s.isDirectory() ? 'd' : '-') + rwxModes; + // ^-- Clients don't need to know about special files and pipes + line += ' 1 ' + + (fileInfo.uname || 'ftp') + ' ' + + (fileInfo.gname === null ? 'ftp' : fileInfo.gname) + ' '; + 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'; + } + self._writeText( + listconn, + line, + (i === fileInfos.length - 1 ? success : undefined) + ); + } + } + }); +}; + +// Create a directory +FtpConnection.prototype._command_MKD = function(pathRequest) { + var pathServer = withCwd(this.cwd, pathRequest); + var pathEscaped = pathEscape(pathServer); + var pathFs = pathModule.join(this.root, pathServer); + this.fs.mkdir(pathFs, 0755, function(err) { + if (err) { + this._logIf(LOG.ERROR, 'MKD ' + pathRequest + ': ' + err); + this.respond('550 "' + pathEscaped + '" directory NOT created'); + } else { + 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) { + this._PORT(x, y); +}; + +FtpConnection.prototype._command_EPRT = function(x, y) { + this._PORT(x, y); +}; + +FtpConnection.prototype._PORT = function(commandArg, command) { + var self = this; + var m; + + self.dataConfigured = false; + + 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})$/); + if (!m) { + self.respond('501 Bad argument to PORT'); + return; + } + + var host = m[1] + '.' + m[2] + '.' + m[3] + '.' + m[4]; + var port = (parseInt(m[5], 10) << 8) + parseInt(m[6], 10); + if (isNaN(port)) { + // The value should never be NaN because the relevant groups in the regex matche 1-3 digits. + throw new Error('Impossible NaN in FtpConnection.prototype._PORT'); + } + } else { // EPRT + if (commandArg.length >= 3 && commandArg.charAt(0) === '|' && + commandArg.charAt(2) === '|' && commandArg.charAt(1) === '2') { + // Only IPv4 is supported. + self.respond('522 Server cannot handle IPv6 EPRT commands, use (1)'); + return; + } + + m = commandArg.match(/^\|1\|([0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3})\|([0-9]{1,5})/); + if (!m) { + self.respond('501 Bad Argument to EPRT'); + return; + } + + var r = parseInt(m[2], 10); + if (isNaN(r)) { + // The value should never be NaN because the relevant group in the regex matches 1-5 digits. + throw new Error('Impossible NaN in FtpConnection.prototype._PORT (2)'); + } + if (r > 65535 || r <= 0) { + self.respond('501 Bad argument to EPRT (invalid port number)'); + return; + } + + host = m[1]; + port = r; + } + + self.dataConfigured = true; + self.dataHost = host; + self.dataPort = port; + self._logIf(LOG.DEBUG, 'self.dataHost, self.dataPort set to ' + self.dataHost + ':' + self.dataPort); + self.respond('200 OK'); +}; + +FtpConnection.prototype._command_PASV = function(x, y) { + this._PASV(x, y); +}; + +FtpConnection.prototype._command_EPSV = function(x, y) { + this._PASV(x, y); +}; + +FtpConnection.prototype._PASV = function(commandArg, command) { + var self = this; + + self.dataConfigured = false; + + if (command === 'EPSV' && commandArg && commandArg !== '1') { + self.respond('202 Not supported'); + return; + } + + // not sure whether the spec limits to 1 data connection at a time ... + if (self.dataSocket) { + self._closeSocket(self.dataSocket, true); + } + + if (self.dataListener) { + 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); + } + + self.dataConfigured = true; +}; + +FtpConnection.prototype._writePASVReady = function(command) { + var self = this; + + var a = self.pasv.address(); + var host = self.server.host; + var port = a.port; + if (command === 'PASV') { + var i1 = (port / 256) | 0; + var i2 = port % 256; + self.respond('227 Entering Passive Mode (' + host.split('.').join(',') + ',' + i1 + ',' + i2 + ')'); + } else { // EPASV + self.respond('229 Entering Extended Passive Mode (|||' + port + '|)'); + } +}; + +FtpConnection.prototype._setupNewPASV = function(commandArg, command) { + var self = this; + + var pasv = self._createPassiveServer(); + var portRangeErrorHandler; + + function normalErrorHandler(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.dataListener = null; + self.dataSocket = null; + 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); + } + + // 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); + } + + 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'); + }); +}; + +FtpConnection.prototype._command_PBSZ = function(commandArg) { + var self = this; + + if (!self.server.options.tlsOptions) { + return self.respond('202 Not supported'); + } + + // Protection Buffer Size (RFC 2228) + if (!self.secure) { + self.respond('503 Secure connection not established'); + } else if (parseInt(commandArg, 10) !== 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'); + } +}; + +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'); + } +}; + +// 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; +}; + +FtpConnection.prototype._command_QUIT = function() { + var self = this; + + self.hasQuit = true; + self.respond('221 Goodbye', function(err) { + if (err) { + self._logIf(LOG.ERROR, "Error writing 'Goodbye' message following QUIT"); + } + self._closeSocket(self.socket, true); + self._closeDataConnections(); + }); +}; + +FtpConnection.prototype._command_RETR = function(commandArg) { + var filename = pathModule.join(this.root, withCwd(this.cwd, commandArg)); + + if (this.server.options.useReadFile) { + this._RETR_usingReadFile(commandArg, filename); + } else { + this._RETR_usingCreateReadStream(commandArg, filename); + } +}; + +FtpConnection.prototype._RETR_usingCreateReadStream = function(commandArg, filename) { + var self = this; + var startTime = new Date(); + + self.emit('file:retr', 'open', { + user: self.username, + file: filename, + sTime: startTime, + }); + + function afterOk(callback) { + self.respond('150 Opening ' + self.mode.toUpperCase() + ' mode data connection', callback); + } + + + self.fs.open(filename, 'r', function(err, fd) { + if (err) { + self.emit('file:retr', 'error', { + user: self.username, + file: filename, + filesize: 0, + sTime: startTime, + eTime: new Date(), + duration: new Date() - startTime, + errorState: true, + error: err, + }); + if (err.code === 'ENOENT') { + self.respond('550 Not Found'); + } else { // Who knows what's going on here... + self.respond('550 Not Accessible'); + self._logIf(LOG.ERROR, "Error at read of '" + filename + "' other than ENOENT " + err); + } + } else { + afterOk(function() { + self._whenDataReady(function(pasvconn) { + var readLength = 0; + var now = new Date(); + var rs = self.fs.createReadStream(null, {fd: fd}); + rs.pause(); + rs.once('error', function(err) { + self.emit('file:retr', 'close', { + user: self.username, + file: filename, + filesize: 0, + sTime: startTime, + eTime: now, + duration: now - startTime, + errorState: true, + error: err, + }); + }); + + rs.on('data', function(buffer) { + readLength += buffer.length; + }); + + rs.on('end', function() { + var now = new Date(); + self.emit('file:retr', 'close', { + user: self.username, + file: filename, + filesize: 0, + sTime: startTime, + eTime: now, + duration: now - startTime, + errorState: false, + }); + self.respond('226 Closing data connection, sent ' + readLength + ' bytes'); + }); + + rs.pipe(pasvconn); + rs.resume(); + }); + }); + } + }); +}; + +FtpConnection.prototype._RETR_usingReadFile = function(commandArg, filename) { + var self = this; + var startTime = new Date(); + + self.emit('file:retr', 'open', { + user: self.username, + file: filename, + sTime: startTime, + }); + + function afterOk(callback) { + self.respond('150 Opening ' + self.mode.toUpperCase() + ' mode data connection', callback); + } + + self.fs.readFile(filename, function(err, contents) { + if (err) { + self.emit('file:retr', 'error', { + user: self.username, + file: filename, + filesize: 0, + sTime: startTime, + eTime: new Date(), + duration: new Date() - startTime, + errorState: true, + error: err, + }); + if (err.code === 'ENOENT') { + self.respond('550 Not Found'); + } else { // Who knows what's going on here... + self.respond('550 Not Accessible'); + self._logIf(LOG.ERROR, "Error at read of '" + filename + "' other than ENOENT " + err); + } + } else { + afterOk(function() { + self._whenDataReady(function(pasvconn) { + contents = {filename: filename, data: contents}; + self.emit('file:retr:contents', contents); + contents = contents.data; + pasvconn.write(contents); + var contentLength = contents.length; + self.respond('226 Closing data connection, sent ' + contentLength + ' bytes'); + self.emit('file:retr', 'close', { + user: self.username, + file: filename, + filesize: contentLength, + sTime: startTime, + eTime: new Date(), + duration: new Date() - startTime, + errorState: false, + }); + self._closeSocket(pasvconn); + }); + }); + } + }); +}; + +// Remove a directory +FtpConnection.prototype._command_RMD = function(pathRequest) { + var pathServer = withCwd(this.cwd, pathRequest); + var pathFs = pathModule.join(this.root, pathServer); + this.fs.rmdir(pathFs, function(err) { + if (err) { + this._logIf(LOG.ERROR, 'RMD ' + pathRequest + ': ' + err); + this.respond('550 Delete operation failed'); + } else { + this.respond('250 "' + pathServer + '" directory removed'); + } + }.bind(this)); + return this; +}; + +FtpConnection.prototype._command_RNFR = function(commandArg) { + var self = this; + self.filefrom = withCwd(self.cwd, commandArg); + self._logIf(LOG.DEBUG, 'Rename from ' + self.filefrom); + self.respond('350 Ready for destination name'); +}; + +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) { + 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' : '')); + } else { + self.respond('250 File renamed successfully'); + } + }); +}; + +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) { + if (err) { + self._logIf(LOG.ERROR, "Error getting size of file '" + filename + "' "); + self.respond('450 Failed to get size of file'); + return; + } + self.respond('213 ' + s.size + ''); + }); +}; + +FtpConnection.prototype._command_TYPE = function(commandArg) { + if (commandArg === 'I' || commandArg === 'A') { + this.respond('200 OK'); + } else { + this.respond('202 Not supported'); + } +}; + +FtpConnection.prototype._command_SYST = function() { + this.respond('215 UNIX Type: I'); +}; + +FtpConnection.prototype._command_STOR = function(commandArg) { + var filename = withCwd(this.cwd, commandArg); + + if (this.server.options.useWriteFile) { + this._STOR_usingWriteFile(filename, 'w'); + } else { + this._STOR_usingCreateWriteStream(filename, null, 'w'); + } +}; + +// 'initialBuffers' argument is set when this is called from _STOR_usingWriteFile. +FtpConnection.prototype._STOR_usingCreateWriteStream = function(filename, initialBuffers, flag) { + var self = this; + + var wStreamFlags = {flags: flag || 'w', mode: 0644}; + var storeStream = self.fs.createWriteStream(pathModule.join(self.root, filename), wStreamFlags); + var notErr = true; + // Adding for event metadata for file upload (STOR) + var startTime = new Date(); + var uploadSize = 0; + + if (initialBuffers) { + //todo: handle back-pressure + initialBuffers.forEach(function(b) { + storeStream.write(b); + }); + } + + self._whenDataReady(handleUpload); + + storeStream.on('open', function() { + self._logIf(LOG.DEBUG, 'File opened/created: ' + filename); + self._logIf(LOG.DEBUG, 'Told client ok to send file data'); + // Adding event emitter for upload start time + self.emit('file:stor', 'open', { + user: self.username, + file: filename, + time: startTime, + }); + + self.respond('150 Ok to send data'); + }); + + storeStream.on('error', function() { + self.emit('file:stor', 'error', { + user: self.username, + file: filename, + filesize: uploadSize, + sTime: startTime, + eTime: new Date(), + duration: new Date() - startTime, + errorState: !notErr, + }); + storeStream.end(); + notErr = false; + if (self.dataSocket) { + self._closeSocket(self.dataSocket, true); + } + self.respond('426 Connection closed; transfer aborted'); + }); + + storeStream.on('finish', function() { + // Adding event emitter for completed upload. + self.emit('file:stor', 'close', { + user: self.username, + file: filename, + filesize: uploadSize, + sTime: startTime, + eTime: new Date(), + duration: new Date() - startTime, + errorState: !notErr, + }); + notErr ? self.respond('226 Closing data connection') : true; + if (self.dataSocket) { + self._closeSocket(self.dataSocket); + } + }); + + function handleUpload(dataSocket) { + var isPaused = false; + dataSocket.on('data', function(buff) { + var result = storeStream.write(buff); + // Handle back-pressure + if (result === false) { + dataSocket.pause(); + isPaused = true; + storeStream.once('drain', function() { + dataSocket.resume(); + isPaused = false; + }); + } + }); + dataSocket.once('error', function() { + notErr = false; + storeStream.end(); + }); + dataSocket.once('finish', function() { + if (isPaused) { + storeStream.once('drain', function() { + storeStream.end(); + }); + } else { + storeStream.end(); + } + }); + } +}; + +FtpConnection.prototype._STOR_usingWriteFile = function(filename, flag) { + var self = this; + + var erroredOut = false; + var slurpBuf = new Buffer(1024); + var totalBytes = 0; + var startTime = new Date(); + + self.emit('file:stor', 'open', { + user: self.username, + file: filename, + time: startTime, + }); + + self.respond('150 Ok to send data', function() { + self._whenDataReady(handleUpload); + }); + + 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 && + totalBytes + buf.length > self.server.options.uploadMaxSlurpSize) { + // Give up trying to slurp it -- it's too big. + + // If the 'fs' module we've been given doesn't implement 'createWriteStream', then + // we give up and send the client an error. + if (!self.fs.createWriteStream) { + if (self.dataSocket) { + self._closeSocket(self.dataSocket, true); + } + self.respond('552 Requested file action aborted; file too big'); + return; + } + + // Otherwise, we call _STOR_usingWriteStream, and tell it to prepend the stuff + // that we've buffered so far to the file. + self._logIf(LOG.WARN, 'uploadMaxSlurpSize exceeded; falling back to createWriteStream'); + self._STOR_usingCreateWriteStream(filename, [slurpBuf.slice(0, totalBytes), buf]); + self.dataSocket.removeListener('data', dataHandler); + self.dataSocket.removeListener('error', errorHandler); + self.dataSocket.removeListener('close', closeHandler); + } else { + if (totalBytes + buf.length > slurpBuf.length) { + var newLength = slurpBuf.length * 2; + if (newLength < totalBytes + buf.length) { + newLength = totalBytes + buf.length; + } + + var newSlurpBuf = new Buffer(newLength); + slurpBuf.copy(newSlurpBuf, 0, 0, totalBytes); + slurpBuf = newSlurpBuf; + } + buf.copy(slurpBuf, totalBytes, 0, buf.length); + totalBytes += buf.length; + } + } + + function closeHandler() { + if (erroredOut) { + return; + } + + 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.emit('file:stor', 'close', { + user: self.username, + file: filename, + filesize: totalBytes, + sTime: startTime, + eTime: new Date(), + duration: new Date() - startTime, + errorState: err ? true : false, + }); + if (err) { + erroredOut = true; + self._logIf(LOG.ERROR, 'Error writing file. ' + err); + if (self.dataSocket) { + self._closeSocket(self.dataSocket, true); + } + self.respond('426 Connection closed; transfer aborted'); + return; + } + + self.respond('226 Closing data connection'); + if (self.dataSocket) { + self._closeSocket(self.dataSocket); + } + }); + } + + function errorHandler() { + erroredOut = true; + } +}; + +FtpConnection.prototype._command_APPE = function(commandArg) { + var filename = withCwd(this.cwd, commandArg); + + if (this.server.options.useWriteFile) { + this._STOR_usingWriteFile(filename, 'a'); + } else { + this._STOR_usingCreateWriteStream(filename, null, 'a'); + } +}; + +// Specify a username for login +FtpConnection.prototype._command_USER = function(username) { + var self = this; + + if (self.server.options.tlsOnly && !self.secure) { + self.respond( + '530 This server does not permit login over ' + + 'a non-secure connection; ' + + 'connect using FTP-SSL with explicit AUTH TLS'); + } else { + self.emit('command:user', username, + function success() { + self.respond('331 User name okay, need password.'); + }, + function failure() { + self.respond('530 Not logged in.'); + } + ); + } + return this; +}; + +// Specify a password for login +FtpConnection.prototype._command_PASS = function(password) { + var self = this; + + if (self.previousCommand !== 'USER') { + self.respond('503 Bad sequence of commands.'); + } else { + self.emit('command:pass', password, + function success(username, userFsModule) { + function panic(error, method) { + self._logIf(LOG.ERROR, method + ' signaled error ' + util.inspect(error)); + self.respond('421 Service not available, closing control connection.', function() { + self._closeSocket(self.socket, true); + }); + } + function setCwd(cwd) { + function setRoot(root) { + self.root = root; + self.respond('230 User logged in, proceed.'); + } + + self.cwd = cwd; + if (self.server.getRoot.length <= 1) { + setRoot(self.server.getRoot(self)); + } else { + self.server.getRoot(self, function(err, root) { + if (err) { + panic(err, 'getRoot'); + } else { + setRoot(root); + } + }); + } + } + self.username = username; + self.fs = userFsModule || fsModule; + if (self.server.getInitialCwd.length <= 1) { + setCwd(withCwd(self.server.getInitialCwd(self))); + } else { + self.server.getInitialCwd(self, function(err, cwd) { + if (err) { + panic(err, 'getInitialCwd'); + } else { + setCwd(withCwd(cwd)); + } + }); + } + }, + function failure() { + self.respond('530 Not logged in.'); + self.username = null; + } + ); + } + return this; +}; + +FtpConnection.prototype._closeSocket = function(socket, shouldDestroy) { + // TODO: Should we always use destroy() to avoid keeping sockets open longer + // than necessary (and possibly exceeding OS max open sockets)? + if (shouldDestroy || this.server.options.destroySockets) { + // Don't call destroy() more than once. + if (!socket.destroyed) { + socket.destroy(); + } + } else { + // Don't call `end()` more than once. + if (socket.writable) { + socket.end(); + } + } +}; + +module.exports = FtpConnection; diff --git a/lib/FtpServer.js b/lib/FtpServer.js new file mode 100644 index 0000000..2ff206e --- /dev/null +++ b/lib/FtpServer.js @@ -0,0 +1,145 @@ +var net = require('net'); +var util = require('util'); +var events = require('events'); +var FtpConnection = require('./FtpConnection'); +var Constants = require('./Constants'); + +var EventEmitter = events.EventEmitter; + +// Use LOG for brevity. +var LOG = Constants.LOG_LEVELS; + +function FtpServer(host, options) { + var self = this; + EventEmitter.call(self); + + self.host = host; + + self.options = options; + + if (!self.options.maxStatsAtOnce) { + self.options.maxStatsAtOnce = 5; + } + + if (!options.getInitialCwd) { + throw new Error("'getInitialCwd' option of FtpServer must be set"); + } + if (!options.getRoot) { + throw new Error("'getRoot' option of FtpServer must be set"); + } + self.getInitialCwd = options.getInitialCwd; + self.getRoot = options.getRoot; + + self.getUsernameFromUid = options.getUsernameFromUid || function(uid, c) { + c(null, 'ftp'); + }; + self.getGroupFromGid = options.getGroupFromGid || function(gid, c) { + c(null, 'ftp'); + }; + self.debugging = options.logLevel || 0; + self.useWriteFile = options.useWriteFile; + self.useReadFile = options.useReadFile; + self.uploadMaxSlurpSize = options.uploadMaxSlurpSize || 0; + + self.server = net.createServer(); + self.server.on('connection', function(socket) { + self._onConnection(socket); + }); + self.server.on('error', function(err) { + self.emit('error', err); + }); + self.server.on('close', function() { + self.emit('close'); + }); +} +util.inherits(FtpServer, EventEmitter); + +FtpServer.prototype._onConnection = function(socket) { + // build an index for the allowable commands for this server + var allowedCommands = null; + if (this.options.allowedCommands) { + allowedCommands = {}; + this.options.allowedCommands.forEach(function(c) { + allowedCommands[c.trim().toUpperCase()] = true; + }); + } + + var conn = new FtpConnection({ + server: this, + socket: socket, + pasv: null, // passive listener server + 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 + // it specified. (This can therefore be true even if there + // is not currently an open data connection.) + dataConfigured: false, + mode: 'ascii', + filefrom: '', + username: null, + filename: '', + fs: null, + cwd: null, + root: null, + hasQuit: false, + + // State for handling TLS upgrades. + secure: false, + pbszReceived: false, + }); + + 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) { + FtpServer.prototype[fname] = function() { + return this.server[fname].apply(this.server, arguments); + }; +}); + +FtpServer.prototype._logIf = function(verbosity, message, conn) { + if (verbosity > this.debugging) { + return; + } + // TODO: Move this to FtpConnection.prototype._logIf. + var peerAddr = (conn && conn.socket && conn.socket.remoteAddress); + if (peerAddr) { + message = '<' + peerAddr + '> ' + message; + } + if (verbosity === LOG.ERROR) { + message = 'ERROR: ' + message; + } else if (verbosity === LOG.WARN) { + message = 'WARNING: ' + message; + } + console.log(message); + var isError = (verbosity === LOG.ERROR); + if (isError && this.debugging === LOG.TRACE) { + console.trace('Trace follows'); + } +}; + +module.exports = FtpServer; diff --git a/lib/PassiveListener.js b/lib/PassiveListener.js new file mode 100644 index 0000000..943179a --- /dev/null +++ b/lib/PassiveListener.js @@ -0,0 +1,10 @@ +var util = require('util'); +var events = require('events'); +var EventEmitter = events.EventEmitter; + +function PassiveListener() { + EventEmitter.call(this); +} +util.inherits(PassiveListener, EventEmitter); + +module.exports = PassiveListener; diff --git a/lib/ftpd.js b/lib/ftpd.js index 5b17259..e90c7cd 100644 --- a/lib/ftpd.js +++ b/lib/ftpd.js @@ -1,1671 +1,4 @@ -/** - * TODO: - * - Implement Full RFC 959 - * - Implement Full RFC 2228 [PBSZ and PROT implemented already] - * - Implement RFC 3659 - * - * - passive command is for server to determine which port it listens on and report that to the client - * - doesn't necessarily mean it needs to be listening (i guess), but i assume it actually SHOULD be listening - * - it keeps listening for subsequent connections - * - * - what sort of security should i enforce? should i require the same IP for data and control connections? - * - maybe just for milesplit's use? - */ -/* eslint-disable no-octal */ -var net = require('net'); -var util = require('util'); -var events = require('events'); -var pathModule = require('path'); -var fsModule = require('fs'); -var StatMode = require('stat-mode'); -var dateformat = require('dateformat'); +var Constants = require('./Constants'); -var glob = require('./glob'); -var starttls = require('./starttls'); - -var LOG_ERROR = 0; -var LOG_WARN = 1; -var LOG_INFO = 2; -var LOG_DEBUG = 3; -var LOG_TRACE = 4; - -exports.LOG_LEVELS = { - LOG_ERROR: LOG_ERROR, - LOG_WARN: LOG_WARN, - LOG_INFO: LOG_INFO, - LOG_DEBUG: LOG_DEBUG, - LOG_TRACE: LOG_TRACE, -}; - -var EventEmitter = events.EventEmitter; - -function pathEscape(text) { - // Rules for quoting: RFC 959 -> Appendix II -> Directory Commands - // (http://www.w3.org/Protocols/rfc959/A2_DirectoryCommands.html) - // -> Reply Codes -> search for "embedded double-quotes" - text = text.replace(/"/g, '""'); - return text; -} - -function withCwd(cwd, path) { - var firstChar = (path || '').charAt(0); - cwd = cwd || pathModule.sep; - path = path || ''; - if (firstChar === '/' || firstChar === pathModule.sep) { - cwd = pathModule.sep; - } - path = pathModule.join(pathModule.sep, cwd, path); - return path; -} - -// Currently used for stripping options from beginning of argument to LIST and NLST. -function stripOptions(str) { - var IN_SPACE = 0; - var IN_DASH = 1; - var state = IN_SPACE; - for (var i = 0; i < str.length; ++i) { - var c = str.charAt(i); - if (state === IN_SPACE) { - if (c === ' ' || c === '\t') { - - } else if (c === '-') { - state = IN_DASH; - } else { - return str.substr(i); - } - } else if (state === IN_DASH && (c === ' ' || c === '\t')) { - state = IN_SPACE; - } - } - return ''; -} - -function PassiveListener() { - EventEmitter.call(this); -} -util.inherits(PassiveListener, EventEmitter); - -function FtpServer(host, options) { - var self = this; - EventEmitter.call(self); - - self.host = host; - - self.options = options; - - if (!self.options.maxStatsAtOnce) { - self.options.maxStatsAtOnce = 5; - } - - if (!options.getInitialCwd) { - throw new Error("'getInitialCwd' option of FtpServer must be set"); - } - if (!options.getRoot) { - throw new Error("'getRoot' option of FtpServer must be set"); - } - self.getInitialCwd = options.getInitialCwd; - self.getRoot = options.getRoot; - - self.getUsernameFromUid = options.getUsernameFromUid || function(uid, c) { - c(null, 'ftp'); - }; - self.getGroupFromGid = options.getGroupFromGid || function(gid, c) { - c(null, 'ftp'); - }; - self.debugging = options.logLevel || 0; - self.useWriteFile = options.useWriteFile; - self.useReadFile = options.useReadFile; - self.uploadMaxSlurpSize = options.uploadMaxSlurpSize || 0; - - self.server = net.createServer(); - self.server.on('connection', function(socket) { - self._onConnection(socket); - }); - self.server.on('error', function(err) { - self.emit('error', err); - }); - self.server.on('close', function() { - self.emit('close'); - }); -} -util.inherits(FtpServer, EventEmitter); - -FtpServer.prototype._onConnection = function(socket) { - // build an index for the allowable commands for this server - var allowedCommands = null; - if (this.options.allowedCommands) { - allowedCommands = {}; - this.options.allowedCommands.forEach(function(c) { - allowedCommands[c.trim().toUpperCase()] = true; - }); - } - - var conn = new FtpConnection({ - server: this, - socket: socket, - pasv: null, // passive listener server - 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 - // it specified. (This can therefore be true even if there - // is not currently an open data connection.) - dataConfigured: false, - mode: 'ascii', - filefrom: '', - username: null, - filename: '', - fs: null, - cwd: null, - root: null, - hasQuit: false, - - // State for handling TLS upgrades. - secure: false, - pbszReceived: false, - }); - - 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) { - FtpServer.prototype[fname] = function() { - return this.server[fname].apply(this.server, arguments); - }; -}); - -FtpServer.prototype._logIf = function(verbosity, message, conn) { - if (verbosity > this.debugging) { - return; - } - // TODO: Move this to FtpConnection.prototype._logIf. - var peerAddr = (conn && conn.socket && conn.socket.remoteAddress); - if (peerAddr) { - message = '<' + peerAddr + '> ' + message; - } - if (verbosity === LOG_ERROR) { - message = 'ERROR: ' + message; - } else if (verbosity === LOG_WARN) { - message = 'WARNING: ' + message; - } - console.log(message); - var isError = (verbosity === LOG_ERROR); - if (isError && this.debugging === LOG_TRACE) { - console.trace('Trace follows'); - } -}; - -function FtpConnection(properties) { - EventEmitter.call(this); - var self = this; - Object.keys(properties).forEach(function(key) { - self[key] = properties[key]; - }); -} -util.inherits(FtpConnection, EventEmitter); - -// TODO: rename this to writeLine? -FtpConnection.prototype.respond = function(message, callback) { - return this._writeText(this.socket, message + '\r\n', callback); -}; - -FtpConnection.prototype._logIf = function(verbosity, message) { - return this.server._logIf(verbosity, message, this); -}; - -// We don't want to use setEncoding because it screws up TLS, but we -// also don't want to explicitly specify ASCII encoding for every call to 'write' -// with a string argument. -FtpConnection.prototype._writeText = function(socket, data, callback) { - if (!socket.writable) { - this._logIf(LOG_DEBUG, 'Attempted writing to a closed socket:\n>> ' + data.trim()); - return; - } - this._logIf(LOG_TRACE, '>> ' + data.trim()); - return socket.write(data, 'utf8', callback); -}; - -FtpConnection.prototype._authenticated = function() { - return !!this.username; -}; - -FtpConnection.prototype._closeDataConnections = function() { - if (this.dataSocket) { - // TODO: should the second arg be false here? - this._closeSocket(this.dataSocket, true); - this.dataSocket = null; - } - if (this.pasv) { - this.pasv.close(); - this.pasv = null; - } -}; - -FtpConnection.prototype._createPassiveServer = function() { - var self = this; - - 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'); - - if (self.secure) { - self._logIf(LOG_INFO, 'Upgrading passive connection to TLS'); - starttls.starttlsServer(psocket, self.server.options.tlsOptions, function(err, cleartext) { - if (err) { - 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, 'Allowing unauthorized passive connection (allowUnauthorizedTls is on)'); - switchToSecure(); - } else { - self._logIf(LOG_INFO, 'Closing unauthorized 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'); - // TODO: Check for existing dataSocket. - self.dataSocket = cleartext; - setupPassiveListener(); - } - }); - } else { - // TODO: Check for existing dataSocket. - self.dataSocket = psocket; - setupPassiveListener(); - } - - function setupPassiveListener() { - if (self.dataListener) { - self.dataListener.emit('ready'); - } else { - self._logIf(LOG_WARN, 'Passive connection initiated, but no data listener'); - } - - // 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')); - function allOver(ename) { - return function(err) { - self._logIf( - (err ? LOG_ERROR : LOG_DEBUG), - 'Passive data event: ' + ename + (err ? ' due to error' : '') - ); - self.dataSocket = null; - }; - } - - self.dataSocket.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; - }); - } - }); -}; - -FtpConnection.prototype._whenDataReady = function(callback) { - var self = this; - - if (self.dataListener) { - // 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.dataListener.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); - }); - } - } -}; - -FtpConnection.prototype._initiateData = function(callback) { - var self = this; - - if (self.dataSocket) { - return callback(self.dataSocket); - } - - 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) : '') - ); - } - - 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) { - this._logIf(LOG_ERROR, 'Client connection error: ' + util.inspect(err)); - this._closeSocket(this.socket, true); -}; - -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. - if (this.dataSocket) { - this._closeSocket(this.dataSocket, hadError); - this.dataSocket = null; - } - 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'); -}; - -// Whitelist of commands which don't require authentication. -// All other commands sent by unauthorized users will be rejected by default. -var DOES_NOT_REQUIRE_AUTH = { }; -[ - 'AUTH', 'FEAT', 'NOOP', 'PASS', 'PBSZ', 'PROT', 'QUIT', - 'TYPE', 'SYST', 'USER', -].forEach(function(c) { - DOES_NOT_REQUIRE_AUTH[c] = true; -}); - -// Commands which can't be issued until a PASV/PORT command has been sent -// without an intervening data connection error. -var REQUIRES_CONFIGURED_DATA = { }; -[ - 'LIST', 'NLST', 'RETR', 'STOR', -].forEach(function(c) { - REQUIRES_CONFIGURED_DATA[c] = true; -}); - -FtpConnection.prototype._onData = function(data) { - var self = this; - - if (self.hasQuit) { - return; - } - - data = data.toString('utf-8').trim(); - self._logIf(LOG_TRACE, '<< ' + data); - // Don't want to include passwords in logs. - self._logIf(LOG_INFO, 'FTP command: ' + - data.replace(/^PASS [\s\S]*$/i, 'PASS ***') - ); - - var command; - var commandArg; - var index = data.indexOf(' '); - if (index !== -1) { - var parts = data.split(' '); - command = parts.shift().toUpperCase(); - commandArg = parts.join(' ').trim(); - } else { - command = data.toUpperCase(); - commandArg = ''; - } - - var m = '_command_' + command; - if (self[m]) { - if (self.allowedCommands != null && self.allowedCommands[command] !== true) { - self.respond('502 ' + command + ' not implemented.'); - } else if (DOES_NOT_REQUIRE_AUTH[command]) { - self[m](commandArg, command); - } else { - // If 'tlsOnly' option is set, all commands which require user authentication will only - // 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()) { - checkData(); - } else { - self.respond('530 Not logged in.'); - } - } - - function checkData() { - if (REQUIRES_CONFIGURED_DATA[command] && !self.dataConfigured) { - self.respond('425 Data connection not configured; send PASV or PORT'); - return; - } - - self[m](commandArg, command); - } - } else { - self.respond('502 Command not implemented.'); - } - self.previousCommand = command; -}; - -/** - * Specify the user's account (superfluous) - * @return {FtpConnection} this - */ -FtpConnection.prototype._command_ACCT = function() { - this.respond('202 Command not implemented, superfluous at this site.'); - return this; -}; - -/** - * Allocate storage space (superfluous) - * @return {FtpConnection} this - */ -FtpConnection.prototype._command_ALLO = function() { - this.respond('202 Command not implemented, superfluous at this site.'); - return this; -}; - -FtpConnection.prototype._command_AUTH = function(commandArg) { - var self = this; - - if (!self.server.options.tlsOptions || commandArg !== 'TLS') { - return self.respond('502 Command not implemented'); - } - - self.respond('234 Honored', function() { - self._logIf(LOG_INFO, 'Establishing secure connection...'); - starttls.starttlsServer(self.socket, self.server.options.tlsOptions, function(err, cleartext) { - 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(); - } - - function switchToSecure() { - self._logIf(LOG_INFO, 'Secure connection started'); - self.socket = cleartext; - self.socket.on('data', function(data) { - self._onData(data); - }); - self.secure = true; - } - }); - }); -}; - -/** - * Change working directory to parent directory - * @return {FtpConnection} this - */ -FtpConnection.prototype._command_CDUP = function() { - var pathServer = pathModule.dirname(this.cwd); - var pathEscaped = pathEscape(pathServer); - this.cwd = pathServer; - this.respond('250 Directory changed to "' + pathEscaped + '"'); - return this; -}; - -/** - * Change working directory - * @param {string} pathRequest - * @return {FtpConnection} this - */ -FtpConnection.prototype._command_CWD = function(pathRequest) { - var pathServer = withCwd(this.cwd, pathRequest); - var pathFs = pathModule.join(this.root, pathServer); - var pathEscaped = pathEscape(pathServer); - this.fs.stat(pathFs, function(err, stats) { - if (err) { - this._logIf(LOG_ERROR, 'CWD ' + pathRequest + ': ' + err); - this.respond('550 Directory not found.'); - } else if (!stats.isDirectory()) { - this._logIf(LOG_WARN, 'Attempt to CWD to non-directory'); - this.respond('550 Not a directory'); - } else { - this.cwd = pathServer; - 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) { - if (err) { - self._logIf(LOG_ERROR, 'Error deleting file: ' + filename + ', ' + err); - // write error to socket - self.respond('550 Permission denied'); - } else { - self.respond('250 File deleted'); - } - }); -}; - -FtpConnection.prototype._command_FEAT = function() { - // Get the feature list implemented by the server. (RFC 2389) - this.respond( - '211-Features\r\n' + - ' SIZE\r\n' + - ' UTF8\r\n' + - ' MDTM\r\n' + - (!this.server.options.tlsOptions ? '' : - ' AUTH TLS\r\n' + - ' PBSZ\r\n' + - ' UTF8\r\n' + - ' PROT\r\n' - ) + - '211 end' - ); -}; - -FtpConnection.prototype._command_OPTS = function(commandArg) { - // http://tools.ietf.org/html/rfc2389#section-4 - if (commandArg.toUpperCase() === 'UTF8 ON') { - this.respond('200 OK'); - } else { - this.respond('451 Not supported'); - } -}; - -/** - * Print the file modification time - * @param {string} file - * @return {FtpConnection} this - */ -FtpConnection.prototype._command_MDTM = function(file) { - file = withCwd(this.cwd, file); - file = pathModule.join(this.root, file); - this.fs.stat(file, function(err, stats) { - if (err) { - this.respond('550 File unavailable'); - } else { - this.respond('213 ' + dateformat(stats.mtime, 'yyyymmddhhMMss')); - } - }.bind(this)); - return this; -}; - -FtpConnection.prototype._command_LIST = function(commandArg) { - this._LIST(commandArg, true/*detailed*/, 'LIST'); -}; -FtpConnection.prototype._command_NLST = function(commandArg) { - this._LIST(commandArg, false/*!detailed*/, 'NLST'); -}; - -FtpConnection.prototype._command_STAT = function(commandArg) { - if (commandArg) { - this._LIST(commandArg, true/*detailed*/, 'STAT'); - } else { - this.respond('211 FTP Server Status OK'); - } -}; - -FtpConnection.prototype._LIST = function(commandArg, detailed, cmd) { - /* - Normally the server responds with a mark using code 150. It then stops accepting new connections, attempts to send the contents of the directory over the data connection, and closes the data connection. Finally it - - accepts the LIST or NLST request with code 226 if the entire directory was successfully transmitted; - rejects the LIST or NLST request with code 425 if no TCP connection was established; - rejects the LIST or NLST request with code 426 if the TCP connection was established but then broken by the client or by network failure; or - rejects the LIST or NLST request with code 451 if the server had trouble reading the directory from disk. - - The server may reject the LIST or NLST request (with code 450 or 550) without first responding with a mark. In this case the server does not touch the data connection. - */ - - var self = this; - - // LIST may be passed options (-a in particular). We just ignore any of these. - // (In the particular case of -a, we show hidden files anyway.) - var dirname = stripOptions(commandArg); - var dir = withCwd(self.cwd, dirname); - - glob.setMaxStatsAtOnce(self.server.options.maxStatsAtOnce); - glob.glob(pathModule.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'); - return; - } - - if (self.server.options.hideDotFiles) { - files = files.filter(function(file) { - if (file.name && file.name[0] !== '.') { - return true; - } - }); - } - - self._logIf(LOG_INFO, 'Directory has ' + files.length + ' files'); - if (files.length === 0) { - return self._listFiles([], detailed, cmd); - } - - var fileInfos; // To contain list of files with info for each. - - if (!detailed) { - // We're not doing a detailed listing, so we don't need to get username - // and group name. - fileInfos = files; - return finished(); - } - - // 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); - } - j = --i; - - function handleFile(ii) { - if (i >= files.length) { - return i === files.length + j ? finished() : null; - } - - self.server.getUsernameFromUid(files[ii].stats.uid, function(e1, uname) { - self.server.getGroupFromGid(files[ii].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, - }); - } - handleFile(++i); - }); - }); - } - - function finished() { - // Sort file names. - if (!self.server.options.dontSortFilenames) { - if (self.server.options.filenameSortMap !== false) { - var sm = ( - self.server.options.filenameSortMap || - function(x) { - return x.toUpperCase(); - } - ); - for (var i = 0; i < fileInfos.length; ++i) { - fileInfos[i]._s = sm(detailed ? fileInfos[i].file.name : fileInfos[i].name); - } - } - - var sf = (self.server.options.filenameSortFunc || - function(x, y) { - return x.localeCompare(y); - }); - fileInfos = fileInfos.sort(function(x, y) { - if (self.server.options.filenameSortMap !== false) { - return sf(x._s, y._s); - } else if (detailed) { - return sf(x.file.name, y.file.name); - } else { - return sf(x.name, y.name); - } - }); - } - - self._listFiles(fileInfos, detailed, cmd); - } - }, self.server.options.noWildcards); -}; - -function leftPad(text, width) { - var out = ''; - for (var j = text.length; j < width; j++) { - out += ' '; - } - out += text; - return out; -} - -FtpConnection.prototype._listFiles = function(fileInfos, detailed, cmd) { - var self = this; - - var m = '150 Here comes the directory listing'; - var BEGIN_MSGS = { - LIST: m, NLST: m, STAT: '213-Status follows', - }; - m = '226 Transfer OK'; - var END_MSGS = { - LIST: m, NLST: m, STAT: '213 End of status', - }; - - self.respond(BEGIN_MSGS[cmd], function() { - if (cmd === 'STAT') { - whenReady(self.socket); - } else { - self._whenDataReady(whenReady); - } - - function whenReady(listconn) { - if (fileInfos.length === 0) { - return success(); - } - - function success(err) { - if (err) { - self.respond('550 Error listing files'); - } else { - self.respond(END_MSGS[cmd]); - } - if (cmd !== 'STAT') { - self._closeSocket(listconn); - } - } - - self._logIf(LOG_DEBUG, 'Sending file list'); - - for (var i = 0; i < fileInfos.length; ++i) { - var fileInfo = fileInfos[i]; - - var line = ''; - var file; - - if (!detailed) { - file = fileInfo; - line += file.name + '\r\n'; - } else { - file = fileInfo.file; - var s = file.stats; - var allModes = (new StatMode({mode: s.mode})).toString(); - var rwxModes = allModes.substr(1, 9); - line += (s.isDirectory() ? 'd' : '-') + rwxModes; - // ^-- Clients don't need to know about special files and pipes - line += ' 1 ' + - (fileInfo.uname || 'ftp') + ' ' + - (fileInfo.gname === null ? 'ftp' : fileInfo.gname) + ' '; - 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'; - } - self._writeText( - listconn, - line, - (i === fileInfos.length - 1 ? success : undefined) - ); - } - } - }); -}; - -/** - * Create a directory - * @param {string} pathRequest - * @return {FtpConnection} this - */ -FtpConnection.prototype._command_MKD = function(pathRequest) { - var pathServer = withCwd(this.cwd, pathRequest); - var pathEscaped = pathEscape(pathServer); - var pathFs = pathModule.join(this.root, pathServer); - this.fs.mkdir(pathFs, 0755, function(err) { - if (err) { - this._logIf(LOG_ERROR, 'MKD ' + pathRequest + ': ' + err); - this.respond('550 "' + pathEscaped + '" directory NOT created'); - } else { - this.respond('257 "' + pathEscaped + '" directory created'); - } - }.bind(this)); - return this; -}; - -/** - * Perform a no-op (used to keep-alive connection) - * @return {FtpConnection} this - */ -FtpConnection.prototype._command_NOOP = function() { - this.respond('200 OK'); - return this; -}; - -FtpConnection.prototype._command_PORT = function(x, y) { - this._PORT(x, y); -}; -FtpConnection.prototype._command_EPRT = function(x, y) { - this._PORT(x, y); -}; -FtpConnection.prototype._PORT = function(commandArg, command) { - var self = this; - var m; - - self.dataConfigured = false; - - 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})$/); - if (!m) { - self.respond('501 Bad argument to PORT'); - return; - } - - var host = m[1] + '.' + m[2] + '.' + m[3] + '.' + m[4]; - var port = (parseInt(m[5], 10) << 8) + parseInt(m[6], 10); - if (isNaN(port)) { - // The value should never be NaN because the relevant groups in the regex matche 1-3 digits. - throw new Error('Impossible NaN in FtpConnection.prototype._PORT'); - } - } else { // EPRT - if (commandArg.length >= 3 && commandArg.charAt(0) === '|' && - commandArg.charAt(2) === '|' && commandArg.charAt(1) === '2') { - // Only IPv4 is supported. - self.respond('522 Server cannot handle IPv6 EPRT commands, use (1)'); - return; - } - - m = commandArg.match(/^\|1\|([0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3})\|([0-9]{1,5})/); - if (!m) { - self.respond('501 Bad Argument to EPRT'); - return; - } - - var r = parseInt(m[2], 10); - if (isNaN(r)) { - // The value should never be NaN because the relevant group in the regex matches 1-5 digits. - throw new Error('Impossible NaN in FtpConnection.prototype._PORT (2)'); - } - if (r > 65535 || r <= 0) { - self.respond('501 Bad argument to EPRT (invalid port number)'); - return; - } - - host = m[1]; - port = r; - } - - self.dataConfigured = true; - self.dataHost = host; - self.dataPort = port; - self._logIf(LOG_DEBUG, 'self.dataHost, self.dataPort set to ' + self.dataHost + ':' + self.dataPort); - self.respond('200 OK'); -}; - -FtpConnection.prototype._command_PASV = function(x, y) { - this._PASV(x, y); -}; -FtpConnection.prototype._command_EPSV = function(x, y) { - this._PASV(x, y); -}; -FtpConnection.prototype._PASV = function(commandArg, command) { - var self = this; - - self.dataConfigured = false; - - if (command === 'EPSV' && commandArg && commandArg !== '1') { - self.respond('202 Not supported'); - return; - } - - // not sure whether the spec limits to 1 data connection at a time ... - if (self.dataSocket) { - self._closeSocket(self.dataSocket, true); - } - - if (self.dataListener) { - 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); - } - - self.dataConfigured = true; -}; - -FtpConnection.prototype._writePASVReady = function(command) { - var self = this; - - var a = self.pasv.address(); - var host = self.server.host; - var port = a.port; - if (command === 'PASV') { - var i1 = (port / 256) | 0; - var i2 = port % 256; - self.respond('227 Entering Passive Mode (' + host.split('.').join(',') + ',' + i1 + ',' + i2 + ')'); - } else { // EPASV - self.respond('229 Entering Extended Passive Mode (|||' + port + '|)'); - } -}; - -FtpConnection.prototype._setupNewPASV = function(commandArg, command) { - var self = this; - - var pasv = self._createPassiveServer(); - var portRangeErrorHandler; - - function normalErrorHandler(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.dataListener = null; - self.dataSocket = null; - 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); - } - - // 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); - } - - 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'); - }); -}; - -FtpConnection.prototype._command_PBSZ = function(commandArg) { - var self = this; - - if (!self.server.options.tlsOptions) { - return self.respond('202 Not supported'); - } - - // Protection Buffer Size (RFC 2228) - if (!self.secure) { - self.respond('503 Secure connection not established'); - } else if (parseInt(commandArg, 10) !== 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'); - } -}; - -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'); - } -}; - -/** - * Print the current working directory. - * @param {string} commandArg must always be empty - * @return {FtpConnection} this - */ -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; -}; - -FtpConnection.prototype._command_QUIT = function() { - var self = this; - - self.hasQuit = true; - self.respond('221 Goodbye', function(err) { - if (err) { - self._logIf(LOG_ERROR, "Error writing 'Goodbye' message following QUIT"); - } - self._closeSocket(self.socket, true); - self._closeDataConnections(); - }); -}; - -FtpConnection.prototype._command_RETR = function(commandArg) { - var filename = pathModule.join(this.root, withCwd(this.cwd, commandArg)); - - if (this.server.options.useReadFile) { - this._RETR_usingReadFile(commandArg, filename); - } else { - this._RETR_usingCreateReadStream(commandArg, filename); - } -}; - -FtpConnection.prototype._RETR_usingCreateReadStream = function(commandArg, filename) { - var self = this; - var startTime = new Date(); - - self.emit('file:retr', 'open', { - user: self.username, - file: filename, - sTime: startTime, - }); - - function afterOk(callback) { - self.respond('150 Opening ' + self.mode.toUpperCase() + ' mode data connection', callback); - } - - - self.fs.open(filename, 'r', function(err, fd) { - if (err) { - self.emit('file:retr', 'error', { - user: self.username, - file: filename, - filesize: 0, - sTime: startTime, - eTime: new Date(), - duration: new Date() - startTime, - errorState: true, - error: err, - }); - if (err.code === 'ENOENT') { - self.respond('550 Not Found'); - } else { // Who knows what's going on here... - self.respond('550 Not Accessible'); - self._logIf(LOG_ERROR, "Error at read of '" + filename + "' other than ENOENT " + err); - } - } else { - afterOk(function() { - self._whenDataReady(function(pasvconn) { - var readLength = 0; - var now = new Date(); - var rs = self.fs.createReadStream(null, {fd: fd}); - rs.pause(); - rs.once('error', function(err) { - self.emit('file:retr', 'close', { - user: self.username, - file: filename, - filesize: 0, - sTime: startTime, - eTime: now, - duration: now - startTime, - errorState: true, - error: err, - }); - }); - - rs.on('data', function(buffer) { - readLength += buffer.length; - }); - - rs.on('end', function() { - var now = new Date(); - self.emit('file:retr', 'close', { - user: self.username, - file: filename, - filesize: 0, - sTime: startTime, - eTime: now, - duration: now - startTime, - errorState: false, - }); - self.respond('226 Closing data connection, sent ' + readLength + ' bytes'); - }); - - rs.pipe(pasvconn); - rs.resume(); - }); - }); - } - }); -}; - -FtpConnection.prototype._RETR_usingReadFile = function(commandArg, filename) { - var self = this; - var startTime = new Date(); - - self.emit('file:retr', 'open', { - user: self.username, - file: filename, - sTime: startTime, - }); - - function afterOk(callback) { - self.respond('150 Opening ' + self.mode.toUpperCase() + ' mode data connection', callback); - } - - self.fs.readFile(filename, function(err, contents) { - if (err) { - self.emit('file:retr', 'error', { - user: self.username, - file: filename, - filesize: 0, - sTime: startTime, - eTime: new Date(), - duration: new Date() - startTime, - errorState: true, - error: err, - }); - if (err.code === 'ENOENT') { - self.respond('550 Not Found'); - } else { // Who knows what's going on here... - self.respond('550 Not Accessible'); - self._logIf(LOG_ERROR, "Error at read of '" + filename + "' other than ENOENT " + err); - } - } else { - afterOk(function() { - self._whenDataReady(function(pasvconn) { - contents = {filename: filename, data: contents}; - self.emit('file:retr:contents', contents); - contents = contents.data; - pasvconn.write(contents); - var contentLength = contents.length; - self.respond('226 Closing data connection, sent ' + contentLength + ' bytes'); - self.emit('file:retr', 'close', { - user: self.username, - file: filename, - filesize: contentLength, - sTime: startTime, - eTime: new Date(), - duration: new Date() - startTime, - errorState: false, - }); - self._closeSocket(pasvconn); - }); - }); - } - }); -}; - -/** - * Remove a directory - * @param {string} pathRequest - * @return {FtpConnection} this - */ -FtpConnection.prototype._command_RMD = function(pathRequest) { - var pathServer = withCwd(this.cwd, pathRequest); - var pathFs = pathModule.join(this.root, pathServer); - this.fs.rmdir(pathFs, function(err) { - if (err) { - this._logIf(LOG_ERROR, 'RMD ' + pathRequest + ': ' + err); - this.respond('550 Delete operation failed'); - } else { - this.respond('250 "' + pathServer + '" directory removed'); - } - }.bind(this)); - return this; -}; - -FtpConnection.prototype._command_RNFR = function(commandArg) { - var self = this; - self.filefrom = withCwd(self.cwd, commandArg); - self._logIf(LOG_DEBUG, 'Rename from ' + self.filefrom); - self.respond('350 Ready for destination name'); -}; - -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) { - 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' : '')); - } else { - self.respond('250 File renamed successfully'); - } - }); -}; - -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) { - if (err) { - self._logIf(LOG_ERROR, "Error getting size of file '" + filename + "' "); - self.respond('450 Failed to get size of file'); - return; - } - self.respond('213 ' + s.size + ''); - }); -}; - -FtpConnection.prototype._command_TYPE = function(commandArg) { - if (commandArg === 'I' || commandArg === 'A') { - this.respond('200 OK'); - } else { - this.respond('202 Not supported'); - } -}; - -FtpConnection.prototype._command_SYST = function() { - this.respond('215 UNIX Type: I'); -}; - -FtpConnection.prototype._command_STOR = function(commandArg) { - var filename = withCwd(this.cwd, commandArg); - - if (this.server.options.useWriteFile) { - this._STOR_usingWriteFile(filename, 'w'); - } else { - this._STOR_usingCreateWriteStream(filename, null, 'w'); - } -}; - -// 'initialBuffers' argument is set when this is called from _STOR_usingWriteFile. -FtpConnection.prototype._STOR_usingCreateWriteStream = function(filename, initialBuffers, flag) { - var self = this; - - var wStreamFlags = {flags: flag || 'w', mode: 0644}; - var storeStream = self.fs.createWriteStream(pathModule.join(self.root, filename), wStreamFlags); - var notErr = true; - // Adding for event metadata for file upload (STOR) - var startTime = new Date(); - var uploadSize = 0; - - if (initialBuffers) { - //todo: handle back-pressure - initialBuffers.forEach(function(b) { - storeStream.write(b); - }); - } - - self._whenDataReady(handleUpload); - - storeStream.on('open', function() { - self._logIf(LOG_DEBUG, 'File opened/created: ' + filename); - self._logIf(LOG_DEBUG, 'Told client ok to send file data'); - // Adding event emitter for upload start time - self.emit('file:stor', 'open', { - user: self.username, - file: filename, - time: startTime, - }); - - self.respond('150 Ok to send data'); - }); - - storeStream.on('error', function() { - self.emit('file:stor', 'error', { - user: self.username, - file: filename, - filesize: uploadSize, - sTime: startTime, - eTime: new Date(), - duration: new Date() - startTime, - errorState: !notErr, - }); - storeStream.end(); - notErr = false; - if (self.dataSocket) { - self._closeSocket(self.dataSocket, true); - } - self.respond('426 Connection closed; transfer aborted'); - }); - - storeStream.on('finish', function() { - // Adding event emitter for completed upload. - self.emit('file:stor', 'close', { - user: self.username, - file: filename, - filesize: uploadSize, - sTime: startTime, - eTime: new Date(), - duration: new Date() - startTime, - errorState: !notErr, - }); - notErr ? self.respond('226 Closing data connection') : true; - if (self.dataSocket) { - self._closeSocket(self.dataSocket); - } - }); - - function handleUpload(dataSocket) { - var isPaused = false; - dataSocket.on('data', function(buff) { - var result = storeStream.write(buff); - // Handle back-pressure - if (result === false) { - dataSocket.pause(); - isPaused = true; - storeStream.once('drain', function() { - dataSocket.resume(); - isPaused = false; - }); - } - }); - dataSocket.once('error', function() { - notErr = false; - storeStream.end(); - }); - dataSocket.once('finish', function() { - if (isPaused) { - storeStream.once('drain', function() { - storeStream.end(); - }); - } else { - storeStream.end(); - } - }); - } -}; - -FtpConnection.prototype._STOR_usingWriteFile = function(filename, flag) { - var self = this; - - var erroredOut = false; - var slurpBuf = new Buffer(1024); - var totalBytes = 0; - var startTime = new Date(); - - self.emit('file:stor', 'open', { - user: self.username, - file: filename, - time: startTime, - }); - - self.respond('150 Ok to send data', function() { - self._whenDataReady(handleUpload); - }); - - 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 && - totalBytes + buf.length > self.server.options.uploadMaxSlurpSize) { - // Give up trying to slurp it -- it's too big. - - // If the 'fs' module we've been given doesn't implement 'createWriteStream', then - // we give up and send the client an error. - if (!self.fs.createWriteStream) { - if (self.dataSocket) { - self._closeSocket(self.dataSocket, true); - } - self.respond('552 Requested file action aborted; file too big'); - return; - } - - // Otherwise, we call _STOR_usingWriteStream, and tell it to prepend the stuff - // that we've buffered so far to the file. - self._logIf(LOG_WARN, 'uploadMaxSlurpSize exceeded; falling back to createWriteStream'); - self._STOR_usingCreateWriteStream(filename, [slurpBuf.slice(0, totalBytes), buf]); - self.dataSocket.removeListener('data', dataHandler); - self.dataSocket.removeListener('error', errorHandler); - self.dataSocket.removeListener('close', closeHandler); - } else { - if (totalBytes + buf.length > slurpBuf.length) { - var newLength = slurpBuf.length * 2; - if (newLength < totalBytes + buf.length) { - newLength = totalBytes + buf.length; - } - - var newSlurpBuf = new Buffer(newLength); - slurpBuf.copy(newSlurpBuf, 0, 0, totalBytes); - slurpBuf = newSlurpBuf; - } - buf.copy(slurpBuf, totalBytes, 0, buf.length); - totalBytes += buf.length; - } - } - - function closeHandler() { - if (erroredOut) { - return; - } - - 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.emit('file:stor', 'close', { - user: self.username, - file: filename, - filesize: totalBytes, - sTime: startTime, - eTime: new Date(), - duration: new Date() - startTime, - errorState: err ? true : false, - }); - if (err) { - erroredOut = true; - self._logIf(LOG_ERROR, 'Error writing file. ' + err); - if (self.dataSocket) { - self._closeSocket(self.dataSocket, true); - } - self.respond('426 Connection closed; transfer aborted'); - return; - } - - self.respond('226 Closing data connection'); - if (self.dataSocket) { - self._closeSocket(self.dataSocket); - } - }); - } - - function errorHandler() { - erroredOut = true; - } -}; - -FtpConnection.prototype._command_APPE = function(commandArg) { - var filename = withCwd(this.cwd, commandArg); - - if (this.server.options.useWriteFile) { - this._STOR_usingWriteFile(filename, 'a'); - } else { - this._STOR_usingCreateWriteStream(filename, null, 'a'); - } -}; - -/** - * Specify a username for login - * @param {string} username - * @return {FtpConnection} this - */ -FtpConnection.prototype._command_USER = function(username) { - var self = this; - - if (self.server.options.tlsOnly && !self.secure) { - self.respond( - '530 This server does not permit login over ' + - 'a non-secure connection; ' + - 'connect using FTP-SSL with explicit AUTH TLS'); - } else { - self.emit('command:user', username, - function success() { - self.respond('331 User name okay, need password.'); - }, - function failure() { - self.respond('530 Not logged in.'); - } - ); - } - return this; -}; - -/** - * Specify a password for login - * @param {string} password - * @return {FtpConnection} this - */ -FtpConnection.prototype._command_PASS = function(password) { - var self = this; - - if (self.previousCommand !== 'USER') { - self.respond('503 Bad sequence of commands.'); - } else { - self.emit('command:pass', password, - function success(username, userFsModule) { - function panic(error, method) { - self._logIf(LOG_ERROR, method + ' signaled error ' + util.inspect(error)); - self.respond('421 Service not available, closing control connection.', function() { - self._closeSocket(self.socket, true); - }); - } - function setCwd(cwd) { - function setRoot(root) { - self.root = root; - self.respond('230 User logged in, proceed.'); - } - - self.cwd = cwd; - if (self.server.getRoot.length <= 1) { - setRoot(self.server.getRoot(self)); - } else { - self.server.getRoot(self, function(err, root) { - if (err) { - panic(err, 'getRoot'); - } else { - setRoot(root); - } - }); - } - } - self.username = username; - self.fs = userFsModule || fsModule; - if (self.server.getInitialCwd.length <= 1) { - setCwd(withCwd(self.server.getInitialCwd(self))); - } else { - self.server.getInitialCwd(self, function(err, cwd) { - if (err) { - panic(err, 'getInitialCwd'); - } else { - setCwd(withCwd(cwd)); - } - }); - } - }, - function failure() { - self.respond('530 Not logged in.'); - self.username = null; - } - ); - } - return this; -}; - -FtpConnection.prototype._closeSocket = function(socket, shouldDestroy) { - // TODO: Should we always use destroy() to avoid keeping sockets open longer - // than necessary (and possibly exceeding OS max open sockets)? - if (shouldDestroy || this.server.options.destroySockets) { - // Don't call destroy() more than once. - if (!socket.destroyed) { - socket.destroy(); - } - } else { - // Don't call `end()` more than once. - if (socket.writable) { - socket.end(); - } - } -}; - -exports.FtpServer = FtpServer; +exports.FtpServer = require('./FtpServer'); +exports.LOG_LEVELS = Constants.LOG_LEVELS; diff --git a/lib/helpers/leftPad.js b/lib/helpers/leftPad.js new file mode 100644 index 0000000..6c6e5b9 --- /dev/null +++ b/lib/helpers/leftPad.js @@ -0,0 +1,10 @@ +function leftPad(text, width) { + var out = ''; + for (var j = text.length; j < width; j++) { + out += ' '; + } + out += text; + return out; +} + +module.exports = leftPad; diff --git a/lib/helpers/pathEscape.js b/lib/helpers/pathEscape.js new file mode 100644 index 0000000..725265e --- /dev/null +++ b/lib/helpers/pathEscape.js @@ -0,0 +1,9 @@ +function pathEscape(text) { + // Rules for quoting: RFC 959 -> Appendix II -> Directory Commands + // (http://www.w3.org/Protocols/rfc959/A2_DirectoryCommands.html) + // -> Reply Codes -> search for "embedded double-quotes" + text = text.replace(/"/g, '""'); + return text; +} + +module.exports = pathEscape; diff --git a/lib/helpers/stripOptions.js b/lib/helpers/stripOptions.js new file mode 100644 index 0000000..229f809 --- /dev/null +++ b/lib/helpers/stripOptions.js @@ -0,0 +1,23 @@ +// Currently used for stripping options from beginning of argument to LIST and NLST. +function stripOptions(str) { + var IN_SPACE = 0; + var IN_DASH = 1; + var state = IN_SPACE; + for (var i = 0; i < str.length; ++i) { + var c = str.charAt(i); + if (state === IN_SPACE) { + if (c === ' ' || c === '\t') { + + } else if (c === '-') { + state = IN_DASH; + } else { + return str.substr(i); + } + } else if (state === IN_DASH && (c === ' ' || c === '\t')) { + state = IN_SPACE; + } + } + return ''; +} + +module.exports = stripOptions; diff --git a/lib/helpers/withCwd.js b/lib/helpers/withCwd.js new file mode 100644 index 0000000..3eed41c --- /dev/null +++ b/lib/helpers/withCwd.js @@ -0,0 +1,14 @@ +var pathModule = 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; + } + path = pathModule.join(pathModule.sep, cwd, path); + return path; +} + +module.exports = withCwd; From ab825447dce457a57d32f7f30ed7d69523de15ef Mon Sep 17 00:00:00 2001 From: Simon Sturmer Date: Sat, 16 Jan 2016 18:44:06 +0800 Subject: [PATCH 03/55] ftpd.js is now just a stub and should not be in lib --- ftpd.js | 4 ++++ lib/ftpd.js | 4 ---- package.json | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) create mode 100644 ftpd.js delete mode 100644 lib/ftpd.js diff --git a/ftpd.js b/ftpd.js new file mode 100644 index 0000000..b582826 --- /dev/null +++ b/ftpd.js @@ -0,0 +1,4 @@ +var Constants = require('./lib/Constants'); + +exports.FtpServer = require('./lib/FtpServer'); +exports.LOG_LEVELS = Constants.LOG_LEVELS; diff --git a/lib/ftpd.js b/lib/ftpd.js deleted file mode 100644 index e90c7cd..0000000 --- a/lib/ftpd.js +++ /dev/null @@ -1,4 +0,0 @@ -var Constants = require('./Constants'); - -exports.FtpServer = require('./FtpServer'); -exports.LOG_LEVELS = Constants.LOG_LEVELS; diff --git a/package.json b/package.json index 131b276..2332189 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "ftpd", "version": "0.2.14", "description": "Node FTP Server", - "main": "./lib/ftpd.js", + "main": "./ftpd.js", "engines": { "node": ">=0.10.0" }, From 82a169c6047d34d7fb2c749d8dede900f5bb0fdf Mon Sep 17 00:00:00 2001 From: Simon Sturmer Date: Sun, 17 Jan 2016 17:08:18 +0700 Subject: [PATCH 04/55] cleanup .eslintrc --- .eslintrc | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.eslintrc b/.eslintrc index b89f710..371bae3 100644 --- a/.eslintrc +++ b/.eslintrc @@ -106,9 +106,9 @@ // Stylistic (these rules are purely matters of style and are quite subjective) - "key-spacing": 0, - "comma-spacing": 0, - "no-multi-spaces": 0, + "key-spacing": 1, // require space after colon `{a: 1}` + "comma-spacing": 1, // require space after comma `var a, b;` + "no-multi-spaces": 1, // don't allow more spaces than necessary "brace-style": [ // enforce one true brace style 1, "1tbs", { "allowSingleLine": false From e20d6ecb664201f4889ca8740f0e343708455e0c Mon Sep 17 00:00:00 2001 From: Simon Sturmer Date: Sun, 17 Jan 2016 07:59:55 +0700 Subject: [PATCH 05/55] prepare for ES6 refactor (add modules, babelrc, eslintrc) --- .babelrc | 29 +++++++++++++++++++++++++++++ package.json | 8 ++++++++ src/.eslintrc | 26 ++++++++++++++++++++++++++ 3 files changed, 63 insertions(+) create mode 100644 .babelrc create mode 100644 src/.eslintrc diff --git a/.babelrc b/.babelrc new file mode 100644 index 0000000..d1a2f52 --- /dev/null +++ b/.babelrc @@ -0,0 +1,29 @@ +{ + "plugins": [ + "transform-class-properties", + "syntax-trailing-function-commas", + "transform-object-rest-spread", + "transform-async-to-generator", + "transform-exponentiation-operator", + "check-es2015-constants", + "transform-es2015-arrow-functions", + "transform-es2015-block-scoped-functions", + "transform-es2015-block-scoping", + "transform-es2015-classes", + "transform-es2015-computed-properties", + "transform-es2015-destructuring", + "transform-es2015-for-of", + "transform-es2015-function-name", + "transform-es2015-literals", + "transform-es2015-modules-commonjs", + "transform-es2015-object-super", + "transform-es2015-parameters", + "transform-es2015-shorthand-properties", + "transform-es2015-spread", + "transform-es2015-sticky-regex", + "transform-es2015-template-literals", + "transform-es2015-typeof-symbol", + "transform-es2015-unicode-regex", + "transform-regenerator" + ] +} diff --git a/package.json b/package.json index 2332189..9d0db6d 100644 --- a/package.json +++ b/package.json @@ -116,8 +116,16 @@ }, "devDependencies": { "async": "~0.1.15", + "babel-core": "^6.4.0", + "babel-eslint": "^4.1.6", + "babel-plugin-transform-class-properties": "^6.4.0", + "babel-polyfill": "^6.3.14", + "babel-preset-es2015": "^6.3.13", + "babel-preset-stage-2": "^6.3.13", + "babel-register": "^6.4.3", "collect-stream": "^1.1.1", "eslint": "^1.10.3", + "eslint-plugin-babel": "^3.0.0", "istanbul": "~0.2.4", "jsftp": "git://github.com/sergi/jsftp.git#master", "mocha": "^2.3.4", diff --git a/src/.eslintrc b/src/.eslintrc new file mode 100644 index 0000000..02ec504 --- /dev/null +++ b/src/.eslintrc @@ -0,0 +1,26 @@ +{ + "parser": "babel-eslint", + "plugins": [ + "babel" + ], + "rules": { + // ECMAScript 6/7 (2015 and above) + "arrow-parens": 1, // require parens in arrow function arguments + "arrow-spacing": 1, // require space before/after arrow function's arrow (fixable) + "constructor-super": 1, // verify calls of super() in constructors + "generator-star-spacing": 0, // enforce spacing around the * in generator functions (fixable) + "no-class-assign": 1, // disallow modifying variables of class declarations + "no-const-assign": 1, // disallow modifying variables that are declared using const + "no-dupe-class-members": 1, // disallow duplicate name in class members + "no-this-before-super": 1, // disallow use of this/super before calling super() in constructors. + "no-var": 0, // require let or const instead of var + "object-shorthand": 0, // require method and property shorthand syntax for object literals + "prefer-arrow-callback": 1, // suggest using arrow functions as callbacks + "prefer-const": 0, // suggest using const declaration for variables that are never modified after declared + "prefer-reflect": 0, // suggest using Reflect methods where applicable + "prefer-spread": 0, // suggest using the spread operator instead of .apply(). + "prefer-template": 0, // suggest using template literals instead of strings concatenation + "require-yield": 0, // disallow generator functions that do not have yield + "babel/no-await-in-loop": 1 // async inside a loop will run operations in serial, when often the desired behavior is to do do in parallel + } +} From 581a48882cde4a0c0fb9bff05d93171b8fc5e288 Mon Sep 17 00:00:00 2001 From: Simon Sturmer Date: Sun, 17 Jan 2016 14:45:53 +0700 Subject: [PATCH 06/55] update package.json for testing ES6 --- package.json | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 9d0db6d..ef092fa 100644 --- a/package.json +++ b/package.json @@ -8,6 +8,7 @@ }, "scripts": { "test": "eslint --max-warnings 0 . && ./node_modules/.bin/istanbul test _mocha", + "test-new": "mocha --compilers js:babel-register **/__tests__/**.js", "lint": "eslint --max-warnings 0 ." }, "repository": { @@ -126,10 +127,11 @@ "collect-stream": "^1.1.1", "eslint": "^1.10.3", "eslint-plugin-babel": "^3.0.0", + "expect": "^1.13.4", + "ftp": "^0.3.10", "istanbul": "~0.2.4", "jsftp": "git://github.com/sergi/jsftp.git#master", "mocha": "^2.3.4", - "should": "~3.1.2", - "ftp": "^0.3.10" + "should": "~3.1.2" } } From 32db95b6ba3a61ae2059628623736e1e8417717b Mon Sep 17 00:00:00 2001 From: Simon Sturmer Date: Sun, 17 Jan 2016 19:40:13 +0700 Subject: [PATCH 07/55] [temp] testing command --- package.json | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/package.json b/package.json index ef092fa..2b567ed 100644 --- a/package.json +++ b/package.json @@ -7,8 +7,7 @@ "node": ">=0.10.0" }, "scripts": { - "test": "eslint --max-warnings 0 . && ./node_modules/.bin/istanbul test _mocha", - "test-new": "mocha --compilers js:babel-register **/__tests__/**.js", + "test": "mocha --compilers js:babel-register **/__tests__/**.js", "lint": "eslint --max-warnings 0 ." }, "repository": { @@ -129,7 +128,6 @@ "eslint-plugin-babel": "^3.0.0", "expect": "^1.13.4", "ftp": "^0.3.10", - "istanbul": "~0.2.4", "jsftp": "git://github.com/sergi/jsftp.git#master", "mocha": "^2.3.4", "should": "~3.1.2" From 533048f4dbe579b4c63befa3022db68221fc18f5 Mon Sep 17 00:00:00 2001 From: Simon Sturmer Date: Sun, 17 Jan 2016 16:27:02 +0700 Subject: [PATCH 08/55] move files to src directory to prepare for ES6 refactor --- {lib => src}/Constants.js | 0 {lib => src}/FtpConnection.js | 0 {lib => src}/FtpServer.js | 0 {lib => src}/PassiveListener.js | 0 {lib => src}/glob.js | 0 {lib => src}/helpers/leftPad.js | 0 {lib => src}/helpers/pathEscape.js | 0 {lib => src}/helpers/stripOptions.js | 0 {lib => src}/helpers/withCwd.js | 0 {lib => src}/starttls.js | 0 10 files changed, 0 insertions(+), 0 deletions(-) rename {lib => src}/Constants.js (100%) rename {lib => src}/FtpConnection.js (100%) rename {lib => src}/FtpServer.js (100%) rename {lib => src}/PassiveListener.js (100%) rename {lib => src}/glob.js (100%) rename {lib => src}/helpers/leftPad.js (100%) rename {lib => src}/helpers/pathEscape.js (100%) rename {lib => src}/helpers/stripOptions.js (100%) rename {lib => src}/helpers/withCwd.js (100%) rename {lib => src}/starttls.js (100%) diff --git a/lib/Constants.js b/src/Constants.js similarity index 100% rename from lib/Constants.js rename to src/Constants.js diff --git a/lib/FtpConnection.js b/src/FtpConnection.js similarity index 100% rename from lib/FtpConnection.js rename to src/FtpConnection.js diff --git a/lib/FtpServer.js b/src/FtpServer.js similarity index 100% rename from lib/FtpServer.js rename to src/FtpServer.js diff --git a/lib/PassiveListener.js b/src/PassiveListener.js similarity index 100% rename from lib/PassiveListener.js rename to src/PassiveListener.js diff --git a/lib/glob.js b/src/glob.js similarity index 100% rename from lib/glob.js rename to src/glob.js diff --git a/lib/helpers/leftPad.js b/src/helpers/leftPad.js similarity index 100% rename from lib/helpers/leftPad.js rename to src/helpers/leftPad.js diff --git a/lib/helpers/pathEscape.js b/src/helpers/pathEscape.js similarity index 100% rename from lib/helpers/pathEscape.js rename to src/helpers/pathEscape.js diff --git a/lib/helpers/stripOptions.js b/src/helpers/stripOptions.js similarity index 100% rename from lib/helpers/stripOptions.js rename to src/helpers/stripOptions.js diff --git a/lib/helpers/withCwd.js b/src/helpers/withCwd.js similarity index 100% rename from lib/helpers/withCwd.js rename to src/helpers/withCwd.js diff --git a/lib/starttls.js b/src/starttls.js similarity index 100% rename from lib/starttls.js rename to src/starttls.js From b8e5223f9659037939ff80db62a108bc4f8c9c88 Mon Sep 17 00:00:00 2001 From: Simon Sturmer Date: Sun, 17 Jan 2016 16:27:59 +0700 Subject: [PATCH 09/55] ES6: refactor FtpConnection --- src/FtpConnection.js | 2361 +++++++++++++++++++++--------------------- 1 file changed, 1181 insertions(+), 1180 deletions(-) diff --git a/src/FtpConnection.js b/src/FtpConnection.js index 9156e8b..8d25924 100644 --- a/src/FtpConnection.js +++ b/src/FtpConnection.js @@ -1,4 +1,4 @@ -/* eslint-disable no-octal */ +/* eslint-disable prefer-arrow-callback, camelcase */ var net = require('net'); var util = require('util'); @@ -21,1395 +21,1396 @@ var leftPad = require('./helpers/leftPad'); 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; - Object.keys(properties).forEach(function(key) { - self[key] = properties[key]; - }); -} -util.inherits(FtpConnection, EventEmitter); - -// TODO: rename this to writeLine? -FtpConnection.prototype.respond = function(message, callback) { - return this._writeText(this.socket, message + '\r\n', callback); -}; - -FtpConnection.prototype._logIf = function(verbosity, message) { - return this.server._logIf(verbosity, message, this); -}; - -// We don't want to use setEncoding because it screws up TLS, but we -// also don't want to explicitly specify ASCII encoding for every call to 'write' -// with a string argument. -FtpConnection.prototype._writeText = function(socket, data, callback) { - if (!socket.writable) { - this._logIf(LOG.DEBUG, 'Attempted writing to a closed socket:\n>> ' + data.trim()); - return; +var { + LOG_LEVELS: LOG, + DOES_NOT_REQUIRE_AUTH, + REQUIRES_CONFIGURED_DATA, +} = Constants; + +class FtpConnection extends EventEmitter { + constructor(properties) { + super(); + Object.keys(properties).forEach((key) => { + this[key] = properties[key]; + }); } - this._logIf(LOG.TRACE, '>> ' + data.trim()); - return socket.write(data, 'utf8', callback); -}; - -FtpConnection.prototype._authenticated = function() { - return !!this.username; -}; - -FtpConnection.prototype._closeDataConnections = function() { - if (this.dataSocket) { - // TODO: should the second arg be false here? - this._closeSocket(this.dataSocket, true); - this.dataSocket = null; + + // TODO: rename this to writeLine? + respond(message, callback) { + return this._writeText(this.socket, message + '\r\n', callback); } - if (this.pasv) { - this.pasv.close(); - this.pasv = null; + + _logIf(verbosity, message) { + return this.server._logIf(verbosity, message, this); } -}; -FtpConnection.prototype._createPassiveServer = function() { - var self = this; + // We don't want to use setEncoding because it screws up TLS, but we + // also don't want to explicitly specify ASCII encoding for every call to 'write' + // with a string argument. + _writeText(socket, data, callback) { + if (!socket.writable) { + this._logIf(LOG.DEBUG, 'Attempted writing to a closed socket:\n>> ' + data.trim()); + return; + } + this._logIf(LOG.TRACE, '>> ' + data.trim()); + return socket.write(data, 'utf8', callback); + } - 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'); + _authenticated() { + return !!this.username; + } - if (self.secure) { - self._logIf(LOG.INFO, 'Upgrading passive connection to TLS'); - starttls.starttlsServer(psocket, self.server.options.tlsOptions, function(err, cleartext) { - if (err) { - 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, 'Allowing unauthorized passive connection (allowUnauthorizedTls is on)'); - switchToSecure(); - } else { - self._logIf(LOG.INFO, 'Closing unauthorized passive connection (allowUnauthorizedTls is off)'); - self._closeSocket(self.socket, true); + _closeDataConnections() { + if (this.dataSocket) { + // TODO: should the second arg be false here? + this._closeSocket(this.dataSocket, true); + this.dataSocket = null; + } + if (this.pasv) { + this.pasv.close(); + this.pasv = null; + } + } + + _createPassiveServer() { + var self = this; + + 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'); + + if (self.secure) { + self._logIf(LOG.INFO, 'Upgrading passive connection to TLS'); + starttls.starttlsServer(psocket, self.server.options.tlsOptions, function(err, cleartext) { + if (err) { + 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, 'Allowing unauthorized passive connection (allowUnauthorizedTls is on)'); + switchToSecure(); + } else { + self._logIf(LOG.INFO, 'Closing unauthorized 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'); + // TODO: Check for existing dataSocket. + self.dataSocket = cleartext; + setupPassiveListener(); } + }); + } else { + // TODO: Check for existing dataSocket. + self.dataSocket = psocket; + setupPassiveListener(); + } + + function setupPassiveListener() { + if (self.dataListener) { + self.dataListener.emit('ready'); } else { - switchToSecure(); + self._logIf(LOG.WARN, 'Passive connection initiated, but no data listener'); } - function switchToSecure() { - self._logIf(LOG.INFO, 'Secure passive connection started'); - // TODO: Check for existing dataSocket. - self.dataSocket = cleartext; - setupPassiveListener(); + // 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')); + function allOver(ename) { + return function(err) { + self._logIf( + (err ? LOG.ERROR : LOG.DEBUG), + 'Passive data event: ' + ename + (err ? ' due to error' : '') + ); + self.dataSocket = null; + }; } - }); - } else { - // TODO: Check for existing dataSocket. - self.dataSocket = psocket; - setupPassiveListener(); - } - - function setupPassiveListener() { - if (self.dataListener) { - self.dataListener.emit('ready'); - } else { - self._logIf(LOG.WARN, 'Passive connection initiated, but no data listener'); - } - // 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')); - function allOver(ename) { - return function(err) { - self._logIf( - (err ? LOG.ERROR : LOG.DEBUG), - 'Passive data event: ' + ename + (err ? ' due to error' : '') - ); + self.dataSocket.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.dataSocket.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; - }); - } - }); -}; - -FtpConnection.prototype._whenDataReady = function(callback) { - var self = this; + _whenDataReady(callback) { + var self = this; - if (self.dataListener) { - // 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.dataListener.once('ready', function() { - self._logIf(LOG.DEBUG, '...client has connected now'); + if (self.dataListener) { + // 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 { - // 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._logIf(LOG.DEBUG, 'Currently no data connection; expecting client to connect to pasv server shortly...'); + self.dataListener.once('ready', function() { + self._logIf(LOG.DEBUG, '...client has connected now'); + callback(self.dataSocket); + }); + } } else { - self._initiateData(function(sock) { - callback(sock); - }); + // 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); + }); + } } } -}; -FtpConnection.prototype._initiateData = function(callback) { - var self = this; + _initiateData(callback) { + var self = this; - if (self.dataSocket) { - return callback(self.dataSocket); - } + if (self.dataSocket) { + return callback(self.dataSocket); + } - 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) : '') - ); + 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) : '') + ); + } + + 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; + }); } - 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) { - this._logIf(LOG.ERROR, 'Client connection error: ' + util.inspect(err)); - this._closeSocket(this.socket, true); -}; - -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. - if (this.dataSocket) { - this._closeSocket(this.dataSocket, hadError); - this.dataSocket = null; + _onError(err) { + this._logIf(LOG.ERROR, 'Client connection error: ' + util.inspect(err)); + this._closeSocket(this.socket, true); } - if (this.socket) { - this._closeSocket(this.socket, hadError); - this.socket = null; + + _onEnd() { + this._logIf(LOG.DEBUG, 'Client connection ended'); } - if (this.pasv) { - this.pasv.close(); - this.pasv = null; + + _onClose(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; + } + 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'); } - // TODO: LOG.DEBUG? - this._logIf(LOG.INFO, 'Client connection closed'); -}; -FtpConnection.prototype._onData = function(data) { - var self = this; + _onData(data) { + var self = this; - if (self.hasQuit) { - return; - } + if (self.hasQuit) { + return; + } - data = data.toString('utf-8').trim(); - self._logIf(LOG.TRACE, '<< ' + data); - // Don't want to include passwords in logs. - self._logIf(LOG.INFO, 'FTP command: ' + - data.replace(/^PASS [\s\S]*$/i, 'PASS ***') - ); - - var command; - var commandArg; - var index = data.indexOf(' '); - if (index !== -1) { - var parts = data.split(' '); - command = parts.shift().toUpperCase(); - commandArg = parts.join(' ').trim(); - } else { - command = data.toUpperCase(); - commandArg = ''; - } + data = data.toString('utf-8').trim(); + self._logIf(LOG.TRACE, '<< ' + data); + // Don't want to include passwords in logs. + self._logIf(LOG.INFO, 'FTP command: ' + + data.replace(/^PASS [\s\S]*$/i, 'PASS ***') + ); - var m = '_command_' + command; - if (self[m]) { - if (self.allowedCommands != null && self.allowedCommands[command] !== true) { - self.respond('502 ' + command + ' not implemented.'); - } else if (DOES_NOT_REQUIRE_AUTH[command]) { - self[m](commandArg, command); + var command; + var commandArg; + var index = data.indexOf(' '); + if (index !== -1) { + var parts = data.split(' '); + command = parts.shift().toUpperCase(); + commandArg = parts.join(' ').trim(); } else { - // If 'tlsOnly' option is set, all commands which require user authentication will only - // 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()) { - checkData(); - } else { - self.respond('530 Not logged in.'); - } + command = data.toUpperCase(); + commandArg = ''; } - function checkData() { - if (REQUIRES_CONFIGURED_DATA[command] && !self.dataConfigured) { - self.respond('425 Data connection not configured; send PASV or PORT'); - return; + var m = '_command_' + command; + if (self[m]) { + if (self.allowedCommands != null && self.allowedCommands[command] !== true) { + self.respond('502 ' + command + ' not implemented.'); + } else if (DOES_NOT_REQUIRE_AUTH[command]) { + self[m](commandArg, command); + } else { + // If 'tlsOnly' option is set, all commands which require user authentication will only + // 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()) { + checkData(); + } else { + self.respond('530 Not logged in.'); + } } - self[m](commandArg, command); + function checkData() { + if (REQUIRES_CONFIGURED_DATA[command] && !self.dataConfigured) { + self.respond('425 Data connection not configured; send PASV or PORT'); + return; + } + + self[m](commandArg, command); + } + } else { + self.respond('502 Command not implemented.'); } - } else { - self.respond('502 Command not implemented.'); + self.previousCommand = command; } - self.previousCommand = command; -}; - -// 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) { - var self = this; - - if (!self.server.options.tlsOptions || commandArg !== 'TLS') { - return self.respond('502 Command not implemented'); + + // Specify the user's account (superfluous) + _command_ACCT() { + this.respond('202 Command not implemented, superfluous at this site.'); + return this; } - self.respond('234 Honored', function() { - self._logIf(LOG.INFO, 'Establishing secure connection...'); - starttls.starttlsServer(self.socket, self.server.options.tlsOptions, function(err, cleartext) { - 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)'); + // Allocate storage space (superfluous) + _command_ALLO() { + this.respond('202 Command not implemented, superfluous at this site.'); + return this; + } + + _command_AUTH(commandArg) { + var self = this; + + if (!self.server.options.tlsOptions || commandArg !== 'TLS') { + return self.respond('502 Command not implemented'); + } + + self.respond('234 Honored', function() { + self._logIf(LOG.INFO, 'Establishing secure connection...'); + starttls.starttlsServer(self.socket, self.server.options.tlsOptions, function(err, cleartext) { + 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(); + } + + function switchToSecure() { + self._logIf(LOG.INFO, 'Secure connection started'); + self.socket = cleartext; + self.socket.on('data', function(data) { + self._onData(data); + }); + self.secure = true; } + }); + }); + } + + // Change working directory to parent directory + _command_CDUP() { + var pathServer = pathModule.dirname(this.cwd); + var pathEscaped = pathEscape(pathServer); + this.cwd = pathServer; + this.respond('250 Directory changed to "' + pathEscaped + '"'); + return this; + } + + // Change working directory + _command_CWD(pathRequest) { + var pathServer = withCwd(this.cwd, pathRequest); + var pathFs = pathModule.join(this.root, pathServer); + var pathEscaped = pathEscape(pathServer); + this.fs.stat(pathFs, function(err, stats) { + if (err) { + this._logIf(LOG.ERROR, 'CWD ' + pathRequest + ': ' + err); + this.respond('550 Directory not found.'); + } else if (!stats.isDirectory()) { + this._logIf(LOG.WARN, 'Attempt to CWD to non-directory'); + this.respond('550 Not a directory'); } else { - switchToSecure(); + this.cwd = pathServer; + this.respond('250 CWD successful. "' + pathEscaped + '" is current directory'); } + }.bind(this)); + return this; + } - function switchToSecure() { - self._logIf(LOG.INFO, 'Secure connection started'); - self.socket = cleartext; - self.socket.on('data', function(data) { - self._onData(data); - }); - self.secure = true; + _command_DELE(commandArg) { + var self = this; + + var filename = withCwd(self.cwd, commandArg); + self.fs.unlink(pathModule.join(self.root, filename), function(err) { + if (err) { + self._logIf(LOG.ERROR, 'Error deleting file: ' + filename + ', ' + err); + // write error to socket + self.respond('550 Permission denied'); + } else { + self.respond('250 File deleted'); } }); - }); -}; - -// Change working directory to parent directory -FtpConnection.prototype._command_CDUP = function() { - var pathServer = pathModule.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 pathEscaped = pathEscape(pathServer); - this.fs.stat(pathFs, function(err, stats) { - if (err) { - this._logIf(LOG.ERROR, 'CWD ' + pathRequest + ': ' + err); - this.respond('550 Directory not found.'); - } else if (!stats.isDirectory()) { - this._logIf(LOG.WARN, 'Attempt to CWD to non-directory'); - this.respond('550 Not a directory'); - } else { - this.cwd = pathServer; - 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) { - if (err) { - self._logIf(LOG.ERROR, 'Error deleting file: ' + filename + ', ' + err); - // write error to socket - self.respond('550 Permission denied'); - } else { - self.respond('250 File deleted'); - } - }); -}; - -FtpConnection.prototype._command_FEAT = function() { - // Get the feature list implemented by the server. (RFC 2389) - this.respond( - '211-Features\r\n' + - ' SIZE\r\n' + - ' UTF8\r\n' + - ' MDTM\r\n' + - (!this.server.options.tlsOptions ? '' : - ' AUTH TLS\r\n' + - ' PBSZ\r\n' + - ' UTF8\r\n' + - ' PROT\r\n' - ) + - '211 end' - ); -}; - -FtpConnection.prototype._command_OPTS = function(commandArg) { - // http://tools.ietf.org/html/rfc2389#section-4 - if (commandArg.toUpperCase() === 'UTF8 ON') { - this.respond('200 OK'); - } else { - this.respond('451 Not supported'); } -}; - -// Print the file modification time -FtpConnection.prototype._command_MDTM = function(file) { - file = withCwd(this.cwd, file); - file = pathModule.join(this.root, file); - this.fs.stat(file, function(err, stats) { - if (err) { - this.respond('550 File unavailable'); + + _command_FEAT() { + // Get the feature list implemented by the server. (RFC 2389) + this.respond( + '211-Features\r\n' + + ' SIZE\r\n' + + ' UTF8\r\n' + + ' MDTM\r\n' + + (!this.server.options.tlsOptions ? '' : + ' AUTH TLS\r\n' + + ' PBSZ\r\n' + + ' UTF8\r\n' + + ' PROT\r\n' + ) + + '211 end' + ); + } + + _command_OPTS(commandArg) { + // http://tools.ietf.org/html/rfc2389#section-4 + if (commandArg.toUpperCase() === 'UTF8 ON') { + this.respond('200 OK'); } else { - this.respond('213 ' + dateformat(stats.mtime, 'yyyymmddhhMMss')); + this.respond('451 Not supported'); } - }.bind(this)); - return this; -}; - -FtpConnection.prototype._command_LIST = function(commandArg) { - this._LIST(commandArg, true/*detailed*/, 'LIST'); -}; - -FtpConnection.prototype._command_NLST = function(commandArg) { - this._LIST(commandArg, false/*!detailed*/, 'NLST'); -}; - -FtpConnection.prototype._command_STAT = function(commandArg) { - if (commandArg) { - this._LIST(commandArg, true/*detailed*/, 'STAT'); - } else { - this.respond('211 FTP Server Status OK'); } -}; - -FtpConnection.prototype._LIST = function(commandArg, detailed, cmd) { - /* - Normally the server responds with a mark using code 150. It then stops accepting new connections, attempts to send the contents of the directory over the data connection, and closes the data connection. Finally it - - accepts the LIST or NLST request with code 226 if the entire directory was successfully transmitted; - rejects the LIST or NLST request with code 425 if no TCP connection was established; - rejects the LIST or NLST request with code 426 if the TCP connection was established but then broken by the client or by network failure; or - rejects the LIST or NLST request with code 451 if the server had trouble reading the directory from disk. - The server may reject the LIST or NLST request (with code 450 or 550) without first responding with a mark. In this case the server does not touch the data connection. - */ - - var self = this; + // Print the file modification time + _command_MDTM(file) { + file = withCwd(this.cwd, file); + file = pathModule.join(this.root, file); + this.fs.stat(file, function(err, stats) { + if (err) { + this.respond('550 File unavailable'); + } else { + this.respond('213 ' + dateformat(stats.mtime, 'yyyymmddhhMMss')); + } + }.bind(this)); + return this; + } - // LIST may be passed options (-a in particular). We just ignore any of these. - // (In the particular case of -a, we show hidden files anyway.) - var dirname = stripOptions(commandArg); - var dir = withCwd(self.cwd, dirname); + _command_LIST(commandArg) { + this._LIST(commandArg, true/*detailed*/, 'LIST'); + } - glob.setMaxStatsAtOnce(self.server.options.maxStatsAtOnce); - glob.glob(pathModule.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'); - return; - } + _command_NLST(commandArg) { + this._LIST(commandArg, false/*!detailed*/, 'NLST'); + } - if (self.server.options.hideDotFiles) { - files = files.filter(function(file) { - if (file.name && file.name[0] !== '.') { - return true; - } - }); + _command_STAT(commandArg) { + if (commandArg) { + this._LIST(commandArg, true/*detailed*/, 'STAT'); + } else { + this.respond('211 FTP Server Status OK'); } + } - self._logIf(LOG.INFO, 'Directory has ' + files.length + ' files'); - if (files.length === 0) { - return self._listFiles([], detailed, cmd); - } + _LIST(commandArg, detailed, cmd) { + /* + Normally the server responds with a mark using code 150. It then stops accepting new connections, attempts to send the contents of the directory over the data connection, and closes the data connection. Finally it - var fileInfos; // To contain list of files with info for each. + accepts the LIST or NLST request with code 226 if the entire directory was successfully transmitted; + rejects the LIST or NLST request with code 425 if no TCP connection was established; + rejects the LIST or NLST request with code 426 if the TCP connection was established but then broken by the client or by network failure; or + rejects the LIST or NLST request with code 451 if the server had trouble reading the directory from disk. - if (!detailed) { - // We're not doing a detailed listing, so we don't need to get username - // and group name. - fileInfos = files; - return finished(); - } + The server may reject the LIST or NLST request (with code 450 or 550) without first responding with a mark. In this case the server does not touch the data connection. + */ - // Now we need to get username and group name for each file from user/group ids. - fileInfos = []; + var self = this; - var CONC = self.server.options.maxStatsAtOnce; - var j = 0; - for (var i = 0; i < files.length && i < CONC; ++i) { - handleFile(i); - } - j = --i; + // LIST may be passed options (-a in particular). We just ignore any of these. + // (In the particular case of -a, we show hidden files anyway.) + var dirname = stripOptions(commandArg); + var dir = withCwd(self.cwd, dirname); - function handleFile(ii) { - if (i >= files.length) { - return i === files.length + j ? finished() : null; + glob.setMaxStatsAtOnce(self.server.options.maxStatsAtOnce); + glob.glob(pathModule.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'); + return; } - self.server.getUsernameFromUid(files[ii].stats.uid, function(e1, uname) { - self.server.getGroupFromGid(files[ii].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, - }); + if (self.server.options.hideDotFiles) { + files = files.filter(function(file) { + if (file.name && file.name[0] !== '.') { + return true; } - handleFile(++i); }); - }); - } - - function finished() { - // Sort file names. - if (!self.server.options.dontSortFilenames) { - if (self.server.options.filenameSortMap !== false) { - var sm = ( - self.server.options.filenameSortMap || - function(x) { - return x.toUpperCase(); - } - ); - for (var i = 0; i < fileInfos.length; ++i) { - fileInfos[i]._s = sm(detailed ? fileInfos[i].file.name : fileInfos[i].name); - } - } + } - var sf = (self.server.options.filenameSortFunc || - function(x, y) { - return x.localeCompare(y); - }); - fileInfos = fileInfos.sort(function(x, y) { - if (self.server.options.filenameSortMap !== false) { - return sf(x._s, y._s); - } else if (detailed) { - return sf(x.file.name, y.file.name); - } else { - return sf(x.name, y.name); - } - }); + self._logIf(LOG.INFO, 'Directory has ' + files.length + ' files'); + if (files.length === 0) { + return self._listFiles([], detailed, cmd); } - self._listFiles(fileInfos, detailed, cmd); - } - }, self.server.options.noWildcards); -}; - -FtpConnection.prototype._listFiles = function(fileInfos, detailed, cmd) { - var self = this; - - var m = '150 Here comes the directory listing'; - var BEGIN_MSGS = { - LIST: m, NLST: m, STAT: '213-Status follows', - }; - m = '226 Transfer OK'; - var END_MSGS = { - LIST: m, NLST: m, STAT: '213 End of status', - }; - - self.respond(BEGIN_MSGS[cmd], function() { - if (cmd === 'STAT') { - whenReady(self.socket); - } else { - self._whenDataReady(whenReady); - } + var fileInfos; // To contain list of files with info for each. - function whenReady(listconn) { - if (fileInfos.length === 0) { - return success(); + if (!detailed) { + // We're not doing a detailed listing, so we don't need to get username + // and group name. + fileInfos = files; + return finished(); } - function success(err) { - if (err) { - self.respond('550 Error listing files'); - } else { - self.respond(END_MSGS[cmd]); - } - if (cmd !== 'STAT') { - self._closeSocket(listconn); - } + // 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); } + j = --i; - self._logIf(LOG.DEBUG, 'Sending file list'); + function handleFile(ii) { + if (i >= files.length) { + return i === files.length + j ? finished() : null; + } - for (var i = 0; i < fileInfos.length; ++i) { - var fileInfo = fileInfos[i]; + self.server.getUsernameFromUid(files[ii].stats.uid, function(e1, uname) { + self.server.getGroupFromGid(files[ii].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, + }); + } + handleFile(++i); + }); + }); + } - var line = ''; - var file; + function finished() { + // Sort file names. + if (!self.server.options.dontSortFilenames) { + if (self.server.options.filenameSortMap !== false) { + var sm = ( + self.server.options.filenameSortMap || + function(x) { + return x.toUpperCase(); + } + ); + for (var i = 0; i < fileInfos.length; ++i) { + fileInfos[i]._s = sm(detailed ? fileInfos[i].file.name : fileInfos[i].name); + } + } - if (!detailed) { - file = fileInfo; - line += file.name + '\r\n'; - } else { - file = fileInfo.file; - var s = file.stats; - var allModes = (new StatMode({mode: s.mode})).toString(); - var rwxModes = allModes.substr(1, 9); - line += (s.isDirectory() ? 'd' : '-') + rwxModes; - // ^-- Clients don't need to know about special files and pipes - line += ' 1 ' + - (fileInfo.uname || 'ftp') + ' ' + - (fileInfo.gname === null ? 'ftp' : fileInfo.gname) + ' '; - 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'; + var sf = (self.server.options.filenameSortFunc || + function(x, y) { + return x.localeCompare(y); + }); + fileInfos = fileInfos.sort(function(x, y) { + if (self.server.options.filenameSortMap !== false) { + return sf(x._s, y._s); + } else if (detailed) { + return sf(x.file.name, y.file.name); + } else { + return sf(x.name, y.name); + } + }); } - self._writeText( - listconn, - line, - (i === fileInfos.length - 1 ? success : undefined) - ); - } - } - }); -}; - -// Create a directory -FtpConnection.prototype._command_MKD = function(pathRequest) { - var pathServer = withCwd(this.cwd, pathRequest); - var pathEscaped = pathEscape(pathServer); - var pathFs = pathModule.join(this.root, pathServer); - this.fs.mkdir(pathFs, 0755, function(err) { - if (err) { - this._logIf(LOG.ERROR, 'MKD ' + pathRequest + ': ' + err); - this.respond('550 "' + pathEscaped + '" directory NOT created'); - } else { - 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) { - this._PORT(x, y); -}; - -FtpConnection.prototype._command_EPRT = function(x, y) { - this._PORT(x, y); -}; - -FtpConnection.prototype._PORT = function(commandArg, command) { - var self = this; - var m; - - self.dataConfigured = false; - - 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})$/); - if (!m) { - self.respond('501 Bad argument to PORT'); - return; - } - var host = m[1] + '.' + m[2] + '.' + m[3] + '.' + m[4]; - var port = (parseInt(m[5], 10) << 8) + parseInt(m[6], 10); - if (isNaN(port)) { - // The value should never be NaN because the relevant groups in the regex matche 1-3 digits. - throw new Error('Impossible NaN in FtpConnection.prototype._PORT'); - } - } else { // EPRT - if (commandArg.length >= 3 && commandArg.charAt(0) === '|' && - commandArg.charAt(2) === '|' && commandArg.charAt(1) === '2') { - // Only IPv4 is supported. - self.respond('522 Server cannot handle IPv6 EPRT commands, use (1)'); - return; - } + self._listFiles(fileInfos, detailed, cmd); + } + }, self.server.options.noWildcards); + } - m = commandArg.match(/^\|1\|([0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3})\|([0-9]{1,5})/); - if (!m) { - self.respond('501 Bad Argument to EPRT'); - return; - } + _listFiles(fileInfos, detailed, cmd) { + var self = this; - var r = parseInt(m[2], 10); - if (isNaN(r)) { - // The value should never be NaN because the relevant group in the regex matches 1-5 digits. - throw new Error('Impossible NaN in FtpConnection.prototype._PORT (2)'); - } - if (r > 65535 || r <= 0) { - self.respond('501 Bad argument to EPRT (invalid port number)'); - return; - } + var m = '150 Here comes the directory listing'; + var BEGIN_MSGS = { + LIST: m, NLST: m, STAT: '213-Status follows', + }; + m = '226 Transfer OK'; + var END_MSGS = { + LIST: m, NLST: m, STAT: '213 End of status', + }; - host = m[1]; - port = r; - } + self.respond(BEGIN_MSGS[cmd], function() { + if (cmd === 'STAT') { + whenReady(self.socket); + } else { + self._whenDataReady(whenReady); + } - self.dataConfigured = true; - self.dataHost = host; - self.dataPort = port; - self._logIf(LOG.DEBUG, 'self.dataHost, self.dataPort set to ' + self.dataHost + ':' + self.dataPort); - self.respond('200 OK'); -}; + function whenReady(listconn) { + if (fileInfos.length === 0) { + return success(); + } -FtpConnection.prototype._command_PASV = function(x, y) { - this._PASV(x, y); -}; + function success(err) { + if (err) { + self.respond('550 Error listing files'); + } else { + self.respond(END_MSGS[cmd]); + } + if (cmd !== 'STAT') { + self._closeSocket(listconn); + } + } -FtpConnection.prototype._command_EPSV = function(x, y) { - this._PASV(x, y); -}; + self._logIf(LOG.DEBUG, 'Sending file list'); -FtpConnection.prototype._PASV = function(commandArg, command) { - var self = this; + for (var i = 0; i < fileInfos.length; ++i) { + var fileInfo = fileInfos[i]; - self.dataConfigured = false; + var line = ''; + var file; - if (command === 'EPSV' && commandArg && commandArg !== '1') { - self.respond('202 Not supported'); - return; + if (!detailed) { + file = fileInfo; + line += file.name + '\r\n'; + } else { + file = fileInfo.file; + var s = file.stats; + var allModes = (new StatMode({mode: s.mode})).toString(); + var rwxModes = allModes.substr(1, 9); + line += (s.isDirectory() ? 'd' : '-') + rwxModes; + // ^-- Clients don't need to know about special files and pipes + line += ' 1 ' + + (fileInfo.uname || 'ftp') + ' ' + + (fileInfo.gname === null ? 'ftp' : fileInfo.gname) + ' '; + 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'; + } + self._writeText( + listconn, + line, + (i === fileInfos.length - 1 ? success : undefined) + ); + } + } + }); } - // not sure whether the spec limits to 1 data connection at a time ... - if (self.dataSocket) { - self._closeSocket(self.dataSocket, true); + // Create a directory + _command_MKD(pathRequest) { + var pathServer = withCwd(this.cwd, pathRequest); + var pathEscaped = pathEscape(pathServer); + var pathFs = pathModule.join(this.root, pathServer); + this.fs.mkdir(pathFs, 0o755, function(err) { + if (err) { + this._logIf(LOG.ERROR, 'MKD ' + pathRequest + ': ' + err); + this.respond('550 "' + pathEscaped + '" directory NOT created'); + } else { + this.respond('257 "' + pathEscaped + '" directory created'); + } + }.bind(this)); + return this; } - if (self.dataListener) { - 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); + // Perform a no-op (used to keep-alive connection) + _command_NOOP() { + this.respond('200 OK'); + return this; } - self.dataConfigured = true; -}; - -FtpConnection.prototype._writePASVReady = function(command) { - var self = this; - - var a = self.pasv.address(); - var host = self.server.host; - var port = a.port; - if (command === 'PASV') { - var i1 = (port / 256) | 0; - var i2 = port % 256; - self.respond('227 Entering Passive Mode (' + host.split('.').join(',') + ',' + i1 + ',' + i2 + ')'); - } else { // EPASV - self.respond('229 Entering Extended Passive Mode (|||' + port + '|)'); + _command_PORT(x, y) { + this._PORT(x, y); } -}; -FtpConnection.prototype._setupNewPASV = function(commandArg, command) { - var self = this; + _command_EPRT(x, y) { + this._PORT(x, y); + } - var pasv = self._createPassiveServer(); - var portRangeErrorHandler; + _PORT(commandArg, command) { + var self = this; + var m; - function normalErrorHandler(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.dataListener = null; - self.dataSocket = null; - 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); + 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})$/); + if (!m) { + self.respond('501 Bad argument to PORT'); + return; } - }; - pasv.on('error', portRangeErrorHandler); - } else { - pasv.listen(0); - pasv.on('error', normalErrorHandler); - } - // Once we're successfully listening, tell the client - pasv.on('listening', function() { - self.pasv = pasv; + var host = m[1] + '.' + m[2] + '.' + m[3] + '.' + m[4]; + var port = (parseInt(m[5], 10) << 8) + parseInt(m[6], 10); + if (isNaN(port)) { + // The value should never be NaN because the relevant groups in the regex matche 1-3 digits. + throw new Error('Impossible NaN in FtpConnection.prototype._PORT'); + } + } else { // EPRT + if (commandArg.length >= 3 && commandArg.charAt(0) === '|' && + commandArg.charAt(2) === '|' && commandArg.charAt(1) === '2') { + // Only IPv4 is supported. + self.respond('522 Server cannot handle IPv6 EPRT commands, use (1)'); + return; + } - if (portRangeErrorHandler) { - pasv.removeListener('error', portRangeErrorHandler); - pasv.addListener('error', normalErrorHandler); - } + m = commandArg.match(/^\|1\|([0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3})\|([0-9]{1,5})/); + if (!m) { + self.respond('501 Bad Argument to EPRT'); + return; + } - 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'); - }); -}; - -FtpConnection.prototype._command_PBSZ = function(commandArg) { - var self = this; - - if (!self.server.options.tlsOptions) { - return self.respond('202 Not supported'); - } + var r = parseInt(m[2], 10); + if (isNaN(r)) { + // The value should never be NaN because the relevant group in the regex matches 1-5 digits. + throw new Error('Impossible NaN in FtpConnection.prototype._PORT (2)'); + } + if (r > 65535 || r <= 0) { + self.respond('501 Bad argument to EPRT (invalid port number)'); + return; + } - // Protection Buffer Size (RFC 2228) - if (!self.secure) { - self.respond('503 Secure connection not established'); - } else if (parseInt(commandArg, 10) !== 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; + host = m[1]; + port = r; + } + + self.dataConfigured = true; + self.dataHost = host; + self.dataPort = port; + self._logIf(LOG.DEBUG, 'self.dataHost, self.dataPort set to ' + self.dataHost + ':' + self.dataPort); self.respond('200 OK'); } -}; - -FtpConnection.prototype._command_PROT = function(commandArg) { - var self = this; - if (!self.server.options.tlsOptions) { - return self.respond('202 Not supported'); + _command_PASV(x, y) { + this._PASV(x, y); } - 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'); - } -}; - -// 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.'); + _command_EPSV(x, y) { + this._PASV(x, y); } - return this; -}; -FtpConnection.prototype._command_QUIT = function() { - var self = this; + _PASV(commandArg, command) { + var self = this; - self.hasQuit = true; - self.respond('221 Goodbye', function(err) { - if (err) { - self._logIf(LOG.ERROR, "Error writing 'Goodbye' message following QUIT"); + self.dataConfigured = false; + + if (command === 'EPSV' && commandArg && commandArg !== '1') { + self.respond('202 Not supported'); + return; } - self._closeSocket(self.socket, true); - self._closeDataConnections(); - }); -}; - -FtpConnection.prototype._command_RETR = function(commandArg) { - var filename = pathModule.join(this.root, withCwd(this.cwd, commandArg)); - - if (this.server.options.useReadFile) { - this._RETR_usingReadFile(commandArg, filename); - } else { - this._RETR_usingCreateReadStream(commandArg, filename); - } -}; -FtpConnection.prototype._RETR_usingCreateReadStream = function(commandArg, filename) { - var self = this; - var startTime = new Date(); + // not sure whether the spec limits to 1 data connection at a time ... + if (self.dataSocket) { + self._closeSocket(self.dataSocket, true); + } - self.emit('file:retr', 'open', { - user: self.username, - file: filename, - sTime: startTime, - }); + if (self.dataListener) { + 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); + } - function afterOk(callback) { - self.respond('150 Opening ' + self.mode.toUpperCase() + ' mode data connection', callback); + self.dataConfigured = true; } + _writePASVReady(command) { + var self = this; + + var a = self.pasv.address(); + var host = self.server.host; + var port = a.port; + if (command === 'PASV') { + var i1 = (port / 256) | 0; + var i2 = port % 256; + self.respond('227 Entering Passive Mode (' + host.split('.').join(',') + ',' + i1 + ',' + i2 + ')'); + } else { // EPASV + self.respond('229 Entering Extended Passive Mode (|||' + port + '|)'); + } + } - self.fs.open(filename, 'r', function(err, fd) { - if (err) { - self.emit('file:retr', 'error', { - user: self.username, - file: filename, - filesize: 0, - sTime: startTime, - eTime: new Date(), - duration: new Date() - startTime, - errorState: true, - error: err, - }); - if (err.code === 'ENOENT') { - self.respond('550 Not Found'); - } else { // Who knows what's going on here... - self.respond('550 Not Accessible'); - self._logIf(LOG.ERROR, "Error at read of '" + filename + "' other than ENOENT " + err); - } - } else { - afterOk(function() { - self._whenDataReady(function(pasvconn) { - var readLength = 0; - var now = new Date(); - var rs = self.fs.createReadStream(null, {fd: fd}); - rs.pause(); - rs.once('error', function(err) { - self.emit('file:retr', 'close', { - user: self.username, - file: filename, - filesize: 0, - sTime: startTime, - eTime: now, - duration: now - startTime, - errorState: true, - error: err, - }); - }); + _setupNewPASV(commandArg, command) { + var self = this; - rs.on('data', function(buffer) { - readLength += buffer.length; - }); + var pasv = self._createPassiveServer(); + var portRangeErrorHandler; - rs.on('end', function() { - var now = new Date(); - self.emit('file:retr', 'close', { - user: self.username, - file: filename, - filesize: 0, - sTime: startTime, - eTime: now, - duration: now - startTime, - errorState: false, - }); - self.respond('226 Closing data connection, sent ' + readLength + ' bytes'); - }); + function normalErrorHandler(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.dataListener = null; + self.dataSocket = null; + self.pasv = null; + } - rs.pipe(pasvconn); - rs.resume(); - }); - }); + 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); } - }); -}; -FtpConnection.prototype._RETR_usingReadFile = function(commandArg, filename) { - var self = this; - var startTime = new Date(); + // 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); + } - self.emit('file:retr', 'open', { - user: self.username, - file: filename, - sTime: startTime, - }); + self._logIf(LOG.DEBUG, 'Passive data connection beginning to listen'); - function afterOk(callback) { - self.respond('150 Opening ' + self.mode.toUpperCase() + ' mode data connection', callback); + 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'); + }); } - self.fs.readFile(filename, function(err, contents) { - if (err) { - self.emit('file:retr', 'error', { - user: self.username, - file: filename, - filesize: 0, - sTime: startTime, - eTime: new Date(), - duration: new Date() - startTime, - errorState: true, - error: err, - }); - if (err.code === 'ENOENT') { - self.respond('550 Not Found'); - } else { // Who knows what's going on here... - self.respond('550 Not Accessible'); - self._logIf(LOG.ERROR, "Error at read of '" + filename + "' other than ENOENT " + err); - } - } else { - afterOk(function() { - self._whenDataReady(function(pasvconn) { - contents = {filename: filename, data: contents}; - self.emit('file:retr:contents', contents); - contents = contents.data; - pasvconn.write(contents); - var contentLength = contents.length; - self.respond('226 Closing data connection, sent ' + contentLength + ' bytes'); - self.emit('file:retr', 'close', { - user: self.username, - file: filename, - filesize: contentLength, - sTime: startTime, - eTime: new Date(), - duration: new Date() - startTime, - errorState: false, - }); - self._closeSocket(pasvconn); - }); - }); - } - }); -}; - -// Remove a directory -FtpConnection.prototype._command_RMD = function(pathRequest) { - var pathServer = withCwd(this.cwd, pathRequest); - var pathFs = pathModule.join(this.root, pathServer); - this.fs.rmdir(pathFs, function(err) { - if (err) { - this._logIf(LOG.ERROR, 'RMD ' + pathRequest + ': ' + err); - this.respond('550 Delete operation failed'); - } else { - this.respond('250 "' + pathServer + '" directory removed'); + _command_PBSZ(commandArg) { + var self = this; + + if (!self.server.options.tlsOptions) { + return self.respond('202 Not supported'); } - }.bind(this)); - return this; -}; - -FtpConnection.prototype._command_RNFR = function(commandArg) { - var self = this; - self.filefrom = withCwd(self.cwd, commandArg); - self._logIf(LOG.DEBUG, 'Rename from ' + self.filefrom); - self.respond('350 Ready for destination name'); -}; - -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) { - 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' : '')); + + // Protection Buffer Size (RFC 2228) + if (!self.secure) { + self.respond('503 Secure connection not established'); + } else if (parseInt(commandArg, 10) !== 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.respond('250 File renamed successfully'); + self.pbszReceived = true; + self.respond('200 OK'); } - }); -}; + } -FtpConnection.prototype._command_SIZE = function(commandArg) { - var self = this; + _command_PROT(commandArg) { + var self = this; - var filename = withCwd(self.cwd, commandArg); - self.fs.stat(pathModule.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'); - return; + if (!self.server.options.tlsOptions) { + return self.respond('202 Not supported'); } - self.respond('213 ' + s.size + ''); - }); -}; -FtpConnection.prototype._command_TYPE = function(commandArg) { - if (commandArg === 'I' || commandArg === 'A') { - this.respond('200 OK'); - } else { - this.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'); + } } -}; -FtpConnection.prototype._command_SYST = function() { - this.respond('215 UNIX Type: I'); -}; + // Print the current working directory. + _command_PWD(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; + } -FtpConnection.prototype._command_STOR = function(commandArg) { - var filename = withCwd(this.cwd, commandArg); + _command_QUIT() { + var self = this; - if (this.server.options.useWriteFile) { - this._STOR_usingWriteFile(filename, 'w'); - } else { - this._STOR_usingCreateWriteStream(filename, null, 'w'); - } -}; - -// 'initialBuffers' argument is set when this is called from _STOR_usingWriteFile. -FtpConnection.prototype._STOR_usingCreateWriteStream = function(filename, initialBuffers, flag) { - var self = this; - - var wStreamFlags = {flags: flag || 'w', mode: 0644}; - var storeStream = self.fs.createWriteStream(pathModule.join(self.root, filename), wStreamFlags); - var notErr = true; - // Adding for event metadata for file upload (STOR) - var startTime = new Date(); - var uploadSize = 0; - - if (initialBuffers) { - //todo: handle back-pressure - initialBuffers.forEach(function(b) { - storeStream.write(b); + self.hasQuit = true; + self.respond('221 Goodbye', function(err) { + if (err) { + self._logIf(LOG.ERROR, "Error writing 'Goodbye' message following QUIT"); + } + self._closeSocket(self.socket, true); + self._closeDataConnections(); }); } - self._whenDataReady(handleUpload); + _command_RETR(commandArg) { + var filename = pathModule.join(this.root, withCwd(this.cwd, commandArg)); - storeStream.on('open', function() { - self._logIf(LOG.DEBUG, 'File opened/created: ' + filename); - self._logIf(LOG.DEBUG, 'Told client ok to send file data'); - // Adding event emitter for upload start time - self.emit('file:stor', 'open', { - user: self.username, - file: filename, - time: startTime, - }); + if (this.server.options.useReadFile) { + this._RETR_usingReadFile(commandArg, filename); + } else { + this._RETR_usingCreateReadStream(commandArg, filename); + } + } - self.respond('150 Ok to send data'); - }); + _RETR_usingCreateReadStream(commandArg, filename) { + var self = this; + var startTime = new Date(); - storeStream.on('error', function() { - self.emit('file:stor', 'error', { + self.emit('file:retr', 'open', { user: self.username, file: filename, - filesize: uploadSize, sTime: startTime, - eTime: new Date(), - duration: new Date() - startTime, - errorState: !notErr, }); - storeStream.end(); - notErr = false; - if (self.dataSocket) { - self._closeSocket(self.dataSocket, true); + + function afterOk(callback) { + self.respond('150 Opening ' + self.mode.toUpperCase() + ' mode data connection', callback); } - self.respond('426 Connection closed; transfer aborted'); - }); - storeStream.on('finish', function() { - // Adding event emitter for completed upload. - self.emit('file:stor', 'close', { + + self.fs.open(filename, 'r', function(err, fd) { + if (err) { + self.emit('file:retr', 'error', { + user: self.username, + file: filename, + filesize: 0, + sTime: startTime, + eTime: new Date(), + duration: new Date() - startTime, + errorState: true, + error: err, + }); + if (err.code === 'ENOENT') { + self.respond('550 Not Found'); + } else { // Who knows what's going on here... + self.respond('550 Not Accessible'); + self._logIf(LOG.ERROR, "Error at read of '" + filename + "' other than ENOENT " + err); + } + } else { + afterOk(function() { + self._whenDataReady(function(pasvconn) { + var readLength = 0; + var now = new Date(); + var rs = self.fs.createReadStream(null, {fd: fd}); + rs.pause(); + rs.once('error', function(err) { + self.emit('file:retr', 'close', { + user: self.username, + file: filename, + filesize: 0, + sTime: startTime, + eTime: now, + duration: now - startTime, + errorState: true, + error: err, + }); + }); + + rs.on('data', function(buffer) { + readLength += buffer.length; + }); + + rs.on('end', function() { + var now = new Date(); + self.emit('file:retr', 'close', { + user: self.username, + file: filename, + filesize: 0, + sTime: startTime, + eTime: now, + duration: now - startTime, + errorState: false, + }); + self.respond('226 Closing data connection, sent ' + readLength + ' bytes'); + }); + + rs.pipe(pasvconn); + rs.resume(); + }); + }); + } + }); + } + + _RETR_usingReadFile(commandArg, filename) { + var self = this; + var startTime = new Date(); + + self.emit('file:retr', 'open', { user: self.username, file: filename, - filesize: uploadSize, sTime: startTime, - eTime: new Date(), - duration: new Date() - startTime, - errorState: !notErr, }); - notErr ? self.respond('226 Closing data connection') : true; - if (self.dataSocket) { - self._closeSocket(self.dataSocket); + + function afterOk(callback) { + self.respond('150 Opening ' + self.mode.toUpperCase() + ' mode data connection', callback); } - }); - - function handleUpload(dataSocket) { - var isPaused = false; - dataSocket.on('data', function(buff) { - var result = storeStream.write(buff); - // Handle back-pressure - if (result === false) { - dataSocket.pause(); - isPaused = true; - storeStream.once('drain', function() { - dataSocket.resume(); - isPaused = false; - }); - } - }); - dataSocket.once('error', function() { - notErr = false; - storeStream.end(); - }); - dataSocket.once('finish', function() { - if (isPaused) { - storeStream.once('drain', function() { - storeStream.end(); + + self.fs.readFile(filename, function(err, contents) { + if (err) { + self.emit('file:retr', 'error', { + user: self.username, + file: filename, + filesize: 0, + sTime: startTime, + eTime: new Date(), + duration: new Date() - startTime, + errorState: true, + error: err, }); + if (err.code === 'ENOENT') { + self.respond('550 Not Found'); + } else { // Who knows what's going on here... + self.respond('550 Not Accessible'); + self._logIf(LOG.ERROR, "Error at read of '" + filename + "' other than ENOENT " + err); + } } else { - storeStream.end(); + afterOk(function() { + self._whenDataReady(function(pasvconn) { + contents = {filename: filename, data: contents}; + self.emit('file:retr:contents', contents); + contents = contents.data; + pasvconn.write(contents); + var contentLength = contents.length; + self.respond('226 Closing data connection, sent ' + contentLength + ' bytes'); + self.emit('file:retr', 'close', { + user: self.username, + file: filename, + filesize: contentLength, + sTime: startTime, + eTime: new Date(), + duration: new Date() - startTime, + errorState: false, + }); + self._closeSocket(pasvconn); + }); + }); } }); } -}; -FtpConnection.prototype._STOR_usingWriteFile = function(filename, flag) { - var self = this; - - var erroredOut = false; - var slurpBuf = new Buffer(1024); - var totalBytes = 0; - var startTime = new Date(); - - self.emit('file:stor', 'open', { - user: self.username, - file: filename, - time: startTime, - }); + // Remove a directory + _command_RMD(pathRequest) { + var pathServer = withCwd(this.cwd, pathRequest); + var pathFs = pathModule.join(this.root, pathServer); + this.fs.rmdir(pathFs, function(err) { + if (err) { + this._logIf(LOG.ERROR, 'RMD ' + pathRequest + ': ' + err); + this.respond('550 Delete operation failed'); + } else { + this.respond('250 "' + pathServer + '" directory removed'); + } + }.bind(this)); + return this; + } - self.respond('150 Ok to send data', function() { - self._whenDataReady(handleUpload); - }); + _command_RNFR(commandArg) { + var self = this; + self.filefrom = withCwd(self.cwd, commandArg); + self._logIf(LOG.DEBUG, 'Rename from ' + self.filefrom); + self.respond('350 Ready for destination name'); + } - function handleUpload() { - self.dataSocket.on('data', dataHandler); - self.dataSocket.once('close', closeHandler); - self.dataSocket.once('error', errorHandler); + _command_RNTO(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) { + 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' : '')); + } else { + self.respond('250 File renamed successfully'); + } + }); } - function dataHandler(buf) { - if (self.server.options.uploadMaxSlurpSize != null && - totalBytes + buf.length > self.server.options.uploadMaxSlurpSize) { - // Give up trying to slurp it -- it's too big. + _command_SIZE(commandArg) { + var self = this; - // If the 'fs' module we've been given doesn't implement 'createWriteStream', then - // we give up and send the client an error. - if (!self.fs.createWriteStream) { - if (self.dataSocket) { - self._closeSocket(self.dataSocket, true); - } - self.respond('552 Requested file action aborted; file too big'); + var filename = withCwd(self.cwd, commandArg); + self.fs.stat(pathModule.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'); return; } + self.respond('213 ' + s.size + ''); + }); + } - // Otherwise, we call _STOR_usingWriteStream, and tell it to prepend the stuff - // that we've buffered so far to the file. - self._logIf(LOG.WARN, 'uploadMaxSlurpSize exceeded; falling back to createWriteStream'); - self._STOR_usingCreateWriteStream(filename, [slurpBuf.slice(0, totalBytes), buf]); - self.dataSocket.removeListener('data', dataHandler); - self.dataSocket.removeListener('error', errorHandler); - self.dataSocket.removeListener('close', closeHandler); + _command_TYPE(commandArg) { + if (commandArg === 'I' || commandArg === 'A') { + this.respond('200 OK'); } else { - if (totalBytes + buf.length > slurpBuf.length) { - var newLength = slurpBuf.length * 2; - if (newLength < totalBytes + buf.length) { - newLength = totalBytes + buf.length; - } + this.respond('202 Not supported'); + } + } - var newSlurpBuf = new Buffer(newLength); - slurpBuf.copy(newSlurpBuf, 0, 0, totalBytes); - slurpBuf = newSlurpBuf; - } - buf.copy(slurpBuf, totalBytes, 0, buf.length); - totalBytes += buf.length; + _command_SYST() { + this.respond('215 UNIX Type: I'); + } + + _command_STOR(commandArg) { + var filename = withCwd(this.cwd, commandArg); + + if (this.server.options.useWriteFile) { + this._STOR_usingWriteFile(filename, 'w'); + } else { + this._STOR_usingCreateWriteStream(filename, null, 'w'); } } - function closeHandler() { - if (erroredOut) { - return; + // 'initialBuffers' argument is set when this is called from _STOR_usingWriteFile. + _STOR_usingCreateWriteStream(filename, initialBuffers, flag) { + var self = this; + + var wStreamFlags = {flags: flag || 'w', mode: 0o644}; + var storeStream = self.fs.createWriteStream(pathModule.join(self.root, filename), wStreamFlags); + var notErr = true; + // Adding for event metadata for file upload (STOR) + var startTime = new Date(); + var uploadSize = 0; + + if (initialBuffers) { + //todo: handle back-pressure + initialBuffers.forEach(function(b) { + storeStream.write(b); + }); } - 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.emit('file:stor', 'close', { + self._whenDataReady(handleUpload); + + storeStream.on('open', function() { + self._logIf(LOG.DEBUG, 'File opened/created: ' + filename); + self._logIf(LOG.DEBUG, 'Told client ok to send file data'); + // Adding event emitter for upload start time + self.emit('file:stor', 'open', { + user: self.username, + file: filename, + time: startTime, + }); + + self.respond('150 Ok to send data'); + }); + + storeStream.on('error', function() { + self.emit('file:stor', 'error', { user: self.username, file: filename, - filesize: totalBytes, + filesize: uploadSize, sTime: startTime, eTime: new Date(), duration: new Date() - startTime, - errorState: err ? true : false, + errorState: !notErr, }); - if (err) { - erroredOut = true; - self._logIf(LOG.ERROR, 'Error writing file. ' + err); - if (self.dataSocket) { - self._closeSocket(self.dataSocket, true); - } - self.respond('426 Connection closed; transfer aborted'); - return; + storeStream.end(); + notErr = false; + if (self.dataSocket) { + self._closeSocket(self.dataSocket, true); } + self.respond('426 Connection closed; transfer aborted'); + }); - self.respond('226 Closing data connection'); + storeStream.on('finish', function() { + // Adding event emitter for completed upload. + self.emit('file:stor', 'close', { + user: self.username, + file: filename, + filesize: uploadSize, + sTime: startTime, + eTime: new Date(), + duration: new Date() - startTime, + errorState: !notErr, + }); + notErr ? self.respond('226 Closing data connection') : true; if (self.dataSocket) { self._closeSocket(self.dataSocket); } }); - } - function errorHandler() { - erroredOut = true; + function handleUpload(dataSocket) { + var isPaused = false; + dataSocket.on('data', function(buff) { + var result = storeStream.write(buff); + // Handle back-pressure + if (result === false) { + dataSocket.pause(); + isPaused = true; + storeStream.once('drain', function() { + dataSocket.resume(); + isPaused = false; + }); + } + }); + dataSocket.once('error', function() { + notErr = false; + storeStream.end(); + }); + dataSocket.once('finish', function() { + if (isPaused) { + storeStream.once('drain', function() { + storeStream.end(); + }); + } else { + storeStream.end(); + } + }); + } } -}; -FtpConnection.prototype._command_APPE = function(commandArg) { - var filename = withCwd(this.cwd, commandArg); + _STOR_usingWriteFile(filename, flag) { + var self = this; - if (this.server.options.useWriteFile) { - this._STOR_usingWriteFile(filename, 'a'); - } else { - this._STOR_usingCreateWriteStream(filename, null, 'a'); - } -}; - -// Specify a username for login -FtpConnection.prototype._command_USER = function(username) { - var self = this; - - if (self.server.options.tlsOnly && !self.secure) { - self.respond( - '530 This server does not permit login over ' + - 'a non-secure connection; ' + - 'connect using FTP-SSL with explicit AUTH TLS'); - } else { - self.emit('command:user', username, - function success() { - self.respond('331 User name okay, need password.'); - }, - function failure() { - self.respond('530 Not logged in.'); + var erroredOut = false; + var slurpBuf = new Buffer(1024); + var totalBytes = 0; + var startTime = new Date(); + + self.emit('file:stor', 'open', { + user: self.username, + file: filename, + time: startTime, + }); + + self.respond('150 Ok to send data', function() { + self._whenDataReady(handleUpload); + }); + + 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 && + totalBytes + buf.length > self.server.options.uploadMaxSlurpSize) { + // Give up trying to slurp it -- it's too big. + + // If the 'fs' module we've been given doesn't implement 'createWriteStream', then + // we give up and send the client an error. + if (!self.fs.createWriteStream) { + if (self.dataSocket) { + self._closeSocket(self.dataSocket, true); + } + self.respond('552 Requested file action aborted; file too big'); + return; } - ); + + // Otherwise, we call _STOR_usingWriteStream, and tell it to prepend the stuff + // that we've buffered so far to the file. + self._logIf(LOG.WARN, 'uploadMaxSlurpSize exceeded; falling back to createWriteStream'); + self._STOR_usingCreateWriteStream(filename, [slurpBuf.slice(0, totalBytes), buf]); + self.dataSocket.removeListener('data', dataHandler); + self.dataSocket.removeListener('error', errorHandler); + self.dataSocket.removeListener('close', closeHandler); + } else { + if (totalBytes + buf.length > slurpBuf.length) { + var newLength = slurpBuf.length * 2; + if (newLength < totalBytes + buf.length) { + newLength = totalBytes + buf.length; + } + + var newSlurpBuf = new Buffer(newLength); + slurpBuf.copy(newSlurpBuf, 0, 0, totalBytes); + slurpBuf = newSlurpBuf; + } + buf.copy(slurpBuf, totalBytes, 0, buf.length); + totalBytes += buf.length; + } + } + + function closeHandler() { + if (erroredOut) { + return; + } + + var wOptions = {flag: flag || 'w', mode: 0o644}; + 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.emit('file:stor', 'close', { + user: self.username, + file: filename, + filesize: totalBytes, + sTime: startTime, + eTime: new Date(), + duration: new Date() - startTime, + errorState: err ? true : false, + }); + if (err) { + erroredOut = true; + self._logIf(LOG.ERROR, 'Error writing file. ' + err); + if (self.dataSocket) { + self._closeSocket(self.dataSocket, true); + } + self.respond('426 Connection closed; transfer aborted'); + return; + } + + self.respond('226 Closing data connection'); + if (self.dataSocket) { + self._closeSocket(self.dataSocket); + } + }); + } + + function errorHandler() { + erroredOut = true; + } } - return this; -}; - -// Specify a password for login -FtpConnection.prototype._command_PASS = function(password) { - var self = this; - - if (self.previousCommand !== 'USER') { - self.respond('503 Bad sequence of commands.'); - } else { - self.emit('command:pass', password, - function success(username, userFsModule) { - function panic(error, method) { - self._logIf(LOG.ERROR, method + ' signaled error ' + util.inspect(error)); - self.respond('421 Service not available, closing control connection.', function() { - self._closeSocket(self.socket, true); - }); + + _command_APPE(commandArg) { + var filename = withCwd(this.cwd, commandArg); + if (this.server.options.useWriteFile) { + this._STOR_usingWriteFile(filename, 'a'); + } else { + this._STOR_usingCreateWriteStream(filename, null, 'a'); + } + } + + // Specify a username for login + _command_USER(username) { + var self = this; + + if (self.server.options.tlsOnly && !self.secure) { + self.respond( + '530 This server does not permit login over ' + + 'a non-secure connection; ' + + 'connect using FTP-SSL with explicit AUTH TLS'); + } else { + self.emit('command:user', username, + function success() { + self.respond('331 User name okay, need password.'); + }, + function failure() { + self.respond('530 Not logged in.'); } - function setCwd(cwd) { - function setRoot(root) { - self.root = root; - self.respond('230 User logged in, proceed.'); + ); + } + return this; + } + + // Specify a password for login + _command_PASS(password) { + var self = this; + + if (self.previousCommand !== 'USER') { + self.respond('503 Bad sequence of commands.'); + } else { + self.emit('command:pass', password, + function success(username, userFsModule) { + function panic(error, method) { + self._logIf(LOG.ERROR, method + ' signaled error ' + util.inspect(error)); + self.respond('421 Service not available, closing control connection.', function() { + self._closeSocket(self.socket, true); + }); } + function setCwd(cwd) { + function setRoot(root) { + self.root = root; + self.respond('230 User logged in, proceed.'); + } - self.cwd = cwd; - if (self.server.getRoot.length <= 1) { - setRoot(self.server.getRoot(self)); + self.cwd = cwd; + if (self.server.getRoot.length <= 1) { + setRoot(self.server.getRoot(self)); + } else { + self.server.getRoot(self, function(err, root) { + if (err) { + panic(err, 'getRoot'); + } else { + setRoot(root); + } + }); + } + } + self.username = username; + self.fs = userFsModule || fsModule; + if (self.server.getInitialCwd.length <= 1) { + setCwd(withCwd(self.server.getInitialCwd(self))); } else { - self.server.getRoot(self, function(err, root) { + self.server.getInitialCwd(self, function(err, cwd) { if (err) { - panic(err, 'getRoot'); + panic(err, 'getInitialCwd'); } else { - setRoot(root); + setCwd(withCwd(cwd)); } }); } + }, + function failure() { + self.respond('530 Not logged in.'); + self.username = null; } - self.username = username; - self.fs = userFsModule || fsModule; - if (self.server.getInitialCwd.length <= 1) { - setCwd(withCwd(self.server.getInitialCwd(self))); - } else { - self.server.getInitialCwd(self, function(err, cwd) { - if (err) { - panic(err, 'getInitialCwd'); - } else { - setCwd(withCwd(cwd)); - } - }); - } - }, - function failure() { - self.respond('530 Not logged in.'); - self.username = null; - } - ); - } - return this; -}; - -FtpConnection.prototype._closeSocket = function(socket, shouldDestroy) { - // TODO: Should we always use destroy() to avoid keeping sockets open longer - // than necessary (and possibly exceeding OS max open sockets)? - if (shouldDestroy || this.server.options.destroySockets) { - // Don't call destroy() more than once. - if (!socket.destroyed) { - socket.destroy(); + ); } - } else { - // Don't call `end()` more than once. - if (socket.writable) { - socket.end(); + return this; + } + + _closeSocket(socket, shouldDestroy) { + // TODO: Should we always use destroy() to avoid keeping sockets open longer + // than necessary (and possibly exceeding OS max open sockets)? + if (shouldDestroy || this.server.options.destroySockets) { + // Don't call destroy() more than once. + if (!socket.destroyed) { + socket.destroy(); + } + } else { + // Don't call `end()` more than once. + if (socket.writable) { + socket.end(); + } } } -}; +} module.exports = FtpConnection; From bcdbfd51c6c3e6f1f63e3273a18609f9c27f0484 Mon Sep 17 00:00:00 2001 From: Simon Sturmer Date: Sun, 17 Jan 2016 16:28:35 +0700 Subject: [PATCH 10/55] ES6: refactor FtpConnection (use arrow functions) --- src/FtpConnection.js | 209 ++++++++++++++++++++++--------------------- 1 file changed, 107 insertions(+), 102 deletions(-) diff --git a/src/FtpConnection.js b/src/FtpConnection.js index 8d25924..3cd8699 100644 --- a/src/FtpConnection.js +++ b/src/FtpConnection.js @@ -1,4 +1,4 @@ -/* eslint-disable prefer-arrow-callback, camelcase */ +/* eslint-disable camelcase */ var net = require('net'); var util = require('util'); @@ -75,14 +75,14 @@ class FtpConnection extends EventEmitter { _createPassiveServer() { var self = this; - return net.createServer(function(psocket) { + return net.createServer((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'); if (self.secure) { self._logIf(LOG.INFO, 'Upgrading passive connection to TLS'); - starttls.starttlsServer(psocket, self.server.options.tlsOptions, function(err, cleartext) { + starttls.starttlsServer(psocket, self.server.options.tlsOptions, (err, cleartext) => { if (err) { self._logIf(LOG.ERROR, 'Error upgrading passive connection to TLS:' + util.inspect(err)); self._closeSocket(psocket, true); @@ -126,7 +126,7 @@ class FtpConnection extends EventEmitter { self.dataSocket.on('close', allOver('close')); self.dataSocket.on('end', allOver('end')); function allOver(ename) { - return function(err) { + return (err) => { self._logIf( (err ? LOG.ERROR : LOG.DEBUG), 'Passive data event: ' + ename + (err ? ' due to error' : '') @@ -135,7 +135,7 @@ class FtpConnection extends EventEmitter { }; } - self.dataSocket.on('error', function(err) { + self.dataSocket.on('error', (err) => { self._logIf(LOG.ERROR, 'Passive data event: error: ' + err); // TODO: Can we can rely on self.dataSocket having been closed? self.dataSocket = null; @@ -156,7 +156,7 @@ class FtpConnection extends EventEmitter { 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.dataListener.once('ready', () => { self._logIf(LOG.DEBUG, '...client has connected now'); callback(self.dataSocket); }); @@ -167,7 +167,7 @@ class FtpConnection extends EventEmitter { self._logIf(LOG.DEBUG, 'Using existing non-passive dataSocket'); callback(self.dataSocket); } else { - self._initiateData(function(sock) { + self._initiateData((sock) => { callback(sock); }); } @@ -182,7 +182,7 @@ class FtpConnection extends EventEmitter { } var sock = net.connect(self.dataPort, self.dataHost || self.socket.remoteAddress); - sock.on('connect', function() { + sock.on('connect', () => { self.dataSocket = sock; callback(sock); }); @@ -196,7 +196,7 @@ class FtpConnection extends EventEmitter { ); } - sock.on('error', function(err) { + sock.on('error', (err) => { self._closeSocket(sock, true); self._logIf(LOG.ERROR, 'Data connection error: ' + util.inspect(err)); self.dataSocket = null; @@ -310,9 +310,9 @@ class FtpConnection extends EventEmitter { return self.respond('502 Command not implemented'); } - self.respond('234 Honored', function() { + self.respond('234 Honored', () => { 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, (err, cleartext) => { if (err) { self._logIf(LOG.ERROR, 'Error upgrading connection to TLS: ' + util.inspect(err)); self._closeSocket(self.socket, true); @@ -332,7 +332,7 @@ class FtpConnection extends EventEmitter { function switchToSecure() { self._logIf(LOG.INFO, 'Secure connection started'); self.socket = cleartext; - self.socket.on('data', function(data) { + self.socket.on('data', (data) => { self._onData(data); }); self.secure = true; @@ -355,7 +355,7 @@ class FtpConnection extends EventEmitter { var pathServer = withCwd(this.cwd, pathRequest); var pathFs = pathModule.join(this.root, pathServer); var pathEscaped = pathEscape(pathServer); - this.fs.stat(pathFs, function(err, stats) { + this.fs.stat(pathFs, (err, stats) => { if (err) { this._logIf(LOG.ERROR, 'CWD ' + pathRequest + ': ' + err); this.respond('550 Directory not found.'); @@ -366,7 +366,7 @@ class FtpConnection extends EventEmitter { this.cwd = pathServer; this.respond('250 CWD successful. "' + pathEscaped + '" is current directory'); } - }.bind(this)); + }); return this; } @@ -374,7 +374,7 @@ class FtpConnection extends EventEmitter { var self = this; var filename = withCwd(self.cwd, commandArg); - self.fs.unlink(pathModule.join(self.root, filename), function(err) { + self.fs.unlink(pathModule.join(self.root, filename), (err) => { if (err) { self._logIf(LOG.ERROR, 'Error deleting file: ' + filename + ', ' + err); // write error to socket @@ -415,13 +415,13 @@ class FtpConnection extends EventEmitter { _command_MDTM(file) { file = withCwd(this.cwd, file); file = pathModule.join(this.root, file); - this.fs.stat(file, function(err, stats) { + this.fs.stat(file, (err, stats) => { if (err) { this.respond('550 File unavailable'); } else { this.respond('213 ' + dateformat(stats.mtime, 'yyyymmddhhMMss')); } - }.bind(this)); + }); return this; } @@ -461,7 +461,7 @@ class FtpConnection extends EventEmitter { 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(pathModule.join(self.root, dir), self.fs, (err, files) => { if (err) { self._logIf(LOG.ERROR, 'Error sending file list, reading directory: ' + err); self.respond('550 Not a directory'); @@ -469,7 +469,7 @@ class FtpConnection extends EventEmitter { } if (self.server.options.hideDotFiles) { - files = files.filter(function(file) { + files = files.filter((file) => { if (file.name && file.name[0] !== '.') { return true; } @@ -505,8 +505,8 @@ class FtpConnection extends EventEmitter { return i === files.length + j ? finished() : null; } - self.server.getUsernameFromUid(files[ii].stats.uid, function(e1, uname) { - self.server.getGroupFromGid(files[ii].stats.gid, function(e2, gname) { + self.server.getUsernameFromUid(files[ii].stats.uid, (e1, uname) => { + self.server.getGroupFromGid(files[ii].stats.gid, (e2, gname) => { if (e1 || e2) { self._logIf(LOG.WARN, 'Error getting user/group name for file: ' + util.inspect(e1 || e2)); fileInfos.push({ @@ -532,9 +532,7 @@ class FtpConnection extends EventEmitter { if (self.server.options.filenameSortMap !== false) { var sm = ( self.server.options.filenameSortMap || - function(x) { - return x.toUpperCase(); - } + ((x) => x.toUpperCase()) ); for (var i = 0; i < fileInfos.length; ++i) { fileInfos[i]._s = sm(detailed ? fileInfos[i].file.name : fileInfos[i].name); @@ -542,10 +540,9 @@ class FtpConnection extends EventEmitter { } var sf = (self.server.options.filenameSortFunc || - function(x, y) { - return x.localeCompare(y); - }); - fileInfos = fileInfos.sort(function(x, y) { + ((x, y) => x.localeCompare(y)) + ); + fileInfos = fileInfos.sort((x, y) => { if (self.server.options.filenameSortMap !== false) { return sf(x._s, y._s); } else if (detailed) { @@ -573,7 +570,7 @@ class FtpConnection extends EventEmitter { LIST: m, NLST: m, STAT: '213 End of status', }; - self.respond(BEGIN_MSGS[cmd], function() { + self.respond(BEGIN_MSGS[cmd], () => { if (cmd === 'STAT') { whenReady(self.socket); } else { @@ -638,14 +635,14 @@ class FtpConnection extends EventEmitter { var pathServer = withCwd(this.cwd, pathRequest); var pathEscaped = pathEscape(pathServer); var pathFs = pathModule.join(this.root, pathServer); - this.fs.mkdir(pathFs, 0o755, function(err) { + this.fs.mkdir(pathFs, 0o755, (err) => { if (err) { this._logIf(LOG.ERROR, 'MKD ' + pathRequest + ': ' + err); this.respond('550 "' + pathEscaped + '" directory NOT created'); } else { this.respond('257 "' + pathEscaped + '" directory created'); } - }.bind(this)); + }); return this; } @@ -788,7 +785,7 @@ class FtpConnection extends EventEmitter { // (iii) We run out of ports to try. var i = self.server.options.pasvPortRangeStart; pasv.listen(i); - portRangeErrorHandler = function(e) { + portRangeErrorHandler = (e) => { if (e.code === 'EADDRINUSE' && i < self.server.options.pasvPortRangeEnd) { pasv.listen(++i); } else { @@ -803,7 +800,7 @@ class FtpConnection extends EventEmitter { } // Once we're successfully listening, tell the client - pasv.on('listening', function() { + pasv.on('listening', () => { self.pasv = pasv; if (portRangeErrorHandler) { @@ -818,7 +815,7 @@ class FtpConnection extends EventEmitter { self._logIf(LOG.DEBUG, 'Passive data connection listening on port ' + port); self._writePASVReady(command); }); - pasv.on('close', function() { + pasv.on('close', () => { self.pasv = null; self.dataListener = null; self._logIf(LOG.DEBUG, 'Passive data listener closed'); @@ -882,7 +879,7 @@ class FtpConnection extends EventEmitter { var self = this; self.hasQuit = true; - self.respond('221 Goodbye', function(err) { + self.respond('221 Goodbye', (err) => { if (err) { self._logIf(LOG.ERROR, "Error writing 'Goodbye' message following QUIT"); } @@ -916,7 +913,7 @@ class FtpConnection extends EventEmitter { } - self.fs.open(filename, 'r', function(err, fd) { + self.fs.open(filename, 'r', (err, fd) => { if (err) { self.emit('file:retr', 'error', { user: self.username, @@ -935,13 +932,13 @@ class FtpConnection extends EventEmitter { self._logIf(LOG.ERROR, "Error at read of '" + filename + "' other than ENOENT " + err); } } else { - afterOk(function() { - self._whenDataReady(function(pasvconn) { + afterOk(() => { + self._whenDataReady((pasvconn) => { var readLength = 0; var now = new Date(); var rs = self.fs.createReadStream(null, {fd: fd}); rs.pause(); - rs.once('error', function(err) { + rs.once('error', (err) => { self.emit('file:retr', 'close', { user: self.username, file: filename, @@ -954,11 +951,11 @@ class FtpConnection extends EventEmitter { }); }); - rs.on('data', function(buffer) { + rs.on('data', (buffer) => { readLength += buffer.length; }); - rs.on('end', function() { + rs.on('end', () => { var now = new Date(); self.emit('file:retr', 'close', { user: self.username, @@ -994,7 +991,7 @@ class FtpConnection extends EventEmitter { self.respond('150 Opening ' + self.mode.toUpperCase() + ' mode data connection', callback); } - self.fs.readFile(filename, function(err, contents) { + self.fs.readFile(filename, (err, contents) => { if (err) { self.emit('file:retr', 'error', { user: self.username, @@ -1013,8 +1010,8 @@ class FtpConnection extends EventEmitter { self._logIf(LOG.ERROR, "Error at read of '" + filename + "' other than ENOENT " + err); } } else { - afterOk(function() { - self._whenDataReady(function(pasvconn) { + afterOk(() => { + self._whenDataReady((pasvconn) => { contents = {filename: filename, data: contents}; self.emit('file:retr:contents', contents); contents = contents.data; @@ -1041,14 +1038,14 @@ class FtpConnection extends EventEmitter { _command_RMD(pathRequest) { var pathServer = withCwd(this.cwd, pathRequest); var pathFs = pathModule.join(this.root, pathServer); - this.fs.rmdir(pathFs, function(err) { + this.fs.rmdir(pathFs, (err) => { if (err) { this._logIf(LOG.ERROR, 'RMD ' + pathRequest + ': ' + err); this.respond('550 Delete operation failed'); } else { this.respond('250 "' + pathServer + '" directory removed'); } - }.bind(this)); + }); return this; } @@ -1062,7 +1059,7 @@ class FtpConnection extends EventEmitter { _command_RNTO(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(pathModule.join(self.root, self.filefrom), pathModule.join(self.root, fileto), (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' : '')); @@ -1076,7 +1073,7 @@ class FtpConnection extends EventEmitter { var self = this; var filename = withCwd(self.cwd, commandArg); - self.fs.stat(pathModule.join(self.root, filename), function(err, s) { + self.fs.stat(pathModule.join(self.root, filename), (err, s) => { if (err) { self._logIf(LOG.ERROR, "Error getting size of file '" + filename + "' "); self.respond('450 Failed to get size of file'); @@ -1121,14 +1118,14 @@ class FtpConnection extends EventEmitter { if (initialBuffers) { //todo: handle back-pressure - initialBuffers.forEach(function(b) { + initialBuffers.forEach((b) => { storeStream.write(b); }); } self._whenDataReady(handleUpload); - storeStream.on('open', function() { + storeStream.on('open', () => { self._logIf(LOG.DEBUG, 'File opened/created: ' + filename); self._logIf(LOG.DEBUG, 'Told client ok to send file data'); // Adding event emitter for upload start time @@ -1141,7 +1138,7 @@ class FtpConnection extends EventEmitter { self.respond('150 Ok to send data'); }); - storeStream.on('error', function() { + storeStream.on('error', () => { self.emit('file:stor', 'error', { user: self.username, file: filename, @@ -1159,7 +1156,7 @@ class FtpConnection extends EventEmitter { self.respond('426 Connection closed; transfer aborted'); }); - storeStream.on('finish', function() { + storeStream.on('finish', () => { // Adding event emitter for completed upload. self.emit('file:stor', 'close', { user: self.username, @@ -1178,25 +1175,25 @@ class FtpConnection extends EventEmitter { function handleUpload(dataSocket) { var isPaused = false; - dataSocket.on('data', function(buff) { + dataSocket.on('data', (buff) => { var result = storeStream.write(buff); // Handle back-pressure if (result === false) { dataSocket.pause(); isPaused = true; - storeStream.once('drain', function() { + storeStream.once('drain', () => { dataSocket.resume(); isPaused = false; }); } }); - dataSocket.once('error', function() { + dataSocket.once('error', () => { notErr = false; storeStream.end(); }); - dataSocket.once('finish', function() { + dataSocket.once('finish', () => { if (isPaused) { - storeStream.once('drain', function() { + storeStream.once('drain', () => { storeStream.end(); }); } else { @@ -1220,7 +1217,7 @@ class FtpConnection extends EventEmitter { time: startTime, }); - self.respond('150 Ok to send data', function() { + self.respond('150 Ok to send data', () => { self._whenDataReady(handleUpload); }); @@ -1276,7 +1273,7 @@ class FtpConnection extends EventEmitter { var wOptions = {flag: flag || 'w', mode: 0o644}; 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(pathModule.join(self.root, filename), contents.data, wOptions, (err) => { self.emit('file:stor', 'close', { user: self.username, file: filename, @@ -1327,13 +1324,17 @@ class FtpConnection extends EventEmitter { 'a non-secure connection; ' + 'connect using FTP-SSL with explicit AUTH TLS'); } else { - self.emit('command:user', username, - function success() { - self.respond('331 User name okay, need password.'); - }, - function failure() { - self.respond('530 Not logged in.'); - } + self.emit( + 'command:user', + username, + // success callback + () => { + self.respond('331 User name okay, need password.'); + }, + // failure callback + () => { + self.respond('530 Not logged in.'); + } ); } return this; @@ -1346,51 +1347,55 @@ class FtpConnection extends EventEmitter { if (self.previousCommand !== 'USER') { self.respond('503 Bad sequence of commands.'); } else { - self.emit('command:pass', password, - function success(username, userFsModule) { - function panic(error, method) { - self._logIf(LOG.ERROR, method + ' signaled error ' + util.inspect(error)); - self.respond('421 Service not available, closing control connection.', function() { - self._closeSocket(self.socket, true); - }); + self.emit( + 'command:pass', + password, + // success callback + (username, userFsModule) => { + function panic(error, method) { + self._logIf(LOG.ERROR, method + ' signaled error ' + util.inspect(error)); + self.respond('421 Service not available, closing control connection.', () => { + self._closeSocket(self.socket, true); + }); + } + function setCwd(cwd) { + function setRoot(root) { + self.root = root; + self.respond('230 User logged in, proceed.'); } - function setCwd(cwd) { - function setRoot(root) { - self.root = root; - self.respond('230 User logged in, proceed.'); - } - self.cwd = cwd; - if (self.server.getRoot.length <= 1) { - setRoot(self.server.getRoot(self)); - } else { - self.server.getRoot(self, function(err, root) { - if (err) { - panic(err, 'getRoot'); - } else { - setRoot(root); - } - }); - } - } - self.username = username; - self.fs = userFsModule || fsModule; - if (self.server.getInitialCwd.length <= 1) { - setCwd(withCwd(self.server.getInitialCwd(self))); + self.cwd = cwd; + if (self.server.getRoot.length <= 1) { + setRoot(self.server.getRoot(self)); } else { - self.server.getInitialCwd(self, function(err, cwd) { + self.server.getRoot(self, (err, root) => { if (err) { - panic(err, 'getInitialCwd'); + panic(err, 'getRoot'); } else { - setCwd(withCwd(cwd)); + setRoot(root); } }); } - }, - function failure() { - self.respond('530 Not logged in.'); - self.username = null; } + self.username = username; + self.fs = userFsModule || fsModule; + if (self.server.getInitialCwd.length <= 1) { + setCwd(withCwd(self.server.getInitialCwd(self))); + } else { + self.server.getInitialCwd(self, (err, cwd) => { + if (err) { + panic(err, 'getInitialCwd'); + } else { + setCwd(withCwd(cwd)); + } + }); + } + }, + // failure callback + () => { + self.respond('530 Not logged in.'); + self.username = null; + } ); } return this; From e347422f745a18ae7ffb34aed290544f98ee03e8 Mon Sep 17 00:00:00 2001 From: Simon Sturmer Date: Sun, 17 Jan 2016 16:28:59 +0700 Subject: [PATCH 11/55] ES6: refactor FtpConnection (remove self) --- src/FtpConnection.js | 623 ++++++++++++++++++++----------------------- 1 file changed, 290 insertions(+), 333 deletions(-) diff --git a/src/FtpConnection.js b/src/FtpConnection.js index 3cd8699..14d79d6 100644 --- a/src/FtpConnection.js +++ b/src/FtpConnection.js @@ -73,101 +73,97 @@ class FtpConnection extends EventEmitter { } _createPassiveServer() { - var self = this; - return net.createServer((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'); + this._logIf(LOG.INFO, 'Passive data event: connect'); - if (self.secure) { - self._logIf(LOG.INFO, 'Upgrading passive connection to TLS'); - starttls.starttlsServer(psocket, self.server.options.tlsOptions, (err, cleartext) => { + if (this.secure) { + this._logIf(LOG.INFO, 'Upgrading passive connection to TLS'); + starttls.starttlsServer(psocket, this.server.options.tlsOptions, (err, cleartext) => { if (err) { - self._logIf(LOG.ERROR, 'Error upgrading passive connection to TLS:' + util.inspect(err)); - self._closeSocket(psocket, true); - self.dataConfigured = false; + this._logIf(LOG.ERROR, 'Error upgrading passive connection to TLS:' + util.inspect(err)); + this._closeSocket(psocket, true); + this.dataConfigured = false; } else if (!cleartext.authorized) { - if (self.server.options.allowUnauthorizedTls) { - self._logIf(LOG.INFO, 'Allowing unauthorized passive connection (allowUnauthorizedTls is on)'); + if (this.server.options.allowUnauthorizedTls) { + this._logIf(LOG.INFO, 'Allowing unauthorized passive connection (allowUnauthorizedTls is on)'); switchToSecure(); } else { - self._logIf(LOG.INFO, 'Closing unauthorized passive connection (allowUnauthorizedTls is off)'); - self._closeSocket(self.socket, true); - self.dataConfigured = false; + this._logIf(LOG.INFO, 'Closing unauthorized passive connection (allowUnauthorizedTls is off)'); + this._closeSocket(this.socket, true); + this.dataConfigured = false; } } else { switchToSecure(); } function switchToSecure() { - self._logIf(LOG.INFO, 'Secure passive connection started'); + this._logIf(LOG.INFO, 'Secure passive connection started'); // TODO: Check for existing dataSocket. - self.dataSocket = cleartext; + this.dataSocket = cleartext; setupPassiveListener(); } }); } else { // TODO: Check for existing dataSocket. - self.dataSocket = psocket; + this.dataSocket = psocket; setupPassiveListener(); } function setupPassiveListener() { - if (self.dataListener) { - self.dataListener.emit('ready'); + if (this.dataListener) { + this.dataListener.emit('ready'); } else { - self._logIf(LOG.WARN, 'Passive connection initiated, but no data listener'); + this._logIf(LOG.WARN, 'Passive connection initiated, but no data listener'); } // 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')); + this.dataSocket.on('close', allOver('close')); + this.dataSocket.on('end', allOver('end')); function allOver(ename) { return (err) => { - self._logIf( + this._logIf( (err ? LOG.ERROR : LOG.DEBUG), 'Passive data event: ' + ename + (err ? ' due to error' : '') ); - self.dataSocket = null; + this.dataSocket = null; }; } - self.dataSocket.on('error', (err) => { - self._logIf(LOG.ERROR, 'Passive data event: error: ' + err); + this.dataSocket.on('error', (err) => { + this._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; + this.dataSocket = null; + this.dataConfigured = false; }); } }); } _whenDataReady(callback) { - var self = this; - - if (self.dataListener) { + if (this.dataListener) { // 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); + if (this.dataSocket) { + this._logIf(LOG.DEBUG, 'A data connection exists'); + callback(this.dataSocket); } else { - self._logIf(LOG.DEBUG, 'Currently no data connection; expecting client to connect to pasv server shortly...'); - self.dataListener.once('ready', () => { - self._logIf(LOG.DEBUG, '...client has connected now'); - callback(self.dataSocket); + this._logIf(LOG.DEBUG, 'Currently no data connection; expecting client to connect to pasv server shortly...'); + this.dataListener.once('ready', () => { + this._logIf(LOG.DEBUG, '...client has connected now'); + callback(this.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); + if (this.dataSocket) { // There really shouldn't be an existing connection + this._logIf(LOG.DEBUG, 'Using existing non-passive dataSocket'); + callback(this.dataSocket); } else { - self._initiateData((sock) => { + this._initiateData((sock) => { callback(sock); }); } @@ -175,32 +171,30 @@ class FtpConnection extends EventEmitter { } _initiateData(callback) { - var self = this; - - if (self.dataSocket) { - return callback(self.dataSocket); + if (this.dataSocket) { + return callback(this.dataSocket); } - var sock = net.connect(self.dataPort, self.dataHost || self.socket.remoteAddress); + var sock = net.connect(this.dataPort, this.dataHost || this.socket.remoteAddress); sock.on('connect', () => { - self.dataSocket = sock; + this.dataSocket = sock; callback(sock); }); sock.on('end', allOver); sock.on('close', allOver); function allOver(err) { - self.dataSocket = null; - self._logIf( + this.dataSocket = null; + this._logIf( err ? LOG.ERROR : LOG.DEBUG, 'Non-passive data connection ended' + (err ? 'due to error: ' + util.inspect(err) : '') ); } sock.on('error', (err) => { - self._closeSocket(sock, true); - self._logIf(LOG.ERROR, 'Data connection error: ' + util.inspect(err)); - self.dataSocket = null; - self.dataConfigured = false; + this._closeSocket(sock, true); + this._logIf(LOG.ERROR, 'Data connection error: ' + util.inspect(err)); + this.dataSocket = null; + this.dataConfigured = false; }); } @@ -234,16 +228,14 @@ class FtpConnection extends EventEmitter { } _onData(data) { - var self = this; - - if (self.hasQuit) { + if (this.hasQuit) { return; } data = data.toString('utf-8').trim(); - self._logIf(LOG.TRACE, '<< ' + data); + this._logIf(LOG.TRACE, '<< ' + data); // Don't want to include passwords in logs. - self._logIf(LOG.INFO, 'FTP command: ' + + this._logIf(LOG.INFO, 'FTP command: ' + data.replace(/^PASS [\s\S]*$/i, 'PASS ***') ); @@ -260,35 +252,35 @@ class FtpConnection extends EventEmitter { } var m = '_command_' + command; - if (self[m]) { - if (self.allowedCommands != null && self.allowedCommands[command] !== true) { - self.respond('502 ' + command + ' not implemented.'); + if (this[m]) { + if (this.allowedCommands != null && this.allowedCommands[command] !== true) { + this.respond('502 ' + command + ' not implemented.'); } else if (DOES_NOT_REQUIRE_AUTH[command]) { - self[m](commandArg, command); + this[m](commandArg, command); } else { // If 'tlsOnly' option is set, all commands which require user authentication will only // 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()) { + if (!this.secure && this.server.options.tlsOnly) { + this.respond('522 Protection level not sufficient; send AUTH TLS'); + } else if (this._authenticated()) { checkData(); } else { - self.respond('530 Not logged in.'); + this.respond('530 Not logged in.'); } } function checkData() { - if (REQUIRES_CONFIGURED_DATA[command] && !self.dataConfigured) { - self.respond('425 Data connection not configured; send PASV or PORT'); + if (REQUIRES_CONFIGURED_DATA[command] && !this.dataConfigured) { + this.respond('425 Data connection not configured; send PASV or PORT'); return; } - self[m](commandArg, command); + this[m](commandArg, command); } } else { - self.respond('502 Command not implemented.'); + this.respond('502 Command not implemented.'); } - self.previousCommand = command; + this.previousCommand = command; } // Specify the user's account (superfluous) @@ -304,38 +296,36 @@ class FtpConnection extends EventEmitter { } _command_AUTH(commandArg) { - var self = this; - - if (!self.server.options.tlsOptions || commandArg !== 'TLS') { - return self.respond('502 Command not implemented'); + if (!this.server.options.tlsOptions || commandArg !== 'TLS') { + return this.respond('502 Command not implemented'); } - self.respond('234 Honored', () => { - self._logIf(LOG.INFO, 'Establishing secure connection...'); - starttls.starttlsServer(self.socket, self.server.options.tlsOptions, (err, cleartext) => { + this.respond('234 Honored', () => { + this._logIf(LOG.INFO, 'Establishing secure connection...'); + starttls.starttlsServer(this.socket, this.server.options.tlsOptions, (err, cleartext) => { if (err) { - self._logIf(LOG.ERROR, 'Error upgrading connection to TLS: ' + util.inspect(err)); - self._closeSocket(self.socket, true); + this._logIf(LOG.ERROR, 'Error upgrading connection to TLS: ' + util.inspect(err)); + this._closeSocket(this.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)'); + this._logIf(LOG.INFO, 'Secure socket not authorized: ' + util.inspect(cleartext.authorizationError)); + if (this.server.options.allowUnauthorizedTls) { + this._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); + this._logIf(LOG.INFO, 'Closing unauthorized connection (allowUnauthorizedTls is off)'); + this._closeSocket(this.socket, true); } } else { switchToSecure(); } function switchToSecure() { - self._logIf(LOG.INFO, 'Secure connection started'); - self.socket = cleartext; - self.socket.on('data', (data) => { - self._onData(data); + this._logIf(LOG.INFO, 'Secure connection started'); + this.socket = cleartext; + this.socket.on('data', (data) => { + this._onData(data); }); - self.secure = true; + this.secure = true; } }); }); @@ -371,16 +361,14 @@ class FtpConnection extends EventEmitter { } _command_DELE(commandArg) { - var self = this; - - var filename = withCwd(self.cwd, commandArg); - self.fs.unlink(pathModule.join(self.root, filename), (err) => { + var filename = withCwd(this.cwd, commandArg); + this.fs.unlink(pathModule.join(this.root, filename), (err) => { if (err) { - self._logIf(LOG.ERROR, 'Error deleting file: ' + filename + ', ' + err); + this._logIf(LOG.ERROR, 'Error deleting file: ' + filename + ', ' + err); // write error to socket - self.respond('550 Permission denied'); + this.respond('550 Permission denied'); } else { - self.respond('250 File deleted'); + this.respond('250 File deleted'); } }); } @@ -453,22 +441,20 @@ class FtpConnection extends EventEmitter { The server may reject the LIST or NLST request (with code 450 or 550) without first responding with a mark. In this case the server does not touch the data connection. */ - var self = this; - // LIST may be passed options (-a in particular). We just ignore any of these. // (In the particular case of -a, we show hidden files anyway.) var dirname = stripOptions(commandArg); - var dir = withCwd(self.cwd, dirname); + var dir = withCwd(this.cwd, dirname); - glob.setMaxStatsAtOnce(self.server.options.maxStatsAtOnce); - glob.glob(pathModule.join(self.root, dir), self.fs, (err, files) => { + glob.setMaxStatsAtOnce(this.server.options.maxStatsAtOnce); + glob.glob(pathModule.join(this.root, dir), this.fs, (err, files) => { if (err) { - self._logIf(LOG.ERROR, 'Error sending file list, reading directory: ' + err); - self.respond('550 Not a directory'); + this._logIf(LOG.ERROR, 'Error sending file list, reading directory: ' + err); + this.respond('550 Not a directory'); return; } - if (self.server.options.hideDotFiles) { + if (this.server.options.hideDotFiles) { files = files.filter((file) => { if (file.name && file.name[0] !== '.') { return true; @@ -476,9 +462,9 @@ class FtpConnection extends EventEmitter { }); } - self._logIf(LOG.INFO, 'Directory has ' + files.length + ' files'); + this._logIf(LOG.INFO, 'Directory has ' + files.length + ' files'); if (files.length === 0) { - return self._listFiles([], detailed, cmd); + return this._listFiles([], detailed, cmd); } var fileInfos; // To contain list of files with info for each. @@ -493,7 +479,7 @@ class FtpConnection extends EventEmitter { // Now we need to get username and group name for each file from user/group ids. fileInfos = []; - var CONC = self.server.options.maxStatsAtOnce; + var CONC = this.server.options.maxStatsAtOnce; var j = 0; for (var i = 0; i < files.length && i < CONC; ++i) { handleFile(i); @@ -505,10 +491,10 @@ class FtpConnection extends EventEmitter { return i === files.length + j ? finished() : null; } - self.server.getUsernameFromUid(files[ii].stats.uid, (e1, uname) => { - self.server.getGroupFromGid(files[ii].stats.gid, (e2, gname) => { + this.server.getUsernameFromUid(files[ii].stats.uid, (e1, uname) => { + this.server.getGroupFromGid(files[ii].stats.gid, (e2, gname) => { if (e1 || e2) { - self._logIf(LOG.WARN, 'Error getting user/group name for file: ' + util.inspect(e1 || e2)); + this._logIf(LOG.WARN, 'Error getting user/group name for file: ' + util.inspect(e1 || e2)); fileInfos.push({ file: files[ii], uname: null, @@ -528,10 +514,10 @@ class FtpConnection extends EventEmitter { function finished() { // Sort file names. - if (!self.server.options.dontSortFilenames) { - if (self.server.options.filenameSortMap !== false) { + if (!this.server.options.dontSortFilenames) { + if (this.server.options.filenameSortMap !== false) { var sm = ( - self.server.options.filenameSortMap || + this.server.options.filenameSortMap || ((x) => x.toUpperCase()) ); for (var i = 0; i < fileInfos.length; ++i) { @@ -539,11 +525,11 @@ class FtpConnection extends EventEmitter { } } - var sf = (self.server.options.filenameSortFunc || + var sf = (this.server.options.filenameSortFunc || ((x, y) => x.localeCompare(y)) ); fileInfos = fileInfos.sort((x, y) => { - if (self.server.options.filenameSortMap !== false) { + if (this.server.options.filenameSortMap !== false) { return sf(x._s, y._s); } else if (detailed) { return sf(x.file.name, y.file.name); @@ -553,14 +539,12 @@ class FtpConnection extends EventEmitter { }); } - self._listFiles(fileInfos, detailed, cmd); + this._listFiles(fileInfos, detailed, cmd); } - }, self.server.options.noWildcards); + }, this.server.options.noWildcards); } _listFiles(fileInfos, detailed, cmd) { - var self = this; - var m = '150 Here comes the directory listing'; var BEGIN_MSGS = { LIST: m, NLST: m, STAT: '213-Status follows', @@ -570,11 +554,11 @@ class FtpConnection extends EventEmitter { LIST: m, NLST: m, STAT: '213 End of status', }; - self.respond(BEGIN_MSGS[cmd], () => { + this.respond(BEGIN_MSGS[cmd], () => { if (cmd === 'STAT') { - whenReady(self.socket); + whenReady(this.socket); } else { - self._whenDataReady(whenReady); + this._whenDataReady(whenReady); } function whenReady(listconn) { @@ -584,16 +568,16 @@ class FtpConnection extends EventEmitter { function success(err) { if (err) { - self.respond('550 Error listing files'); + this.respond('550 Error listing files'); } else { - self.respond(END_MSGS[cmd]); + this.respond(END_MSGS[cmd]); } if (cmd !== 'STAT') { - self._closeSocket(listconn); + this._closeSocket(listconn); } } - self._logIf(LOG.DEBUG, 'Sending file list'); + this._logIf(LOG.DEBUG, 'Sending file list'); for (var i = 0; i < fileInfos.length; ++i) { var fileInfo = fileInfos[i]; @@ -620,7 +604,7 @@ class FtpConnection extends EventEmitter { line += file.name; line += '\r\n'; } - self._writeText( + this._writeText( listconn, line, (i === fileInfos.length - 1 ? success : undefined) @@ -661,15 +645,14 @@ class FtpConnection extends EventEmitter { } _PORT(commandArg, command) { - var self = this; var m; - self.dataConfigured = false; + this.dataConfigured = false; 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})$/); if (!m) { - self.respond('501 Bad argument to PORT'); + this.respond('501 Bad argument to PORT'); return; } @@ -683,13 +666,13 @@ class FtpConnection extends EventEmitter { if (commandArg.length >= 3 && commandArg.charAt(0) === '|' && commandArg.charAt(2) === '|' && commandArg.charAt(1) === '2') { // Only IPv4 is supported. - self.respond('522 Server cannot handle IPv6 EPRT commands, use (1)'); + this.respond('522 Server cannot handle IPv6 EPRT commands, use (1)'); return; } m = commandArg.match(/^\|1\|([0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3})\|([0-9]{1,5})/); if (!m) { - self.respond('501 Bad Argument to EPRT'); + this.respond('501 Bad Argument to EPRT'); return; } @@ -699,7 +682,7 @@ class FtpConnection extends EventEmitter { throw new Error('Impossible NaN in FtpConnection.prototype._PORT (2)'); } if (r > 65535 || r <= 0) { - self.respond('501 Bad argument to EPRT (invalid port number)'); + this.respond('501 Bad argument to EPRT (invalid port number)'); return; } @@ -707,11 +690,11 @@ class FtpConnection extends EventEmitter { port = r; } - self.dataConfigured = true; - self.dataHost = host; - self.dataPort = port; - self._logIf(LOG.DEBUG, 'self.dataHost, self.dataPort set to ' + self.dataHost + ':' + self.dataPort); - self.respond('200 OK'); + this.dataConfigured = true; + this.dataHost = host; + this.dataPort = port; + this._logIf(LOG.DEBUG, 'self.dataHost, self.dataPort set to ' + this.dataHost + ':' + this.dataPort); + this.respond('200 OK'); } _command_PASV(x, y) { @@ -723,73 +706,67 @@ class FtpConnection extends EventEmitter { } _PASV(commandArg, command) { - var self = this; - - self.dataConfigured = false; + this.dataConfigured = false; if (command === 'EPSV' && commandArg && commandArg !== '1') { - self.respond('202 Not supported'); + this.respond('202 Not supported'); return; } // not sure whether the spec limits to 1 data connection at a time ... - if (self.dataSocket) { - self._closeSocket(self.dataSocket, true); + if (this.dataSocket) { + this._closeSocket(this.dataSocket, true); } - if (self.dataListener) { - self._logIf(LOG.DEBUG, 'Telling client that they can connect now'); - self._writePASVReady(command); + if (this.dataListener) { + this._logIf(LOG.DEBUG, 'Telling client that they can connect now'); + this._writePASVReady(command); } else { - self._logIf(LOG.DEBUG, 'Setting up listener for passive connections'); - self._setupNewPASV(commandArg, command); + this._logIf(LOG.DEBUG, 'Setting up listener for passive connections'); + this._setupNewPASV(commandArg, command); } - self.dataConfigured = true; + this.dataConfigured = true; } _writePASVReady(command) { - var self = this; - - var a = self.pasv.address(); - var host = self.server.host; + var a = this.pasv.address(); + var host = this.server.host; var port = a.port; if (command === 'PASV') { var i1 = (port / 256) | 0; var i2 = port % 256; - self.respond('227 Entering Passive Mode (' + host.split('.').join(',') + ',' + i1 + ',' + i2 + ')'); + this.respond('227 Entering Passive Mode (' + host.split('.').join(',') + ',' + i1 + ',' + i2 + ')'); } else { // EPASV - self.respond('229 Entering Extended Passive Mode (|||' + port + '|)'); + this.respond('229 Entering Extended Passive Mode (|||' + port + '|)'); } } _setupNewPASV(commandArg, command) { - var self = this; - - var pasv = self._createPassiveServer(); + var pasv = this._createPassiveServer(); var portRangeErrorHandler; function normalErrorHandler(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.dataListener = null; - self.dataSocket = null; - self.pasv = null; + this._logIf(LOG.WARN, 'Error with passive data listener: ' + util.inspect(e)); + this.respond('421 Server was unable to open passive connection listener'); + this.dataConfigured = false; + this.dataListener = null; + this.dataSocket = null; + this.pasv = null; } - if (self.server.options.pasvPortRangeStart != null && self.server.options.pasvPortRangeEnd != null) { + if (this.server.options.pasvPortRangeStart != null && this.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; + var i = this.server.options.pasvPortRangeStart; pasv.listen(i); portRangeErrorHandler = (e) => { - if (e.code === 'EADDRINUSE' && i < self.server.options.pasvPortRangeEnd) { + if (e.code === 'EADDRINUSE' && i < this.server.options.pasvPortRangeEnd) { pasv.listen(++i); } else { - self._logIf(LOG.DEBUG, 'Passing on error from portRangeErrorHandler to normalErrorHandler:' + JSON.stringify(e)); + this._logIf(LOG.DEBUG, 'Passing on error from portRangeErrorHandler to normalErrorHandler:' + JSON.stringify(e)); normalErrorHandler(e); } }; @@ -801,66 +778,62 @@ class FtpConnection extends EventEmitter { // Once we're successfully listening, tell the client pasv.on('listening', () => { - self.pasv = pasv; + this.pasv = pasv; if (portRangeErrorHandler) { pasv.removeListener('error', portRangeErrorHandler); pasv.addListener('error', normalErrorHandler); } - self._logIf(LOG.DEBUG, 'Passive data connection beginning to listen'); + this._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); + this.dataListener = new PassiveListener(); + this._logIf(LOG.DEBUG, 'Passive data connection listening on port ' + port); + this._writePASVReady(command); }); pasv.on('close', () => { - self.pasv = null; - self.dataListener = null; - self._logIf(LOG.DEBUG, 'Passive data listener closed'); + this.pasv = null; + this.dataListener = null; + this._logIf(LOG.DEBUG, 'Passive data listener closed'); }); } _command_PBSZ(commandArg) { - var self = this; - - if (!self.server.options.tlsOptions) { - return self.respond('202 Not supported'); + if (!this.server.options.tlsOptions) { + return this.respond('202 Not supported'); } // Protection Buffer Size (RFC 2228) - if (!self.secure) { - self.respond('503 Secure connection not established'); + if (!this.secure) { + this.respond('503 Secure connection not established'); } else if (parseInt(commandArg, 10) !== 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'); + this.pbszReceived = true; + this.respond('200 buffer too big, PBSZ=0'); } else { - self.pbszReceived = true; - self.respond('200 OK'); + this.pbszReceived = true; + this.respond('200 OK'); } } _command_PROT(commandArg) { - var self = this; - - if (!self.server.options.tlsOptions) { - return self.respond('202 Not supported'); + if (!this.server.options.tlsOptions) { + return this.respond('202 Not supported'); } - if (!self.pbszReceived) { - self.respond('503 No PBSZ command received'); + if (!this.pbszReceived) { + this.respond('503 No PBSZ command received'); } else if (commandArg === 'S' || commandArg === 'E' || commandArg === 'C') { - self.respond('536 Not supported'); + this.respond('536 Not supported'); } else if (commandArg === 'P') { - self.respond('200 OK'); + this.respond('200 OK'); } else { // Don't even recognize this one... - self.respond('504 Not recognized'); + this.respond('504 Not recognized'); } } @@ -876,15 +849,13 @@ class FtpConnection extends EventEmitter { } _command_QUIT() { - var self = this; - - self.hasQuit = true; - self.respond('221 Goodbye', (err) => { + this.hasQuit = true; + this.respond('221 Goodbye', (err) => { if (err) { - self._logIf(LOG.ERROR, "Error writing 'Goodbye' message following QUIT"); + this._logIf(LOG.ERROR, "Error writing 'Goodbye' message following QUIT"); } - self._closeSocket(self.socket, true); - self._closeDataConnections(); + this._closeSocket(this.socket, true); + this._closeDataConnections(); }); } @@ -899,24 +870,23 @@ class FtpConnection extends EventEmitter { } _RETR_usingCreateReadStream(commandArg, filename) { - var self = this; var startTime = new Date(); - self.emit('file:retr', 'open', { - user: self.username, + this.emit('file:retr', 'open', { + user: this.username, file: filename, sTime: startTime, }); function afterOk(callback) { - self.respond('150 Opening ' + self.mode.toUpperCase() + ' mode data connection', callback); + this.respond('150 Opening ' + this.mode.toUpperCase() + ' mode data connection', callback); } - self.fs.open(filename, 'r', (err, fd) => { + this.fs.open(filename, 'r', (err, fd) => { if (err) { - self.emit('file:retr', 'error', { - user: self.username, + this.emit('file:retr', 'error', { + user: this.username, file: filename, filesize: 0, sTime: startTime, @@ -926,21 +896,21 @@ class FtpConnection extends EventEmitter { error: err, }); if (err.code === 'ENOENT') { - self.respond('550 Not Found'); + this.respond('550 Not Found'); } else { // Who knows what's going on here... - self.respond('550 Not Accessible'); - self._logIf(LOG.ERROR, "Error at read of '" + filename + "' other than ENOENT " + err); + this.respond('550 Not Accessible'); + this._logIf(LOG.ERROR, "Error at read of '" + filename + "' other than ENOENT " + err); } } else { afterOk(() => { - self._whenDataReady((pasvconn) => { + this._whenDataReady((pasvconn) => { var readLength = 0; var now = new Date(); - var rs = self.fs.createReadStream(null, {fd: fd}); + var rs = this.fs.createReadStream(null, {fd: fd}); rs.pause(); rs.once('error', (err) => { - self.emit('file:retr', 'close', { - user: self.username, + this.emit('file:retr', 'close', { + user: this.username, file: filename, filesize: 0, sTime: startTime, @@ -957,8 +927,8 @@ class FtpConnection extends EventEmitter { rs.on('end', () => { var now = new Date(); - self.emit('file:retr', 'close', { - user: self.username, + this.emit('file:retr', 'close', { + user: this.username, file: filename, filesize: 0, sTime: startTime, @@ -966,7 +936,7 @@ class FtpConnection extends EventEmitter { duration: now - startTime, errorState: false, }); - self.respond('226 Closing data connection, sent ' + readLength + ' bytes'); + this.respond('226 Closing data connection, sent ' + readLength + ' bytes'); }); rs.pipe(pasvconn); @@ -978,23 +948,22 @@ class FtpConnection extends EventEmitter { } _RETR_usingReadFile(commandArg, filename) { - var self = this; var startTime = new Date(); - self.emit('file:retr', 'open', { - user: self.username, + this.emit('file:retr', 'open', { + user: this.username, file: filename, sTime: startTime, }); function afterOk(callback) { - self.respond('150 Opening ' + self.mode.toUpperCase() + ' mode data connection', callback); + this.respond('150 Opening ' + this.mode.toUpperCase() + ' mode data connection', callback); } - self.fs.readFile(filename, (err, contents) => { + this.fs.readFile(filename, (err, contents) => { if (err) { - self.emit('file:retr', 'error', { - user: self.username, + this.emit('file:retr', 'error', { + user: this.username, file: filename, filesize: 0, sTime: startTime, @@ -1004,22 +973,22 @@ class FtpConnection extends EventEmitter { error: err, }); if (err.code === 'ENOENT') { - self.respond('550 Not Found'); + this.respond('550 Not Found'); } else { // Who knows what's going on here... - self.respond('550 Not Accessible'); - self._logIf(LOG.ERROR, "Error at read of '" + filename + "' other than ENOENT " + err); + this.respond('550 Not Accessible'); + this._logIf(LOG.ERROR, "Error at read of '" + filename + "' other than ENOENT " + err); } } else { afterOk(() => { - self._whenDataReady((pasvconn) => { + this._whenDataReady((pasvconn) => { contents = {filename: filename, data: contents}; - self.emit('file:retr:contents', contents); + this.emit('file:retr:contents', contents); contents = contents.data; pasvconn.write(contents); var contentLength = contents.length; - self.respond('226 Closing data connection, sent ' + contentLength + ' bytes'); - self.emit('file:retr', 'close', { - user: self.username, + this.respond('226 Closing data connection, sent ' + contentLength + ' bytes'); + this.emit('file:retr', 'close', { + user: this.username, file: filename, filesize: contentLength, sTime: startTime, @@ -1027,7 +996,7 @@ class FtpConnection extends EventEmitter { duration: new Date() - startTime, errorState: false, }); - self._closeSocket(pasvconn); + this._closeSocket(pasvconn); }); }); } @@ -1050,36 +1019,32 @@ class FtpConnection extends EventEmitter { } _command_RNFR(commandArg) { - var self = this; - self.filefrom = withCwd(self.cwd, commandArg); - self._logIf(LOG.DEBUG, 'Rename from ' + self.filefrom); - self.respond('350 Ready for destination name'); + this.filefrom = withCwd(this.cwd, commandArg); + this._logIf(LOG.DEBUG, 'Rename from ' + this.filefrom); + this.respond('350 Ready for destination name'); } _command_RNTO(commandArg) { - var self = this; - var fileto = withCwd(self.cwd, commandArg); - self.fs.rename(pathModule.join(self.root, self.filefrom), pathModule.join(self.root, fileto), (err) => { + var fileto = withCwd(this.cwd, commandArg); + this.fs.rename(pathModule.join(this.root, this.filefrom), pathModule.join(this.root, fileto), (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' : '')); + this._logIf(LOG.ERROR, 'Error renaming file from ' + this.filefrom + ' to ' + fileto); + this.respond('550 Rename failed' + (err.code === 'ENOENT' ? '; file does not exist' : '')); } else { - self.respond('250 File renamed successfully'); + this.respond('250 File renamed successfully'); } }); } _command_SIZE(commandArg) { - var self = this; - - var filename = withCwd(self.cwd, commandArg); - self.fs.stat(pathModule.join(self.root, filename), (err, s) => { + var filename = withCwd(this.cwd, commandArg); + this.fs.stat(pathModule.join(this.root, filename), (err, s) => { if (err) { - self._logIf(LOG.ERROR, "Error getting size of file '" + filename + "' "); - self.respond('450 Failed to get size of file'); + this._logIf(LOG.ERROR, "Error getting size of file '" + filename + "' "); + this.respond('450 Failed to get size of file'); return; } - self.respond('213 ' + s.size + ''); + this.respond('213 ' + s.size + ''); }); } @@ -1107,10 +1072,8 @@ class FtpConnection extends EventEmitter { // 'initialBuffers' argument is set when this is called from _STOR_usingWriteFile. _STOR_usingCreateWriteStream(filename, initialBuffers, flag) { - var self = this; - var wStreamFlags = {flags: flag || 'w', mode: 0o644}; - var storeStream = self.fs.createWriteStream(pathModule.join(self.root, filename), wStreamFlags); + var storeStream = this.fs.createWriteStream(pathModule.join(this.root, filename), wStreamFlags); var notErr = true; // Adding for event metadata for file upload (STOR) var startTime = new Date(); @@ -1123,24 +1086,24 @@ class FtpConnection extends EventEmitter { }); } - self._whenDataReady(handleUpload); + this._whenDataReady(handleUpload); storeStream.on('open', () => { - self._logIf(LOG.DEBUG, 'File opened/created: ' + filename); - self._logIf(LOG.DEBUG, 'Told client ok to send file data'); + this._logIf(LOG.DEBUG, 'File opened/created: ' + filename); + this._logIf(LOG.DEBUG, 'Told client ok to send file data'); // Adding event emitter for upload start time - self.emit('file:stor', 'open', { - user: self.username, + this.emit('file:stor', 'open', { + user: this.username, file: filename, time: startTime, }); - self.respond('150 Ok to send data'); + this.respond('150 Ok to send data'); }); storeStream.on('error', () => { - self.emit('file:stor', 'error', { - user: self.username, + this.emit('file:stor', 'error', { + user: this.username, file: filename, filesize: uploadSize, sTime: startTime, @@ -1150,16 +1113,16 @@ class FtpConnection extends EventEmitter { }); storeStream.end(); notErr = false; - if (self.dataSocket) { - self._closeSocket(self.dataSocket, true); + if (this.dataSocket) { + this._closeSocket(this.dataSocket, true); } - self.respond('426 Connection closed; transfer aborted'); + this.respond('426 Connection closed; transfer aborted'); }); storeStream.on('finish', () => { // Adding event emitter for completed upload. - self.emit('file:stor', 'close', { - user: self.username, + this.emit('file:stor', 'close', { + user: this.username, file: filename, filesize: uploadSize, sTime: startTime, @@ -1167,9 +1130,9 @@ class FtpConnection extends EventEmitter { duration: new Date() - startTime, errorState: !notErr, }); - notErr ? self.respond('226 Closing data connection') : true; - if (self.dataSocket) { - self._closeSocket(self.dataSocket); + notErr ? this.respond('226 Closing data connection') : true; + if (this.dataSocket) { + this._closeSocket(this.dataSocket); } }); @@ -1204,51 +1167,49 @@ class FtpConnection extends EventEmitter { } _STOR_usingWriteFile(filename, flag) { - var self = this; - var erroredOut = false; var slurpBuf = new Buffer(1024); var totalBytes = 0; var startTime = new Date(); - self.emit('file:stor', 'open', { - user: self.username, + this.emit('file:stor', 'open', { + user: this.username, file: filename, time: startTime, }); - self.respond('150 Ok to send data', () => { - self._whenDataReady(handleUpload); + this.respond('150 Ok to send data', () => { + this._whenDataReady(handleUpload); }); function handleUpload() { - self.dataSocket.on('data', dataHandler); - self.dataSocket.once('close', closeHandler); - self.dataSocket.once('error', errorHandler); + this.dataSocket.on('data', dataHandler); + this.dataSocket.once('close', closeHandler); + this.dataSocket.once('error', errorHandler); } function dataHandler(buf) { - if (self.server.options.uploadMaxSlurpSize != null && - totalBytes + buf.length > self.server.options.uploadMaxSlurpSize) { + if (this.server.options.uploadMaxSlurpSize != null && + totalBytes + buf.length > this.server.options.uploadMaxSlurpSize) { // Give up trying to slurp it -- it's too big. // If the 'fs' module we've been given doesn't implement 'createWriteStream', then // we give up and send the client an error. - if (!self.fs.createWriteStream) { - if (self.dataSocket) { - self._closeSocket(self.dataSocket, true); + if (!this.fs.createWriteStream) { + if (this.dataSocket) { + this._closeSocket(this.dataSocket, true); } - self.respond('552 Requested file action aborted; file too big'); + this.respond('552 Requested file action aborted; file too big'); return; } // Otherwise, we call _STOR_usingWriteStream, and tell it to prepend the stuff // that we've buffered so far to the file. - self._logIf(LOG.WARN, 'uploadMaxSlurpSize exceeded; falling back to createWriteStream'); - self._STOR_usingCreateWriteStream(filename, [slurpBuf.slice(0, totalBytes), buf]); - self.dataSocket.removeListener('data', dataHandler); - self.dataSocket.removeListener('error', errorHandler); - self.dataSocket.removeListener('close', closeHandler); + this._logIf(LOG.WARN, 'uploadMaxSlurpSize exceeded; falling back to createWriteStream'); + this._STOR_usingCreateWriteStream(filename, [slurpBuf.slice(0, totalBytes), buf]); + this.dataSocket.removeListener('data', dataHandler); + this.dataSocket.removeListener('error', errorHandler); + this.dataSocket.removeListener('close', closeHandler); } else { if (totalBytes + buf.length > slurpBuf.length) { var newLength = slurpBuf.length * 2; @@ -1272,10 +1233,10 @@ class FtpConnection extends EventEmitter { var wOptions = {flag: flag || 'w', mode: 0o644}; 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, (err) => { - self.emit('file:stor', 'close', { - user: self.username, + this.emit('file:stor:contents', contents); + this.fs.writeFile(pathModule.join(this.root, filename), contents.data, wOptions, (err) => { + this.emit('file:stor', 'close', { + user: this.username, file: filename, filesize: totalBytes, sTime: startTime, @@ -1285,17 +1246,17 @@ class FtpConnection extends EventEmitter { }); if (err) { erroredOut = true; - self._logIf(LOG.ERROR, 'Error writing file. ' + err); - if (self.dataSocket) { - self._closeSocket(self.dataSocket, true); + this._logIf(LOG.ERROR, 'Error writing file. ' + err); + if (this.dataSocket) { + this._closeSocket(this.dataSocket, true); } - self.respond('426 Connection closed; transfer aborted'); + this.respond('426 Connection closed; transfer aborted'); return; } - self.respond('226 Closing data connection'); - if (self.dataSocket) { - self._closeSocket(self.dataSocket); + this.respond('226 Closing data connection'); + if (this.dataSocket) { + this._closeSocket(this.dataSocket); } }); } @@ -1316,24 +1277,22 @@ class FtpConnection extends EventEmitter { // Specify a username for login _command_USER(username) { - var self = this; - - if (self.server.options.tlsOnly && !self.secure) { - self.respond( + if (this.server.options.tlsOnly && !this.secure) { + this.respond( '530 This server does not permit login over ' + 'a non-secure connection; ' + 'connect using FTP-SSL with explicit AUTH TLS'); } else { - self.emit( + this.emit( 'command:user', username, // success callback () => { - self.respond('331 User name okay, need password.'); + this.respond('331 User name okay, need password.'); }, // failure callback () => { - self.respond('530 Not logged in.'); + this.respond('530 Not logged in.'); } ); } @@ -1342,33 +1301,31 @@ class FtpConnection extends EventEmitter { // Specify a password for login _command_PASS(password) { - var self = this; - - if (self.previousCommand !== 'USER') { - self.respond('503 Bad sequence of commands.'); + if (this.previousCommand !== 'USER') { + this.respond('503 Bad sequence of commands.'); } else { - self.emit( + this.emit( 'command:pass', password, // success callback (username, userFsModule) => { function panic(error, method) { - self._logIf(LOG.ERROR, method + ' signaled error ' + util.inspect(error)); - self.respond('421 Service not available, closing control connection.', () => { - self._closeSocket(self.socket, true); + this._logIf(LOG.ERROR, method + ' signaled error ' + util.inspect(error)); + this.respond('421 Service not available, closing control connection.', () => { + this._closeSocket(this.socket, true); }); } function setCwd(cwd) { function setRoot(root) { - self.root = root; - self.respond('230 User logged in, proceed.'); + this.root = root; + this.respond('230 User logged in, proceed.'); } - self.cwd = cwd; - if (self.server.getRoot.length <= 1) { - setRoot(self.server.getRoot(self)); + this.cwd = cwd; + if (this.server.getRoot.length <= 1) { + setRoot(this.server.getRoot(this)); } else { - self.server.getRoot(self, (err, root) => { + this.server.getRoot(this, (err, root) => { if (err) { panic(err, 'getRoot'); } else { @@ -1377,12 +1334,12 @@ class FtpConnection extends EventEmitter { }); } } - self.username = username; - self.fs = userFsModule || fsModule; - if (self.server.getInitialCwd.length <= 1) { - setCwd(withCwd(self.server.getInitialCwd(self))); + this.username = username; + this.fs = userFsModule || fsModule; + if (this.server.getInitialCwd.length <= 1) { + setCwd(withCwd(this.server.getInitialCwd(this))); } else { - self.server.getInitialCwd(self, (err, cwd) => { + this.server.getInitialCwd(this, (err, cwd) => { if (err) { panic(err, 'getInitialCwd'); } else { @@ -1393,8 +1350,8 @@ class FtpConnection extends EventEmitter { }, // failure callback () => { - self.respond('530 Not logged in.'); - self.username = null; + this.respond('530 Not logged in.'); + this.username = null; } ); } From 49e5b98ac28fab5d1d28e0ae59e2761075932d48 Mon Sep 17 00:00:00 2001 From: Simon Sturmer Date: Sun, 17 Jan 2016 16:29:24 +0700 Subject: [PATCH 12/55] ES6: refactor FtpConnection (require -> import) --- src/FtpConnection.js | 444 +++++++++++++++++++++---------------------- 1 file changed, 220 insertions(+), 224 deletions(-) diff --git a/src/FtpConnection.js b/src/FtpConnection.js index 14d79d6..5b1753a 100644 --- a/src/FtpConnection.js +++ b/src/FtpConnection.js @@ -1,27 +1,25 @@ /* eslint-disable camelcase */ -var net = require('net'); -var util = require('util'); -var events = require('events'); -var pathModule = require('path'); -var fsModule = require('fs'); -var StatMode = require('stat-mode'); -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'); -var stripOptions = require('./helpers/stripOptions'); -var leftPad = require('./helpers/leftPad'); - -var EventEmitter = events.EventEmitter; - -// Use LOG for brevity. +import net from 'net'; +import util from 'util'; +import {EventEmitter} from 'events'; +import pathModule from 'path'; +import fsModule from 'fs'; +import StatMode from 'stat-mode'; +import dateformat from 'dateformat'; + +import * as glob from './glob'; +import starttls from './starttls'; +import Constants from './Constants'; +import PassiveListener from './PassiveListener'; + +import pathEscape from './helpers/pathEscape'; +import withCwd from './helpers/withCwd'; +import stripOptions from './helpers/stripOptions'; +import leftPad from './helpers/leftPad'; + var { + // Use LOG for brevity. LOG_LEVELS: LOG, DOES_NOT_REQUIRE_AUTH, REQUIRES_CONFIGURED_DATA, @@ -78,9 +76,46 @@ class FtpConnection extends EventEmitter { // TODO: Should we keep track of *all* connections, or enforce just one? this._logIf(LOG.INFO, 'Passive data event: connect'); + const setupPassiveListener = () => { + if (this.dataListener) { + this.dataListener.emit('ready'); + } else { + this._logIf(LOG.WARN, 'Passive connection initiated, but no data listener'); + } + + const allOver = (ename) => { + return (err) => { + this._logIf( + (err ? LOG.ERROR : LOG.DEBUG), + 'Passive data event: ' + ename + (err ? ' due to error' : '') + ); + this.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. + this.dataSocket.on('close', allOver('close')); + this.dataSocket.on('end', allOver('end')); + + this.dataSocket.on('error', (err) => { + this._logIf(LOG.ERROR, 'Passive data event: error: ' + err); + // TODO: Can we can rely on self.dataSocket having been closed? + this.dataSocket = null; + this.dataConfigured = false; + }); + }; + if (this.secure) { this._logIf(LOG.INFO, 'Upgrading passive connection to TLS'); starttls.starttlsServer(psocket, this.server.options.tlsOptions, (err, cleartext) => { + const switchToSecure = () => { + this._logIf(LOG.INFO, 'Secure passive connection started'); + // TODO: Check for existing dataSocket. + this.dataSocket = cleartext; + setupPassiveListener(); + }; if (err) { this._logIf(LOG.ERROR, 'Error upgrading passive connection to TLS:' + util.inspect(err)); this._closeSocket(psocket, true); @@ -97,49 +132,12 @@ class FtpConnection extends EventEmitter { } else { switchToSecure(); } - - function switchToSecure() { - this._logIf(LOG.INFO, 'Secure passive connection started'); - // TODO: Check for existing dataSocket. - this.dataSocket = cleartext; - setupPassiveListener(); - } }); } else { // TODO: Check for existing dataSocket. this.dataSocket = psocket; setupPassiveListener(); } - - function setupPassiveListener() { - if (this.dataListener) { - this.dataListener.emit('ready'); - } else { - this._logIf(LOG.WARN, 'Passive connection initiated, but no data listener'); - } - - // 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. - this.dataSocket.on('close', allOver('close')); - this.dataSocket.on('end', allOver('end')); - function allOver(ename) { - return (err) => { - this._logIf( - (err ? LOG.ERROR : LOG.DEBUG), - 'Passive data event: ' + ename + (err ? ' due to error' : '') - ); - this.dataSocket = null; - }; - } - - this.dataSocket.on('error', (err) => { - this._logIf(LOG.ERROR, 'Passive data event: error: ' + err); - // TODO: Can we can rely on self.dataSocket having been closed? - this.dataSocket = null; - this.dataConfigured = false; - }); - } }); } @@ -180,15 +178,15 @@ class FtpConnection extends EventEmitter { this.dataSocket = sock; callback(sock); }); - sock.on('end', allOver); - sock.on('close', allOver); - function allOver(err) { + const allOver = (err) => { this.dataSocket = null; this._logIf( err ? LOG.ERROR : LOG.DEBUG, 'Non-passive data connection ended' + (err ? 'due to error: ' + util.inspect(err) : '') ); - } + }; + sock.on('end', allOver); + sock.on('close', allOver); sock.on('error', (err) => { this._closeSocket(sock, true); @@ -251,6 +249,14 @@ class FtpConnection extends EventEmitter { commandArg = ''; } + const checkData = () => { + if (REQUIRES_CONFIGURED_DATA[command] && !this.dataConfigured) { + this.respond('425 Data connection not configured; send PASV or PORT'); + return; + } + this[m](commandArg, command); + }; + var m = '_command_' + command; if (this[m]) { if (this.allowedCommands != null && this.allowedCommands[command] !== true) { @@ -269,14 +275,6 @@ class FtpConnection extends EventEmitter { } } - function checkData() { - if (REQUIRES_CONFIGURED_DATA[command] && !this.dataConfigured) { - this.respond('425 Data connection not configured; send PASV or PORT'); - return; - } - - this[m](commandArg, command); - } } else { this.respond('502 Command not implemented.'); } @@ -303,6 +301,14 @@ class FtpConnection extends EventEmitter { this.respond('234 Honored', () => { this._logIf(LOG.INFO, 'Establishing secure connection...'); starttls.starttlsServer(this.socket, this.server.options.tlsOptions, (err, cleartext) => { + const switchToSecure = () => { + this._logIf(LOG.INFO, 'Secure connection started'); + this.socket = cleartext; + this.socket.on('data', (data) => { + this._onData(data); + }); + this.secure = true; + }; if (err) { this._logIf(LOG.ERROR, 'Error upgrading connection to TLS: ' + util.inspect(err)); this._closeSocket(this.socket, true); @@ -318,15 +324,6 @@ class FtpConnection extends EventEmitter { } else { switchToSecure(); } - - function switchToSecure() { - this._logIf(LOG.INFO, 'Secure connection started'); - this.socket = cleartext; - this.socket.on('data', (data) => { - this._onData(data); - }); - this.secure = true; - } }); }); } @@ -446,6 +443,7 @@ class FtpConnection extends EventEmitter { var dirname = stripOptions(commandArg); var dir = withCwd(this.cwd, dirname); + // TODO: this is bad practice, use a class if options are required: new Glob({maxConcurrency: 5}).glob() glob.setMaxStatsAtOnce(this.server.options.maxStatsAtOnce); glob.glob(pathModule.join(this.root, dir), this.fs, (err, files) => { if (err) { @@ -454,43 +452,10 @@ class FtpConnection extends EventEmitter { return; } - if (this.server.options.hideDotFiles) { - files = files.filter((file) => { - if (file.name && file.name[0] !== '.') { - return true; - } - }); - } - - this._logIf(LOG.INFO, 'Directory has ' + files.length + ' files'); - if (files.length === 0) { - return this._listFiles([], detailed, cmd); - } - - var fileInfos; // To contain list of files with info for each. - - if (!detailed) { - // We're not doing a detailed listing, so we don't need to get username - // and group name. - fileInfos = files; - return finished(); - } - - // Now we need to get username and group name for each file from user/group ids. - fileInfos = []; - - var CONC = this.server.options.maxStatsAtOnce; - var j = 0; - for (var i = 0; i < files.length && i < CONC; ++i) { - handleFile(i); - } - j = --i; - - function handleFile(ii) { + const handleFile = (ii) => { if (i >= files.length) { return i === files.length + j ? finished() : null; } - this.server.getUsernameFromUid(files[ii].stats.uid, (e1, uname) => { this.server.getGroupFromGid(files[ii].stats.gid, (e2, gname) => { if (e1 || e2) { @@ -510,9 +475,9 @@ class FtpConnection extends EventEmitter { handleFile(++i); }); }); - } + }; - function finished() { + const finished = () => { // Sort file names. if (!this.server.options.dontSortFilenames) { if (this.server.options.filenameSortMap !== false) { @@ -540,11 +505,93 @@ class FtpConnection extends EventEmitter { } this._listFiles(fileInfos, detailed, cmd); + }; + + if (this.server.options.hideDotFiles) { + files = files.filter((file) => ( + (file.name && file.name[0] !== '.') ? true : false + )); } + + this._logIf(LOG.INFO, 'Directory has ' + files.length + ' files'); + if (files.length === 0) { + return this._listFiles([], detailed, cmd); + } + + var fileInfos; // To contain list of files with info for each. + + if (!detailed) { + // We're not doing a detailed listing, so we don't need to get username + // and group name. + fileInfos = files; + return finished(); + } + + // Now we need to get username and group name for each file from user/group ids. + fileInfos = []; + + var CONC = this.server.options.maxStatsAtOnce; + var j = 0; + for (var i = 0; i < files.length && i < CONC; ++i) { + handleFile(i); + } + j = --i; + }, this.server.options.noWildcards); } _listFiles(fileInfos, detailed, cmd) { + const whenReady = (listconn) => { + const success = (err) => { + if (err) { + this.respond('550 Error listing files'); + } else { + this.respond(END_MSGS[cmd]); + } + if (cmd !== 'STAT') { + this._closeSocket(listconn); + } + }; + + if (fileInfos.length === 0) { + return success(); + } + + this._logIf(LOG.DEBUG, 'Sending file list'); + + for (var i = 0; i < fileInfos.length; ++i) { + var fileInfo = fileInfos[i]; + + var line = ''; + var file; + + if (!detailed) { + file = fileInfo; + line += file.name + '\r\n'; + } else { + file = fileInfo.file; + var s = file.stats; + var allModes = (new StatMode({mode: s.mode})).toString(); + var rwxModes = allModes.substr(1, 9); + line += (s.isDirectory() ? 'd' : '-') + rwxModes; + // ^-- Clients don't need to know about special files and pipes + line += ' 1 ' + + (fileInfo.uname || 'ftp') + ' ' + + (fileInfo.gname === null ? 'ftp' : fileInfo.gname) + ' '; + 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'; + } + this._writeText( + listconn, + line, + (i === fileInfos.length - 1 ? success : undefined) + ); + } + }; + var m = '150 Here comes the directory listing'; var BEGIN_MSGS = { LIST: m, NLST: m, STAT: '213-Status follows', @@ -561,56 +608,6 @@ class FtpConnection extends EventEmitter { this._whenDataReady(whenReady); } - function whenReady(listconn) { - if (fileInfos.length === 0) { - return success(); - } - - function success(err) { - if (err) { - this.respond('550 Error listing files'); - } else { - this.respond(END_MSGS[cmd]); - } - if (cmd !== 'STAT') { - this._closeSocket(listconn); - } - } - - this._logIf(LOG.DEBUG, 'Sending file list'); - - for (var i = 0; i < fileInfos.length; ++i) { - var fileInfo = fileInfos[i]; - - var line = ''; - var file; - - if (!detailed) { - file = fileInfo; - line += file.name + '\r\n'; - } else { - file = fileInfo.file; - var s = file.stats; - var allModes = (new StatMode({mode: s.mode})).toString(); - var rwxModes = allModes.substr(1, 9); - line += (s.isDirectory() ? 'd' : '-') + rwxModes; - // ^-- Clients don't need to know about special files and pipes - line += ' 1 ' + - (fileInfo.uname || 'ftp') + ' ' + - (fileInfo.gname === null ? 'ftp' : fileInfo.gname) + ' '; - 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'; - } - this._writeText( - listconn, - line, - (i === fileInfos.length - 1 ? success : undefined) - ); - } - } }); } @@ -746,14 +743,14 @@ class FtpConnection extends EventEmitter { var pasv = this._createPassiveServer(); var portRangeErrorHandler; - function normalErrorHandler(e) { + const normalErrorHandler = (e) => { this._logIf(LOG.WARN, 'Error with passive data listener: ' + util.inspect(e)); this.respond('421 Server was unable to open passive connection listener'); this.dataConfigured = false; this.dataListener = null; this.dataSocket = null; this.pasv = null; - } + }; if (this.server.options.pasvPortRangeStart != null && this.server.options.pasvPortRangeEnd != null) { // Keep trying ports in the range supplied until either: @@ -878,10 +875,9 @@ class FtpConnection extends EventEmitter { sTime: startTime, }); - function afterOk(callback) { + const afterOk = (callback) => { this.respond('150 Opening ' + this.mode.toUpperCase() + ' mode data connection', callback); - } - + }; this.fs.open(filename, 'r', (err, fd) => { if (err) { @@ -956,9 +952,9 @@ class FtpConnection extends EventEmitter { sTime: startTime, }); - function afterOk(callback) { + const afterOk = (callback) => { this.respond('150 Opening ' + this.mode.toUpperCase() + ' mode data connection', callback); - } + }; this.fs.readFile(filename, (err, contents) => { if (err) { @@ -1086,6 +1082,35 @@ class FtpConnection extends EventEmitter { }); } + const handleUpload = (dataSocket) => { + var isPaused = false; + dataSocket.on('data', (buff) => { + var result = storeStream.write(buff); + // Handle back-pressure + if (result === false) { + dataSocket.pause(); + isPaused = true; + storeStream.once('drain', () => { + dataSocket.resume(); + isPaused = false; + }); + } + }); + dataSocket.once('error', () => { + notErr = false; + storeStream.end(); + }); + dataSocket.once('finish', () => { + if (isPaused) { + storeStream.once('drain', () => { + storeStream.end(); + }); + } else { + storeStream.end(); + } + }); + }; + this._whenDataReady(handleUpload); storeStream.on('open', () => { @@ -1135,35 +1160,6 @@ class FtpConnection extends EventEmitter { this._closeSocket(this.dataSocket); } }); - - function handleUpload(dataSocket) { - var isPaused = false; - dataSocket.on('data', (buff) => { - var result = storeStream.write(buff); - // Handle back-pressure - if (result === false) { - dataSocket.pause(); - isPaused = true; - storeStream.once('drain', () => { - dataSocket.resume(); - isPaused = false; - }); - } - }); - dataSocket.once('error', () => { - notErr = false; - storeStream.end(); - }); - dataSocket.once('finish', () => { - if (isPaused) { - storeStream.once('drain', () => { - storeStream.end(); - }); - } else { - storeStream.end(); - } - }); - } } _STOR_usingWriteFile(filename, flag) { @@ -1178,17 +1174,7 @@ class FtpConnection extends EventEmitter { time: startTime, }); - this.respond('150 Ok to send data', () => { - this._whenDataReady(handleUpload); - }); - - function handleUpload() { - this.dataSocket.on('data', dataHandler); - this.dataSocket.once('close', closeHandler); - this.dataSocket.once('error', errorHandler); - } - - function dataHandler(buf) { + const dataHandler = (buf) => { if (this.server.options.uploadMaxSlurpSize != null && totalBytes + buf.length > this.server.options.uploadMaxSlurpSize) { // Give up trying to slurp it -- it's too big. @@ -1224,9 +1210,9 @@ class FtpConnection extends EventEmitter { buf.copy(slurpBuf, totalBytes, 0, buf.length); totalBytes += buf.length; } - } + }; - function closeHandler() { + const closeHandler = () => { if (erroredOut) { return; } @@ -1259,11 +1245,21 @@ class FtpConnection extends EventEmitter { this._closeSocket(this.dataSocket); } }); - } + }; - function errorHandler() { + const errorHandler = () => { erroredOut = true; - } + }; + + const handleUpload = () => { + this.dataSocket.on('data', dataHandler); + this.dataSocket.once('close', closeHandler); + this.dataSocket.once('error', errorHandler); + }; + + this.respond('150 Ok to send data', () => { + this._whenDataReady(handleUpload); + }); } _command_APPE(commandArg) { @@ -1309,17 +1305,17 @@ class FtpConnection extends EventEmitter { password, // success callback (username, userFsModule) => { - function panic(error, method) { + const panic = (error, method) => { this._logIf(LOG.ERROR, method + ' signaled error ' + util.inspect(error)); this.respond('421 Service not available, closing control connection.', () => { this._closeSocket(this.socket, true); }); - } - function setCwd(cwd) { - function setRoot(root) { + }; + const setCwd = (cwd) => { + const setRoot = (root) => { this.root = root; this.respond('230 User logged in, proceed.'); - } + }; this.cwd = cwd; if (this.server.getRoot.length <= 1) { @@ -1333,7 +1329,7 @@ class FtpConnection extends EventEmitter { } }); } - } + }; this.username = username; this.fs = userFsModule || fsModule; if (this.server.getInitialCwd.length <= 1) { @@ -1375,4 +1371,4 @@ class FtpConnection extends EventEmitter { } } -module.exports = FtpConnection; +export default FtpConnection; From 975c98f33b67f857181b16c7b04d6750952ef6a5 Mon Sep 17 00:00:00 2001 From: Simon Sturmer Date: Sun, 17 Jan 2016 16:29:48 +0700 Subject: [PATCH 13/55] ES6: refactor FtpServer --- src/FtpServer.js | 258 ++++++++++++++++++++++++----------------------- 1 file changed, 130 insertions(+), 128 deletions(-) diff --git a/src/FtpServer.js b/src/FtpServer.js index 2ff206e..5b78ee0 100644 --- a/src/FtpServer.js +++ b/src/FtpServer.js @@ -1,145 +1,147 @@ -var net = require('net'); -var util = require('util'); -var events = require('events'); -var FtpConnection = require('./FtpConnection'); -var Constants = require('./Constants'); +import net from 'net'; +import events from 'events'; +import FtpConnection from './FtpConnection'; +import Constants from './Constants'; -var EventEmitter = events.EventEmitter; +var {EventEmitter} = events; // Use LOG for brevity. var LOG = Constants.LOG_LEVELS; -function FtpServer(host, options) { - var self = this; - EventEmitter.call(self); +class FtpServer extends EventEmitter { + constructor(host, options) { + super(); + var self = this; - self.host = host; + self.host = host; - self.options = options; + self.options = options; - if (!self.options.maxStatsAtOnce) { - self.options.maxStatsAtOnce = 5; - } + if (!self.options.maxStatsAtOnce) { + self.options.maxStatsAtOnce = 5; + } - if (!options.getInitialCwd) { - throw new Error("'getInitialCwd' option of FtpServer must be set"); - } - if (!options.getRoot) { - throw new Error("'getRoot' option of FtpServer must be set"); - } - self.getInitialCwd = options.getInitialCwd; - self.getRoot = options.getRoot; - - self.getUsernameFromUid = options.getUsernameFromUid || function(uid, c) { - c(null, 'ftp'); - }; - self.getGroupFromGid = options.getGroupFromGid || function(gid, c) { - c(null, 'ftp'); - }; - self.debugging = options.logLevel || 0; - self.useWriteFile = options.useWriteFile; - self.useReadFile = options.useReadFile; - self.uploadMaxSlurpSize = options.uploadMaxSlurpSize || 0; - - self.server = net.createServer(); - self.server.on('connection', function(socket) { - self._onConnection(socket); - }); - self.server.on('error', function(err) { - self.emit('error', err); - }); - self.server.on('close', function() { - self.emit('close'); - }); -} -util.inherits(FtpServer, EventEmitter); - -FtpServer.prototype._onConnection = function(socket) { - // build an index for the allowable commands for this server - var allowedCommands = null; - if (this.options.allowedCommands) { - allowedCommands = {}; - this.options.allowedCommands.forEach(function(c) { - allowedCommands[c.trim().toUpperCase()] = true; + if (!options.getInitialCwd) { + throw new Error("'getInitialCwd' option of FtpServer must be set"); + } + if (!options.getRoot) { + throw new Error("'getRoot' option of FtpServer must be set"); + } + self.getInitialCwd = options.getInitialCwd; + self.getRoot = options.getRoot; + + self.getUsernameFromUid = options.getUsernameFromUid || ((uid, c) => { + c(null, 'ftp'); + }); + self.getGroupFromGid = options.getGroupFromGid || ((gid, c) => { + c(null, 'ftp'); + }); + self.debugging = options.logLevel || 0; + self.useWriteFile = options.useWriteFile; + self.useReadFile = options.useReadFile; + self.uploadMaxSlurpSize = options.uploadMaxSlurpSize || 0; + + self.server = net.createServer(); + self.server.on('connection', (socket) => { + self._onConnection(socket); + }); + self.server.on('error', (err) => { + self.emit('error', err); + }); + self.server.on('close', () => { + self.emit('close'); }); } - var conn = new FtpConnection({ - server: this, - socket: socket, - pasv: null, // passive listener server - 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 - // it specified. (This can therefore be true even if there - // is not currently an open data connection.) - dataConfigured: false, - mode: 'ascii', - filefrom: '', - username: null, - filename: '', - fs: null, - cwd: null, - root: null, - hasQuit: false, - - // State for handling TLS upgrades. - secure: false, - pbszReceived: false, - }); - - 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) { - FtpServer.prototype[fname] = function() { - return this.server[fname].apply(this.server, arguments); - }; -}); - -FtpServer.prototype._logIf = function(verbosity, message, conn) { - if (verbosity > this.debugging) { - return; + _onConnection(socket) { + // build an index for the allowable commands for this server + var allowedCommands = null; + if (this.options.allowedCommands) { + allowedCommands = {}; + this.options.allowedCommands.forEach((c) => { + allowedCommands[c.trim().toUpperCase()] = true; + }); + } + + var conn = new FtpConnection({ + server: this, + socket: socket, + pasv: null, // passive listener server + 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 + // it specified. (This can therefore be true even if there + // is not currently an open data connection.) + dataConfigured: false, + mode: 'ascii', + filefrom: '', + username: null, + filename: '', + fs: null, + cwd: null, + root: null, + hasQuit: false, + + // State for handling TLS upgrades. + secure: false, + pbszReceived: false, + }); + + 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', (buf) => { + conn._onData(buf); + }); + socket.on('end', () => { + conn._onEnd(); + }); + socket.on('error', (err) => { + conn._onError(err); + }); + // `close` will always be called once (directly after `end` or `error`) + socket.on('close', (hadError) => { + conn._onClose(hadError); + }); } - // TODO: Move this to FtpConnection.prototype._logIf. - var peerAddr = (conn && conn.socket && conn.socket.remoteAddress); - if (peerAddr) { - message = '<' + peerAddr + '> ' + message; + + _logIf(verbosity, message, conn) { + if (verbosity > this.debugging) { + return; + } + // TODO: Move this to FtpConnection.prototype._logIf. + var peerAddr = (conn && conn.socket && conn.socket.remoteAddress); + if (peerAddr) { + message = '<' + peerAddr + '> ' + message; + } + if (verbosity === LOG.ERROR) { + message = 'ERROR: ' + message; + } else if (verbosity === LOG.WARN) { + message = 'WARNING: ' + message; + } + console.log(message); + var isError = (verbosity === LOG.ERROR); + if (isError && this.debugging === LOG.TRACE) { + console.trace('Trace follows'); + } } - if (verbosity === LOG.ERROR) { - message = 'ERROR: ' + message; - } else if (verbosity === LOG.WARN) { - message = 'WARNING: ' + message; + + listen() { + this.server.listen(...arguments); } - console.log(message); - var isError = (verbosity === LOG.ERROR); - if (isError && this.debugging === LOG.TRACE) { - console.trace('Trace follows'); + + close() { + this.server.close(...arguments); } -}; +} -module.exports = FtpServer; +export default FtpServer; From cba5208cb7f8a72754381a688278f26d57a95cd4 Mon Sep 17 00:00:00 2001 From: Simon Sturmer Date: Sun, 24 Jan 2016 08:19:35 +0700 Subject: [PATCH 14/55] ES6: refactor FtpServer: use `this` instead of `self`; create DEFAULT_OPTIONS --- src/FtpServer.js | 62 ++++++++++++++++++++++-------------------------- 1 file changed, 29 insertions(+), 33 deletions(-) diff --git a/src/FtpServer.js b/src/FtpServer.js index 5b78ee0..05a2b4e 100644 --- a/src/FtpServer.js +++ b/src/FtpServer.js @@ -7,49 +7,45 @@ var {EventEmitter} = events; // Use LOG for brevity. var LOG = Constants.LOG_LEVELS; +var DEFAULT_OPTIONS = { + logLevel: 0, + maxStatsAtOnce: 5, + uploadMaxSlurpSize: null, + getGroupFromGid: (gid, c) => { + c(null, 'ftp'); + }, + getUsernameFromUid: (uid, c) => { + c(null, 'ftp'); + }, +}; class FtpServer extends EventEmitter { constructor(host, options) { super(); - var self = this; - - self.host = host; - - self.options = options; - - if (!self.options.maxStatsAtOnce) { - self.options.maxStatsAtOnce = 5; - } - + this.host = host; + options = Object.assign({}, DEFAULT_OPTIONS, options); if (!options.getInitialCwd) { throw new Error("'getInitialCwd' option of FtpServer must be set"); } if (!options.getRoot) { throw new Error("'getRoot' option of FtpServer must be set"); } - self.getInitialCwd = options.getInitialCwd; - self.getRoot = options.getRoot; - - self.getUsernameFromUid = options.getUsernameFromUid || ((uid, c) => { - c(null, 'ftp'); - }); - self.getGroupFromGid = options.getGroupFromGid || ((gid, c) => { - c(null, 'ftp'); - }); - self.debugging = options.logLevel || 0; - self.useWriteFile = options.useWriteFile; - self.useReadFile = options.useReadFile; - self.uploadMaxSlurpSize = options.uploadMaxSlurpSize || 0; - - self.server = net.createServer(); - self.server.on('connection', (socket) => { - self._onConnection(socket); + this.options = options; + this.getInitialCwd = options.getInitialCwd; + this.getRoot = options.getRoot; + this.getUsernameFromUid = options.getUsernameFromUid; + this.getGroupFromGid = options.getGroupFromGid; + this.useWriteFile = options.useWriteFile; + this.useReadFile = options.useReadFile; + this.server = net.createServer(); + this.server.on('connection', (socket) => { + this._onConnection(socket); }); - self.server.on('error', (err) => { - self.emit('error', err); + this.server.on('error', (err) => { + this.emit('error', err); }); - self.server.on('close', () => { - self.emit('close'); + this.server.on('close', () => { + this.emit('close'); }); } @@ -115,7 +111,7 @@ class FtpServer extends EventEmitter { } _logIf(verbosity, message, conn) { - if (verbosity > this.debugging) { + if (verbosity > this.options.logLevel) { return; } // TODO: Move this to FtpConnection.prototype._logIf. @@ -130,7 +126,7 @@ class FtpServer extends EventEmitter { } console.log(message); var isError = (verbosity === LOG.ERROR); - if (isError && this.debugging === LOG.TRACE) { + if (isError && this.options.logLevel === LOG.TRACE) { console.trace('Trace follows'); } } From 1bd0017e174bf7b16d4e57a410eca41ec54e28db Mon Sep 17 00:00:00 2001 From: Simon Sturmer Date: Sun, 17 Jan 2016 16:30:18 +0700 Subject: [PATCH 15/55] ES6: refactor PassiveListener --- src/PassiveListener.js | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/src/PassiveListener.js b/src/PassiveListener.js index 943179a..0c0d17a 100644 --- a/src/PassiveListener.js +++ b/src/PassiveListener.js @@ -1,10 +1,5 @@ -var util = require('util'); -var events = require('events'); -var EventEmitter = events.EventEmitter; +import events from 'events'; -function PassiveListener() { - EventEmitter.call(this); -} -util.inherits(PassiveListener, EventEmitter); +const {EventEmitter} = events; -module.exports = PassiveListener; +export default class PassiveListener extends EventEmitter {} From 38566f4cb33a328e6845381e193f4e1267d9e44e Mon Sep 17 00:00:00 2001 From: Simon Sturmer Date: Sun, 17 Jan 2016 16:30:40 +0700 Subject: [PATCH 16/55] ES6: refactor Constants --- src/Constants.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/Constants.js b/src/Constants.js index 9d0f028..3de0ba7 100644 --- a/src/Constants.js +++ b/src/Constants.js @@ -1,4 +1,5 @@ -module.exports = { +const Constants = { + CONCURRENT_STAT_CALLS: 5, // Whitelist of commands which don't require authentication. // All other commands sent by unauthorized users will be rejected by default. DOES_NOT_REQUIRE_AUTH: { @@ -29,3 +30,5 @@ module.exports = { TRACE: 4, }, }; + +export default Constants; From e6979e3322b6b9b91fa4ee72f8236791dc82fe16 Mon Sep 17 00:00:00 2001 From: Simon Sturmer Date: Sun, 17 Jan 2016 16:30:54 +0700 Subject: [PATCH 17/55] ES6: refactor glob --- src/glob.js | 118 +++++++++++++++++++++++++--------------------------- 1 file changed, 57 insertions(+), 61 deletions(-) diff --git a/src/glob.js b/src/glob.js index e6805aa..88da6f4 100644 --- a/src/glob.js +++ b/src/glob.js @@ -1,28 +1,24 @@ -var PathModule = require('path'); +import pathModule from 'path'; +import Constants from './Constants'; -var CONC = 5; -function setMaxStatsAtOnce(n) { - CONC = n; -} +const {CONCURRENT_STAT_CALLS} = Constants; + +// TODO: this is bad practice, use a class if options are required: new Glob({maxConcurrency: 5}).glob() +let concurrentStatCalls = CONCURRENT_STAT_CALLS; +export const setMaxStatsAtOnce = (n) => { + concurrentStatCalls = n; +}; // Wildcard directory listing. There is no way that a client should // use wildcards in directory names unless they're identifying a // 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 < CONC; ++i) { - handleFile(); - } - - var erroredOut = false; +const statList = (fsm, list, callback) => { + const handleFile = () => { + const finished = () => { + callback(null, stats); + }; - function handleFile() { if (erroredOut) { return; } @@ -34,26 +30,34 @@ function statList(fsm, list, callback) { } var path = list.shift(); - fsm.stat(path, function(err, st) { + fsm.stat(path, (err, st) => { if (err) { erroredOut = true; callback(err); } else { stats.push({ - name: PathModule.basename(path), + name: pathModule.basename(path), stats: st, }); handleFile(); } }); + }; + + if (list.length === 0) { + return callback(null, []); } - function finished() { - callback(null, stats); + var stats = []; + var total = list.length; + for (var i = 0; i < concurrentStatCalls; ++i) { + handleFile(); } -} -function matchPattern(pattern, string) { + var erroredOut = false; +}; + +export const matchPattern = (pattern, string) => { var pi = 0; var si = 0; for (; si < string.length && pi < pattern.length; ++si) { @@ -81,19 +85,19 @@ function matchPattern(pattern, string) { } return (pi === pattern.length || (pi === pattern.length - 1 && pattern.charAt(pi) === '*')) && si === string.length; -} +}; -function glob(path, fsm, callback, noWildcards) { +export const glob = (path, fsm, callback, noWildcards) => { var w; for (w = 0; !noWildcards && w < path.length && path.charAt(w) !== '*' && path.charAt(w) !== '?'; ++w) { } if (w === path.length) { // There are no wildcards. - fsm.readdir(path, function(err, contents) { + fsm.readdir(path, (err, contents) => { if (err) { if (err.code === 'ENOTDIR') { - statList(fsm, [path], function(err, list) { + statList(fsm, [path], (err, list) => { if (err) { return callback(err); } @@ -110,10 +114,8 @@ function glob(path, fsm, callback, noWildcards) { } else { statList( fsm, - contents.map(function(p) { - return PathModule.join(path, p); - }), - function(err, list) { + contents.map((p) => pathModule.join(path, p)), + (err, list) => { if (err) { callback(err); } else { @@ -163,9 +165,22 @@ function glob(path, fsm, callback, noWildcards) { // 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) { + const readTheDir = (listingSingleDir) => { + fsm.readdir(base, (err, contents) => { + const doTheNormalThing = () => { + statList( + fsm, + matches.map((p) => pathModule.join(base, p)), + (err, list) => { + if (err) { + callback(err); + } else { + callback(null, list); + } + } + ); + }; + if (err) { if (err.code === 'ENOTDIR' || err.code === 'ENOENT') { callback(null, []); @@ -175,9 +190,7 @@ function glob(path, fsm, callback, noWildcards) { } else { var matches; if (!listingSingleDir) { - matches = contents.filter(function(n) { - return matchPattern(pattern, n); - }); + matches = contents.filter((n) => matchPattern(pattern, n)); } else { matches = contents; } @@ -187,8 +200,8 @@ function glob(path, fsm, callback, noWildcards) { // 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) { + var dir = pathModule.join(base, matches[0]); + fsm.stat(dir, (err, st) => { if (err) { return callback(err); } @@ -204,27 +217,10 @@ function glob(path, fsm, callback, noWildcards) { doTheNormalThing(); } - function doTheNormalThing() { - statList( - fsm, - matches.map(function(p) { - return PathModule.join(base, p); - }), - function(err, list) { - if (err) { - callback(err); - } else { - callback(null, list); - } - } - ); - } } }); - } - } -} + }; -exports.glob = glob; -exports.matchPattern = matchPattern; -exports.setMaxStatsAtOnce = setMaxStatsAtOnce; + readTheDir(false); + } +}; From 7d105916769eedbec45f2102e4aa1604e8483957 Mon Sep 17 00:00:00 2001 From: Simon Sturmer Date: Sun, 17 Jan 2016 16:31:22 +0700 Subject: [PATCH 18/55] ES6: refactor starttls --- src/starttls.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/starttls.js b/src/starttls.js index ede6b39..63c6f28 100644 --- a/src/starttls.js +++ b/src/starttls.js @@ -36,7 +36,7 @@ function starttls(socket, options, callback, isServer) { var sslcontext; var opts = {}; - Object.keys(options).forEach(function(key) { + Object.keys(options).forEach((key) => { opts[key] = options[key]; }); if (!opts.ciphers) { @@ -53,7 +53,7 @@ function starttls(socket, options, callback, isServer) { var cleartext = pipe(pair, socket); var erroredOut = false; - pair.on('secure', function() { + pair.on('secure', () => { if (erroredOut) { pair.end(); return; @@ -70,7 +70,7 @@ function starttls(socket, options, callback, isServer) { callback(null, cleartext); }); - pair.once('error', function(err) { + pair.once('error', (err) => { if (!erroredOut) { erroredOut = true; callback(err); From 9b94e5a9c703e9afc89a194e4e71c987b388c946 Mon Sep 17 00:00:00 2001 From: Simon Sturmer Date: Sun, 17 Jan 2016 16:31:43 +0700 Subject: [PATCH 19/55] ES6: refactor helpers/leftPad --- src/helpers/__tests__/leftPad-test.js | 13 +++++++++++++ src/helpers/leftPad.js | 6 +++--- 2 files changed, 16 insertions(+), 3 deletions(-) create mode 100644 src/helpers/__tests__/leftPad-test.js diff --git a/src/helpers/__tests__/leftPad-test.js b/src/helpers/__tests__/leftPad-test.js new file mode 100644 index 0000000..81dc157 --- /dev/null +++ b/src/helpers/__tests__/leftPad-test.js @@ -0,0 +1,13 @@ +/*eslint-env node, mocha */ +import leftPad from '../leftPad'; +import expect from 'expect'; + +describe('helpers/leftPad', () => { + it('should prepend space to a string', () => { + expect(leftPad('abc', 5)).toBe(' abc'); + expect(leftPad('abc', 3)).toBe('abc'); + expect(leftPad('abc', 2)).toBe('abc'); + expect(leftPad('', 3)).toBe(' '); + expect(leftPad(' a', 3)).toBe(' a'); + }); +}); diff --git a/src/helpers/leftPad.js b/src/helpers/leftPad.js index 6c6e5b9..6fe8ff1 100644 --- a/src/helpers/leftPad.js +++ b/src/helpers/leftPad.js @@ -1,10 +1,10 @@ -function leftPad(text, width) { +const leftPad = (text, width) => { var out = ''; for (var j = text.length; j < width; j++) { out += ' '; } out += text; return out; -} +}; -module.exports = leftPad; +export default leftPad; From c31b822146fd6e2beeb661d6f300f8d76e001fb6 Mon Sep 17 00:00:00 2001 From: Simon Sturmer Date: Sun, 17 Jan 2016 16:32:10 +0700 Subject: [PATCH 20/55] ES6: refactor helpers/pathEscape --- src/helpers/__tests__/pathEscape-test.js | 14 ++++++++++++++ src/helpers/pathEscape.js | 9 ++++----- 2 files changed, 18 insertions(+), 5 deletions(-) create mode 100644 src/helpers/__tests__/pathEscape-test.js diff --git a/src/helpers/__tests__/pathEscape-test.js b/src/helpers/__tests__/pathEscape-test.js new file mode 100644 index 0000000..fce4d42 --- /dev/null +++ b/src/helpers/__tests__/pathEscape-test.js @@ -0,0 +1,14 @@ +/*eslint-env node, mocha */ +import pathEscape from '../pathEscape'; +import expect from 'expect'; + +describe('helpers/pathEscape', () => { + it('should not change a string that does not need escaping', () => { + expect(pathEscape('')).toBe(''); + expect(pathEscape('abc')).toBe('abc'); + }); + it('should escape one ore more occurances', () => { + expect(pathEscape('a"b')).toBe('a""b'); + expect(pathEscape('a"b"c')).toBe('a""b""c'); + }); +}); diff --git a/src/helpers/pathEscape.js b/src/helpers/pathEscape.js index 725265e..0cbc7d7 100644 --- a/src/helpers/pathEscape.js +++ b/src/helpers/pathEscape.js @@ -1,9 +1,8 @@ -function pathEscape(text) { +const pathEscape = (text) => { // Rules for quoting: RFC 959 -> Appendix II -> Directory Commands // (http://www.w3.org/Protocols/rfc959/A2_DirectoryCommands.html) // -> Reply Codes -> search for "embedded double-quotes" - text = text.replace(/"/g, '""'); - return text; -} + return text.replace(/"/g, '""'); +}; -module.exports = pathEscape; +export default pathEscape; From 84ccdde0a6ac5647ff6448c6aa31e9020350cdf5 Mon Sep 17 00:00:00 2001 From: Simon Sturmer Date: Sun, 17 Jan 2016 16:32:35 +0700 Subject: [PATCH 21/55] ES6: refactor helpers/stripOptions --- src/helpers/__tests__/stripOptions-test.js | 15 +++++++++++++++ src/helpers/stripOptions.js | 6 +++--- 2 files changed, 18 insertions(+), 3 deletions(-) create mode 100644 src/helpers/__tests__/stripOptions-test.js diff --git a/src/helpers/__tests__/stripOptions-test.js b/src/helpers/__tests__/stripOptions-test.js new file mode 100644 index 0000000..ded3466 --- /dev/null +++ b/src/helpers/__tests__/stripOptions-test.js @@ -0,0 +1,15 @@ +/*eslint-env node, mocha */ +import stripOptions from '../stripOptions'; +import expect from 'expect'; + +describe('helpers/stripOptions', () => { + it('should not change a string unnecessarily', () => { + expect(stripOptions('')).toBe(''); + expect(stripOptions('/ab/c')).toBe('/ab/c'); + }); + it('should remove options', () => { + expect(stripOptions('-a /foo/bar')).toBe('/foo/bar'); + expect(stripOptions(' \t-a foo/bar')).toBe('foo/bar'); + expect(stripOptions('-d -ef A B C')).toBe('A B C'); + }); +}); diff --git a/src/helpers/stripOptions.js b/src/helpers/stripOptions.js index 229f809..abc685a 100644 --- a/src/helpers/stripOptions.js +++ b/src/helpers/stripOptions.js @@ -1,5 +1,5 @@ // Currently used for stripping options from beginning of argument to LIST and NLST. -function stripOptions(str) { +const stripOptions = (str) => { var IN_SPACE = 0; var IN_DASH = 1; var state = IN_SPACE; @@ -18,6 +18,6 @@ function stripOptions(str) { } } return ''; -} +}; -module.exports = stripOptions; +export default stripOptions; From 810f9bce80689bd226172f363ac67c81eae3ccb7 Mon Sep 17 00:00:00 2001 From: Simon Sturmer Date: Sun, 17 Jan 2016 16:32:56 +0700 Subject: [PATCH 22/55] ES6: refactor helpers/withCwd --- src/helpers/__tests__/withCwd-test.js | 22 ++++++++++++++++++++++ src/helpers/withCwd.js | 21 ++++++++++----------- 2 files changed, 32 insertions(+), 11 deletions(-) create mode 100644 src/helpers/__tests__/withCwd-test.js diff --git a/src/helpers/__tests__/withCwd-test.js b/src/helpers/__tests__/withCwd-test.js new file mode 100644 index 0000000..e4c0c3a --- /dev/null +++ b/src/helpers/__tests__/withCwd-test.js @@ -0,0 +1,22 @@ +/*eslint-env node, mocha */ +import withCwd from '../withCwd'; +import expect from 'expect'; + +describe('helpers/withCwd', () => { + it('should accept no arguments', () => { + expect(withCwd()).toBe('/'); + }); + it('should accept one argument', () => { + expect(withCwd('/foo')).toBe('/foo'); + }); + it('should support relative paths', () => { + expect(withCwd('/a', '../b')).toBe('/b'); + expect(withCwd('/a/b/c', './d')).toBe('/a/b/c/d'); + expect(withCwd('/a/b/c', 'd')).toBe('/a/b/c/d'); + }); + it('should support absolute paths', () => { + expect(withCwd('/a', '/b')).toBe('/b'); + expect(withCwd('/a/b', '/c')).toBe('/c'); + expect(withCwd('/a/b/c', '/d')).toBe('/d'); + }); +}); diff --git a/src/helpers/withCwd.js b/src/helpers/withCwd.js index 3eed41c..cc26e4d 100644 --- a/src/helpers/withCwd.js +++ b/src/helpers/withCwd.js @@ -1,14 +1,13 @@ -var pathModule = require('path'); +import pathModule from 'path'; -function withCwd(cwd, path) { - var firstChar = (path || '').charAt(0); - cwd = cwd || pathModule.sep; - path = path || ''; - if (firstChar === '/' || firstChar === pathModule.sep) { - cwd = pathModule.sep; +const SEP = pathModule.sep; + +const withCwd = (cwd, path) => { + let firstChar = (path || '').charAt(0); + if (firstChar === '/' || firstChar === SEP) { + cwd = SEP; } - path = pathModule.join(pathModule.sep, cwd, path); - return path; -} + return pathModule.join(SEP, cwd || SEP, path || ''); +}; -module.exports = withCwd; +export default withCwd; From 89668da4e41ee5a98a510afa581b9eb96d0705a2 Mon Sep 17 00:00:00 2001 From: Simon Sturmer Date: Sun, 17 Jan 2016 16:40:55 +0700 Subject: [PATCH 23/55] move tests into src directory so we can use ES6 --- {test => src/__tests__}/.eslintrc | 0 {test => src/__tests__}/acct.js | 0 {test => src/__tests__}/allo.js | 0 {test => src/__tests__}/appe.js | 0 {test => src/__tests__}/cwd-cdup.js | 0 {test => src/__tests__}/glob.js | 0 {test => src/__tests__}/init.js | 0 {test => src/__tests__}/lib/common.js | 0 {test => src/__tests__}/list.js | 0 {test => src/__tests__}/mdtm.js | 0 {test => src/__tests__}/mkd-rmd.js | 0 {test => src/__tests__}/noop.js | 0 {test => src/__tests__}/pass.js | 0 {test => src/__tests__}/pwd.js | 0 {test => src/__tests__}/retr.js | 0 {test => src/__tests__}/tricky-paths.js | 0 {test => src/__tests__}/unsupported.js | 0 {test => src/__tests__}/user.js | 0 {test => src/__tests__}/utf8.js | 0 {test => src/__tests__}/whitelisted.js | 0 20 files changed, 0 insertions(+), 0 deletions(-) rename {test => src/__tests__}/.eslintrc (100%) rename {test => src/__tests__}/acct.js (100%) rename {test => src/__tests__}/allo.js (100%) rename {test => src/__tests__}/appe.js (100%) rename {test => src/__tests__}/cwd-cdup.js (100%) rename {test => src/__tests__}/glob.js (100%) rename {test => src/__tests__}/init.js (100%) rename {test => src/__tests__}/lib/common.js (100%) rename {test => src/__tests__}/list.js (100%) rename {test => src/__tests__}/mdtm.js (100%) rename {test => src/__tests__}/mkd-rmd.js (100%) rename {test => src/__tests__}/noop.js (100%) rename {test => src/__tests__}/pass.js (100%) rename {test => src/__tests__}/pwd.js (100%) rename {test => src/__tests__}/retr.js (100%) rename {test => src/__tests__}/tricky-paths.js (100%) rename {test => src/__tests__}/unsupported.js (100%) rename {test => src/__tests__}/user.js (100%) rename {test => src/__tests__}/utf8.js (100%) rename {test => src/__tests__}/whitelisted.js (100%) diff --git a/test/.eslintrc b/src/__tests__/.eslintrc similarity index 100% rename from test/.eslintrc rename to src/__tests__/.eslintrc diff --git a/test/acct.js b/src/__tests__/acct.js similarity index 100% rename from test/acct.js rename to src/__tests__/acct.js diff --git a/test/allo.js b/src/__tests__/allo.js similarity index 100% rename from test/allo.js rename to src/__tests__/allo.js diff --git a/test/appe.js b/src/__tests__/appe.js similarity index 100% rename from test/appe.js rename to src/__tests__/appe.js diff --git a/test/cwd-cdup.js b/src/__tests__/cwd-cdup.js similarity index 100% rename from test/cwd-cdup.js rename to src/__tests__/cwd-cdup.js diff --git a/test/glob.js b/src/__tests__/glob.js similarity index 100% rename from test/glob.js rename to src/__tests__/glob.js diff --git a/test/init.js b/src/__tests__/init.js similarity index 100% rename from test/init.js rename to src/__tests__/init.js diff --git a/test/lib/common.js b/src/__tests__/lib/common.js similarity index 100% rename from test/lib/common.js rename to src/__tests__/lib/common.js diff --git a/test/list.js b/src/__tests__/list.js similarity index 100% rename from test/list.js rename to src/__tests__/list.js diff --git a/test/mdtm.js b/src/__tests__/mdtm.js similarity index 100% rename from test/mdtm.js rename to src/__tests__/mdtm.js diff --git a/test/mkd-rmd.js b/src/__tests__/mkd-rmd.js similarity index 100% rename from test/mkd-rmd.js rename to src/__tests__/mkd-rmd.js diff --git a/test/noop.js b/src/__tests__/noop.js similarity index 100% rename from test/noop.js rename to src/__tests__/noop.js diff --git a/test/pass.js b/src/__tests__/pass.js similarity index 100% rename from test/pass.js rename to src/__tests__/pass.js diff --git a/test/pwd.js b/src/__tests__/pwd.js similarity index 100% rename from test/pwd.js rename to src/__tests__/pwd.js diff --git a/test/retr.js b/src/__tests__/retr.js similarity index 100% rename from test/retr.js rename to src/__tests__/retr.js diff --git a/test/tricky-paths.js b/src/__tests__/tricky-paths.js similarity index 100% rename from test/tricky-paths.js rename to src/__tests__/tricky-paths.js diff --git a/test/unsupported.js b/src/__tests__/unsupported.js similarity index 100% rename from test/unsupported.js rename to src/__tests__/unsupported.js diff --git a/test/user.js b/src/__tests__/user.js similarity index 100% rename from test/user.js rename to src/__tests__/user.js diff --git a/test/utf8.js b/src/__tests__/utf8.js similarity index 100% rename from test/utf8.js rename to src/__tests__/utf8.js diff --git a/test/whitelisted.js b/src/__tests__/whitelisted.js similarity index 100% rename from test/whitelisted.js rename to src/__tests__/whitelisted.js From 09103196b3c2c96436f0b3adbeec2dcc18580382 Mon Sep 17 00:00:00 2001 From: Simon Sturmer Date: Sun, 17 Jan 2016 16:41:50 +0700 Subject: [PATCH 24/55] refactor tests for ES6 --- src/__tests__/acct.js | 10 ++-- src/__tests__/allo.js | 10 ++-- src/__tests__/appe.js | 29 +++++++----- src/__tests__/cwd-cdup.js | 42 ++++++++--------- src/__tests__/glob.js | 6 +-- src/__tests__/init.js | 38 +++++++-------- src/__tests__/lib/common.js | 71 ++++++++++++++-------------- src/__tests__/list.js | 28 +++++------ src/__tests__/mdtm.js | 14 +++--- src/__tests__/mkd-rmd.js | 26 +++++------ src/__tests__/noop.js | 10 ++-- src/__tests__/pass.js | 18 +++---- src/__tests__/pwd.js | 34 +++++++------- src/__tests__/retr.js | 24 +++++----- src/__tests__/tricky-paths.js | 88 ++++++++++++++++++----------------- src/__tests__/unsupported.js | 12 ++--- src/__tests__/user.js | 18 +++---- src/__tests__/utf8.js | 18 +++---- src/__tests__/whitelisted.js | 22 ++++----- 19 files changed, 264 insertions(+), 254 deletions(-) diff --git a/src/__tests__/acct.js b/src/__tests__/acct.js index ad261cf..be594f4 100644 --- a/src/__tests__/acct.js +++ b/src/__tests__/acct.js @@ -1,25 +1,25 @@ var common = require('./lib/common'); -describe('ACCT command', function() { +describe('ACCT command', () => { 'use strict'; var client; var server; - beforeEach(function(done) { + beforeEach((done) => { server = common.server(); client = common.client(done); }); - it('should reply 202', function(done) { - client.execute('ACCT', function(error, reply) { + it('should reply 202', (done) => { + client.execute('ACCT', (error, reply) => { common.should.not.exist(error); reply.code.should.equal(202); done(); }); }); - afterEach(function() { + afterEach(() => { server.close(); }); }); diff --git a/src/__tests__/allo.js b/src/__tests__/allo.js index 0014819..cd214c9 100644 --- a/src/__tests__/allo.js +++ b/src/__tests__/allo.js @@ -1,25 +1,25 @@ var common = require('./lib/common'); -describe('ALLO command', function() { +describe('ALLO command', () => { 'use strict'; var client; var server; - beforeEach(function(done) { + beforeEach((done) => { server = common.server(); client = common.client(done); }); - it('should reply 202', function(done) { - client.execute('ALLO', function(error, reply) { + it('should reply 202', (done) => { + client.execute('ALLO', (error, reply) => { common.should.not.exist(error); reply.code.should.equal(202); done(); }); }); - afterEach(function() { + afterEach(() => { server.close(); }); }); diff --git a/src/__tests__/appe.js b/src/__tests__/appe.js index 58d5e02..eb632ee 100644 --- a/src/__tests__/appe.js +++ b/src/__tests__/appe.js @@ -3,20 +3,20 @@ var FtpClient = require('ftp'); var path = require('path'); var fs = require('fs'); -describe('APPE command', function() { +describe('APPE command', () => { 'use strict'; var client = new FtpClient(); var server; //run tests both ways - [true, false].forEach(function(useWriteFile) { + [true, false].forEach((useWriteFile) => { - describe('with useWriteFile = ' + useWriteFile, function() { + describe('with useWriteFile = ' + useWriteFile, () => { - beforeEach(function(done) { - server = common.server({useWriteFile:useWriteFile}); - client.once('ready', function() { + beforeEach((done) => { + server = common.server({useWriteFile: useWriteFile}); + client.once('ready', () => { done(); }); client.connect({ @@ -27,21 +27,27 @@ describe('APPE command', function() { }); }); - it('should append data to existing file', function(done) { + it('should append data to existing file', (done) => { var basename = path.basename(__filename); var fileSize = fs.statSync(__filename).size; - client.put(__filename, '/uploads/' + basename, function(err) { + client.put(__filename, '/uploads/' + basename, (err) => { common.should.not.exist(err); - client.append(__filename, '/uploads/' + basename, function(err) { + client.append(__filename, '/uploads/' + basename, (err) => { common.should.not.exist(err); - var newSize = fs.statSync(path.join(common.fixturesPath(), common.defaultOptions().user, 'uploads', basename)).size; + var filePath = path.join( + common.fixturesPath(), + common.defaultOptions().user, + 'uploads', + basename + ); + var newSize = fs.statSync(filePath).size; newSize.should.be.eql(fileSize * 2); done(); }); }); }); - afterEach(function() { + afterEach(() => { server.close(); }); @@ -49,5 +55,4 @@ describe('APPE command', function() { }); - }); diff --git a/src/__tests__/cwd-cdup.js b/src/__tests__/cwd-cdup.js index 3a6d9c0..53e4f6d 100644 --- a/src/__tests__/cwd-cdup.js +++ b/src/__tests__/cwd-cdup.js @@ -1,6 +1,6 @@ var common = require('./lib/common'); -describe('CWD/CDUP commands', function() { +describe('CWD/CDUP commands', () => { 'use strict'; var client; @@ -19,17 +19,17 @@ describe('CWD/CDUP commands', function() { return text; } - beforeEach(function(done) { + beforeEach((done) => { server = common.server(); client = common.client(done); }); - describe('CWD command', function() { - it('should change to existing directory', function(done) { - client.raw('CWD', pathExisting, function(error, response) { + describe('CWD command', () => { + it('should change to existing directory', (done) => { + client.raw('CWD', pathExisting, (error, response) => { var pathCwd = pathExtract(response); response.code.should.equal(250); - client.raw('PWD', function(error, response) { + client.raw('PWD', (error, response) => { var pathPwd = pathExtract(response); response.code.should.equal(257); pathPwd.should.equal(pathCwd); @@ -38,35 +38,35 @@ describe('CWD/CDUP commands', function() { }); }); - it('should not change to non-existent directory', function(done) { - client.raw('CWD', pathExisting, function(error, response) { + it('should not change to non-existent directory', (done) => { + client.raw('CWD', pathExisting, (error, response) => { response.code.should.equal(250); server.suppressExpecteErrMsgs.push( /^CWD \S+: Error: ENOENT/ ); - client.raw('CWD', pathExisting, function(error) { + client.raw('CWD', pathExisting, (error) => { error.code.should.equal(550); done(); }); }); }); - it('should not change to regular file', function(done) { - client.raw('CWD', pathFile, function(error) { + it('should not change to regular file', (done) => { + client.raw('CWD', pathFile, (error) => { error.code.should.equal(550); done(); }); }); - it('should escape quotation marks', function(done) { - client.raw('MKD', pathWithQuotes, function(error, response) { + it('should escape quotation marks', (done) => { + client.raw('MKD', pathWithQuotes, (error, response) => { var pathEscaped = pathEscape(pathWithQuotes); var pathMkd = pathExtract(response); pathMkd.should.equal(pathEscaped); - client.raw('CWD', pathWithQuotes, function(error, response) { + client.raw('CWD', pathWithQuotes, (error, response) => { var pathCwd = pathExtract(response); pathCwd.should.equal(pathEscaped); - client.raw('PWD', function(error, response) { + client.raw('PWD', (error, response) => { var pathPwd = pathExtract(response); pathPwd.should.equal(pathEscaped); client.raw('RMD', pathWithQuotes); @@ -77,14 +77,14 @@ describe('CWD/CDUP commands', function() { }); }); - describe('CDUP command', function() { - it('should change to parent directory', function(done) { - client.raw('CWD', pathExisting, function(error, response) { + describe('CDUP command', () => { + it('should change to parent directory', (done) => { + client.raw('CWD', pathExisting, (error, response) => { response.code.should.equal(250); - client.raw('CDUP', function(error, response) { + client.raw('CDUP', (error, response) => { var pathCdup = pathExtract(response); response.code.should.equal(250); - client.raw('PWD', function(error, response) { + client.raw('PWD', (error, response) => { var pathPwd = pathExtract(response); response.code.should.equal(257); pathCdup.should.equal(pathPwd); @@ -95,7 +95,7 @@ describe('CWD/CDUP commands', function() { }); }); - afterEach(function() { + afterEach(() => { server.close(); }); }); diff --git a/src/__tests__/glob.js b/src/__tests__/glob.js index 1af6d30..69cdd66 100644 --- a/src/__tests__/glob.js +++ b/src/__tests__/glob.js @@ -1,10 +1,10 @@ -var glob = require('../lib/glob'); +var glob = require('../glob'); var assert = require('assert'); var matchPattern = glob.matchPattern; -describe('glob.matchPattern', function() { - it('should match ? and * glob characters', function() { +describe('glob.matchPattern', () => { + it('should match ? and * glob characters', () => { assert.equal(matchPattern('foo*', 'foooxx'), true); assert.equal(matchPattern('foo*', 'foo'), true); assert.equal(matchPattern('foo*', 'fo'), false); diff --git a/src/__tests__/init.js b/src/__tests__/init.js index 2a20262..d7ac183 100644 --- a/src/__tests__/init.js +++ b/src/__tests__/init.js @@ -1,7 +1,7 @@ var common = require('./lib/common'); var Client = require('jsftp'); -describe('initialization', function() { +describe('initialization', () => { 'use strict'; var client; @@ -13,44 +13,44 @@ describe('initialization', function() { pass: 'esoj', }; - beforeEach(function(done) { + beforeEach((done) => { done(); }); - it('should getRoot synchronously', function(done) { + it('should getRoot synchronously', (done) => { server = common.server({ - getRoot: function() { + getRoot: () => { return '../fixture/'; }, }); client = common.client(done); }); - it('should getRoot asynchronously', function(done) { + it('should getRoot asynchronously', (done) => { server = common.server({ - getRoot: function(connection, callback) { + getRoot: (connection, callback) => { callback(null, '../fixture/'); }, }); client = common.client(done); }); - it('should bail if getRoot fails', function(done) { + it('should bail if getRoot fails', (done) => { server = common.server({ - getRoot: function(connection, callback) { + getRoot: (connection, callback) => { server.suppressExpecteErrMsgs.push( 'getRoot signaled error [Error: intentional failure]'); callback(new Error('intentional failure')); }, }); client = new Client(options); - client.auth(options.user, options.pass, function(error) { + client.auth(options.user, options.pass, (error) => { error.code.should.eql(421); done(); }); }); - it('should throw if getRoot is null', function(done) { + it('should throw if getRoot is null', (done) => { var fail = false; try { server = common.server({ @@ -64,40 +64,40 @@ describe('initialization', function() { done(); }); - it('should getInitialCwd synchronously', function(done) { + it('should getInitialCwd synchronously', (done) => { server = common.server({ - getInitialCwd: function() { + getInitialCwd: () => { return '/'; }, }); client = common.client(done); }); - it('should getInitialCwd asynchronously', function(done) { + it('should getInitialCwd asynchronously', (done) => { server = common.server({ - getInitialCwd: function(connection, callback) { + getInitialCwd: (connection, callback) => { callback(null, '/'); }, }); client = common.client(done); }); - it('should bail if getInitialCwd fails', function(done) { + it('should bail if getInitialCwd fails', (done) => { server = common.server({ - getInitialCwd: function(connection, callback) { + getInitialCwd: (connection, callback) => { server.suppressExpecteErrMsgs.push( 'getInitialCwd signaled error [Error: intentional failure]'); callback(new Error('intentional failure')); }, }); client = new Client(options); - client.auth(options.user, options.pass, function(error) { + client.auth(options.user, options.pass, (error) => { error.code.should.eql(421); done(); }); }); - it('should throw if getInitialCwd is null', function(done) { + it('should throw if getInitialCwd is null', (done) => { var fail = false; try { server = common.server({ @@ -111,7 +111,7 @@ describe('initialization', function() { done(); }); - afterEach(function() { + afterEach(() => { server.close(); }); }); diff --git a/src/__tests__/lib/common.js b/src/__tests__/lib/common.js index e41847b..66714f1 100644 --- a/src/__tests__/lib/common.js +++ b/src/__tests__/lib/common.js @@ -1,30 +1,29 @@ -'use strict'; +import path from 'path'; +import util from 'util'; +import fs from 'fs'; +import Server from '../../FtpServer'; +import Constants from '../../Constants'; +import Client from 'jsftp'; +import should from 'should'; -var path = require('path'); -var util = require('util'); -var fs = require('fs'); -var ftpd = require('../../'); -var Client = require('jsftp'); -var should = require('should'); - -var Server = ftpd.FtpServer; -var LogLevels = ftpd.LOG_LEVELS; -var LogLevelNames = Object.keys(LogLevels).reduce(function(map, name) { - var value = LogLevels[name]; +var {LOG_LEVELS} = Constants; +// TODO: replace this stuff with github.com/rauschma/enumify +var LogLevelNames = Object.keys(LOG_LEVELS).reduce((map, name) => { + var value = LOG_LEVELS[name]; map[value] = name; return map; }, {}); -var fixturesPath = path.join(__dirname, '../../fixture'); +var fixturesPath = path.join(__dirname, '../../../fixture'); -function toString(value) { +const toString = (value) => { var isPrimitive = Object(value) !== value; if (isPrimitive) { return JSON.stringify(value); } else { return ('toString' in value) ? value.toString() : Object.prototype.toString(value); } -} +}; var options = { host: process.env.IP || '127.0.0.1', @@ -32,38 +31,38 @@ var options = { user: 'jose', pass: 'esoj', tlsOnly: false, - getInitialCwd: function() { + getInitialCwd() { return options.cwd; }, - getRoot: function(connection, callback) { + getRoot(connection, callback) { var username = connection.username; var root = path.join(fixturesPath, username); fs.realpath(root, callback); }, }; -var common = module.exports = { +const common = { should: should, - fixturesPath: function() { + fixturesPath() { return fixturesPath; }, - defaultOptions: function() { + defaultOptions() { return options; }, - server: function(customOptions) { + server(customOptions) { customOptions = customOptions || {}; - Object.keys(options).forEach(function(key) { + Object.keys(options).forEach((key) => { if (!customOptions.hasOwnProperty(key)) { customOptions[key] = options[key]; } }); var server = new Server(customOptions.host, customOptions); - server.on('client:connected', function(connection) { + server.on('client:connected', (connection) => { var username; - connection.on('command:user', function(user, success, failure) { + connection.on('command:user', (user, success, failure) => { if (user === customOptions.user) { username = user; success(); @@ -71,7 +70,7 @@ var common = module.exports = { failure(); } }); - connection.on('command:pass', function(pass, success, failure) { + connection.on('command:pass', (pass, success, failure) => { if (pass === customOptions.pass) { success(username); } else { @@ -81,10 +80,10 @@ var common = module.exports = { }); var origLogIf = server._logIf; server.suppressExpecteErrMsgs = []; - server._logIf = function logIfNotExpected(verbosity, message, conn) { + server._logIf = (verbosity, message, conn) => { var expecteErrMsgs = server.suppressExpecteErrMsgs; message = String(message).split(fixturesPath).join('fixture:/'); - if ((expecteErrMsgs.length > 0) && (verbosity < LogLevels.LOG_INFO)) { + if ((expecteErrMsgs.length > 0) && (verbosity < LOG_LEVELS.INFO)) { var expected = expecteErrMsgs.shift(); if (message === expected) { return; @@ -105,15 +104,15 @@ var common = module.exports = { ); } } - return origLogIf.call(this, verbosity, message, conn); + return origLogIf.call(server, verbosity, message, conn); }; server.listen(customOptions.port); return server; }, - client: function(done, customOptions) { + client(done, customOptions) { customOptions = customOptions || {}; - Object.keys(options).forEach(function(key) { + Object.keys(options).forEach((key) => { if (!customOptions.hasOwnProperty(key)) { customOptions[key] = options[key]; } @@ -125,7 +124,7 @@ var common = module.exports = { client.auth( customOptions.user, customOptions.pass, - function(error, response) { + (error, response) => { should.not.exist(error); should.exist(response); response.should.have.property('code', 230); @@ -134,9 +133,9 @@ var common = module.exports = { ); return client; }, - genFilterFuncFrom: function(filter) { + genFilterFuncFrom(filter) { if (!filter) { - return function() { + return () => { return true; }; } @@ -144,7 +143,7 @@ var common = module.exports = { return filter; } if ((typeof filter) === 'string') { - return function(item) { + return (item) => { return String(item).indexOf(filter) !== -1; }; } @@ -154,7 +153,7 @@ var common = module.exports = { } throw new Error('unsupported filter precursor: ' + util.inspect(filter)); }, - splitResponseLines: function(resp, filter) { + splitResponseLines(resp, filter) { var respType = typeof resp; respType.should.equal('string'); resp = String(resp); @@ -169,3 +168,5 @@ var common = module.exports = { return resp; }, }; + +module.exports = common; diff --git a/src/__tests__/list.js b/src/__tests__/list.js index 31e0773..6b8bff4 100644 --- a/src/__tests__/list.js +++ b/src/__tests__/list.js @@ -1,12 +1,12 @@ var common = require('./lib/common'); -describe('LIST command', function() { +describe('LIST command', () => { 'use strict'; var client; var server; - beforeEach(function(done) { + beforeEach((done) => { server = common.server(); client = common.client(done); }); @@ -15,8 +15,8 @@ describe('LIST command', function() { return String(rgx).replace(/^\/|\/$/g, ''); } - it('should return "-" as first character for files', function(done) { - client.list('/', function(error, listing) { + it('should return "-" as first character for files', (done) => { + client.list('/', (error, listing) => { error.should.equal(false); listing = common.splitResponseLines(listing, / data\d*\.txt$/); listing.should.have.lengthOf(6); @@ -25,8 +25,8 @@ describe('LIST command', function() { }); }); - it('should return "d" as first character for directories', function(done) { - client.list('/', function(error, listing) { + it('should return "d" as first character for directories', (done) => { + client.list('/', (error, listing) => { error.should.equal(false); listing = common.splitResponseLines(listing, / usr$/); listing.should.have.lengthOf(1); @@ -35,14 +35,14 @@ describe('LIST command', function() { }); }); - it('should list files similar to ls -l', function(done) { - client.list('/usr', function(error, listing) { + it('should list files similar to ls -l', (done) => { + client.list('/usr', (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+/, + /($# ?�?�? inodes?: |)\d+/, /($# owner name: ___|)\S+/, /($# owner group: __|)\S+/, /($# size in bytes: |)\d+/, @@ -58,9 +58,9 @@ describe('LIST command', function() { }); }); - it('should list a single file', function(done) { + it('should list a single file', (done) => { var filename = 'data.txt'; - client.list('/' + filename, function(error, listing) { + client.list('/' + filename, (error, listing) => { error.should.equal(false); listing = common.splitResponseLines(listing, ' ' + filename); listing.should.have.lengthOf(1); @@ -69,8 +69,8 @@ describe('LIST command', function() { }); }); - it('should list a subdirectory', function(done) { - client.list('/usr', function(error, listing) { + it('should list a subdirectory', (done) => { + client.list('/usr', (error, listing) => { error.should.equal(false); listing = common.splitResponseLines(listing); listing.should.have.lengthOf(1); @@ -80,7 +80,7 @@ describe('LIST command', function() { }); }); - afterEach(function() { + afterEach(() => { server.close(); }); }); diff --git a/src/__tests__/mdtm.js b/src/__tests__/mdtm.js index 61c6fda..89cd230 100644 --- a/src/__tests__/mdtm.js +++ b/src/__tests__/mdtm.js @@ -1,32 +1,32 @@ var common = require('./lib/common'); -describe('MDTM command', function() { +describe('MDTM command', () => { 'use strict'; var client; var server; - beforeEach(function(done) { + beforeEach((done) => { server = common.server(); client = common.client(done); }); - it('should respond 213 for a valid file', function(done) { - client.raw('MDTM', '/data.txt', function(error, response) { + it('should respond 213 for a valid file', (done) => { + client.raw('MDTM', '/data.txt', (error, response) => { common.should.not.exist(error); response.text.should.match(/^213 [0-9]{14}$/); done(); }); }); - it('should respond 550 for an invalid file', function(done) { - client.raw('MDTM', '/data-something.txt', function(error) { + it('should respond 550 for an invalid file', (done) => { + client.raw('MDTM', '/data-something.txt', (error) => { error.code.should.equal(550); done(); }); }); - afterEach(function() { + afterEach(() => { server.close(); }); }); diff --git a/src/__tests__/mkd-rmd.js b/src/__tests__/mkd-rmd.js index 0dee870..cab7739 100644 --- a/src/__tests__/mkd-rmd.js +++ b/src/__tests__/mkd-rmd.js @@ -1,57 +1,57 @@ var common = require('./lib/common'); -describe('MKD/RMD commands', function() { +describe('MKD/RMD commands', () => { 'use strict'; var client; var server; var directory = '/testdir'; - beforeEach(function(done) { + beforeEach((done) => { server = common.server(); client = common.client(done); }); - describe('MKD command', function() { - it('should create a new directory', function(done) { - client.raw('MKD', directory, function(error, response) { + describe('MKD command', () => { + it('should create a new directory', (done) => { + client.raw('MKD', directory, (error, response) => { common.should.not.exist(error); response.text.should.startWith(257); done(); }); }); - it('should not create a duplicate directory', function(done) { + it('should not create a duplicate directory', (done) => { server.suppressExpecteErrMsgs.push( /^MKD \S+: Error: EEXIST/ ); - client.raw('MKD', directory, function(error) { + client.raw('MKD', directory, (error) => { error.code.should.equal(550); done(); }); }); }); - describe('RMD command', function() { - it('should delete an existing directory', function(done) { - client.raw('RMD', directory, function(error, response) { + describe('RMD command', () => { + it('should delete an existing directory', (done) => { + client.raw('RMD', directory, (error, response) => { common.should.not.exist(error); response.text.should.startWith(250); done(); }); }); - it('should not delete a non-existent directory', function(done) { + it('should not delete a non-existent directory', (done) => { server.suppressExpecteErrMsgs.push( /^RMD \S+: Error: ENOENT/); - client.raw('RMD', directory, function(error) { + client.raw('RMD', directory, (error) => { error.code.should.equal(550); done(); }); }); }); - afterEach(function() { + afterEach(() => { server.close(); }); }); diff --git a/src/__tests__/noop.js b/src/__tests__/noop.js index bdc6a44..5dcea13 100644 --- a/src/__tests__/noop.js +++ b/src/__tests__/noop.js @@ -1,25 +1,25 @@ var common = require('./lib/common'); -describe('NOOP command', function() { +describe('NOOP command', () => { 'use strict'; var client; var server; - beforeEach(function(done) { + beforeEach((done) => { server = common.server(); client = common.client(done); }); - it('should perform a NOOP', function(done) { - client.raw('NOOP', function(error, response) { + it('should perform a NOOP', (done) => { + client.raw('NOOP', (error, response) => { common.should.not.exist(error); response.code.should.equal(200); done(); }); }); - afterEach(function() { + afterEach(() => { server.close(); }); }); diff --git a/src/__tests__/pass.js b/src/__tests__/pass.js index c900b7f..bf08f7b 100644 --- a/src/__tests__/pass.js +++ b/src/__tests__/pass.js @@ -1,7 +1,7 @@ var common = require('./lib/common'); var Client = require('jsftp'); -describe('PASS command', function() { +describe('PASS command', () => { 'use strict'; var client; @@ -13,19 +13,19 @@ describe('PASS command', function() { pass: 'esoj', }; - beforeEach(function(done) { + beforeEach((done) => { server = common.server(options); done(); }); - it('should reject invalid password', function(done) { + it('should reject invalid password', (done) => { var badPass = options.pass + '_invalid'; client = new Client(options); - client.auth(options.user, badPass, function(error) { + client.auth(options.user, badPass, (error) => { error.code.should.eql(530); - client.raw.user(options.user, function(error, reply) { + client.raw.user(options.user, (error, reply) => { reply.code.should.eql(331); - client.raw.pass(badPass, function(error) { + client.raw.pass(badPass, (error) => { error.code.should.eql(530); done(); }); @@ -33,15 +33,15 @@ describe('PASS command', function() { }); }); - it('should reject PASS without USER', function(done) { + it('should reject PASS without USER', (done) => { client = new Client(options); - client.raw.pass(options.pass, function(error) { + client.raw.pass(options.pass, (error) => { error.code.should.eql(503); done(); }); }); - afterEach(function() { + afterEach(() => { server.close(); }); }); diff --git a/src/__tests__/pwd.js b/src/__tests__/pwd.js index 4508784..6a6bc1d 100644 --- a/src/__tests__/pwd.js +++ b/src/__tests__/pwd.js @@ -1,7 +1,7 @@ var common = require('./lib/common'); var path = require('path'); -describe('PWD command', function() { +describe('PWD command', () => { 'use strict'; var client; @@ -18,19 +18,19 @@ describe('PWD command', function() { undefined, ]; - directories.forEach(function(directory) { - describe('CWD = "' + directory + '"', function() { - beforeEach(function(done) { + directories.forEach((directory) => { + describe('CWD = "' + directory + '"', () => { + beforeEach((done) => { server = common.server({ - getInitialCwd: function() { + getInitialCwd: () => { return directory; }, }); client = common.client(done); }); - it('should be "' + directory + '"', function(done) { - client.raw.pwd(function(error, reply) { + it('should be "' + directory + '"', (done) => { + client.raw.pwd((error, reply) => { common.should.not.exist(error); reply.code.should.equal(257); reply.text.should.startWith('257 "' + directory + '"'); @@ -38,33 +38,33 @@ describe('PWD command', function() { }); }); - it('should reject parameters', function(done) { - client.raw.pwd(directory, function(error, reply) { + it('should reject parameters', (done) => { + client.raw.pwd(directory, (error, reply) => { error.code.should.equal(501); reply.code.should.equal(501); done(); }); }); - afterEach(function() { + afterEach(() => { server.close(); }); }); }); - falseyDirectories.forEach(function(directory) { - describe('CWD = "' + directory + '"', function() { - beforeEach(function(done) { + falseyDirectories.forEach((directory) => { + describe('CWD = "' + directory + '"', () => { + beforeEach((done) => { server = common.server({ - getInitialCwd: function() { + getInitialCwd: () => { return directory; }, }); client = common.client(done); }); - it('should be "/"', function(done) { - client.raw.pwd(function(error, reply) { + it('should be "/"', (done) => { + client.raw.pwd((error, reply) => { common.should.not.exist(error); reply.code.should.equal(257); reply.text.should.startWith('257 "/"'); @@ -72,7 +72,7 @@ describe('PWD command', function() { }); }); - afterEach(function() { + afterEach(() => { server.close(); }); }); diff --git a/src/__tests__/retr.js b/src/__tests__/retr.js index 804614d..d58c6b6 100644 --- a/src/__tests__/retr.js +++ b/src/__tests__/retr.js @@ -1,28 +1,28 @@ var common = require('./lib/common'); -describe('RETR command', function() { +describe('RETR command', () => { 'use strict'; var client; var server; //run tests both ways - [true, false].forEach(function(useReadFile) { + [true, false].forEach((useReadFile) => { - describe('with useReadFile = ' + useReadFile, function() { + describe('with useReadFile = ' + useReadFile, () => { - beforeEach(function(done) { - server = common.server({useReadFile:useReadFile}); + beforeEach((done) => { + server = common.server({useReadFile: useReadFile}); client = common.client(done); }); - it('should contain "hola!"', function(done) { + it('should contain "hola!"', (done) => { var str = ''; - client.get('/data.txt', function(error, socket) { + client.get('/data.txt', (error, socket) => { common.should.not.exist(error); - socket.on('data', function(data) { + socket.on('data', (data) => { str += data.toString(); - }).on('close', function(error) { + }).on('close', (error) => { error.should.not.equal(true); str.should.eql('hola!'); done(); @@ -30,14 +30,14 @@ describe('RETR command', function() { }); }); - it('should fail when file not found', function(done) { - client.get('/bad.file', function(error) { + it('should fail when file not found', (done) => { + client.get('/bad.file', (error) => { common.should.exist(error); done(); }); }); - afterEach(function() { + afterEach(() => { server.close(); }); diff --git a/src/__tests__/tricky-paths.js b/src/__tests__/tricky-paths.js index aa3d6c4..3bf286a 100644 --- a/src/__tests__/tricky-paths.js +++ b/src/__tests__/tricky-paths.js @@ -6,28 +6,28 @@ var common = require('./lib/common'); var async = require('async'); var collectStream = require('collect-stream'); -describe('Tricky paths', function() { +describe('Tricky paths', () => { var client; var server; //run tests both ways - [true, false].forEach(function(useReadFile) { + [true, false].forEach((useReadFile) => { - describe('with useReadFile = ' + useReadFile, function() { + describe('with useReadFile = ' + useReadFile, () => { - beforeEach(function(done) { + beforeEach((done) => { server = common.server({useReadFile: useReadFile}); client = common.client(done); }); - it('should cope with unusual paths', function(done) { + it('should cope with unusual paths', (done) => { var coolGlasses = '\uD83D\uDE0E'; var trickyName = "b\\\\s\\l, \"\"q'u\"o\"te''; pi|p|e & ^up^"; var dirPath = 'tricky_paths/' + trickyName; var expectedData = 'good ' + coolGlasses + '\nfilesystem.\n'; function receiveAndCompare(socket, nxt) { - collectStream(socket, function(error, receivedData) { + collectStream(socket, (error, receivedData) => { common.should.not.exist(error); String(receivedData).should.eql(expectedData); nxt(); @@ -35,45 +35,49 @@ describe('Tricky paths', function() { socket.resume(); } - async.waterfall([ - function strangePathRedundantEscape(nxt) { - var dirRfcQuoted = dirPath.replace(/"/g, '""'); - server.suppressExpecteErrMsgs.push( - /^CWD [\S\s]+: Error: ENOENT/ - ); - client.raw('CWD', dirRfcQuoted, function(error) { - common.should.exist(error); - error.code.should.equal(550); + async.waterfall( + [ + function strangePathRedundantEscape(nxt) { + var dirRfcQuoted = dirPath.replace(/"/g, '""'); + server.suppressExpecteErrMsgs.push( + /^CWD [\S\s]+: Error: ENOENT/ + ); + client.raw('CWD', dirRfcQuoted, (error) => { + common.should.exist(error); + error.code.should.equal(550); + nxt(); + }); + }, + function strangePathCwd(nxt) { + client.raw('CWD', dirPath, nxt); + }, + function checkResponse(response, nxt) { + response.code.should.equal(250); + if (response.code !== 250) { + return nxt(new Error('failed to CWD to unusual path')); + } nxt(); - }); - }, - function strangePathCwd(nxt) { - client.raw('CWD', dirPath, nxt); - }, - function checkResponse(response, nxt) { - response.code.should.equal(250); - if (response.code !== 250) { - return nxt(new Error('failed to CWD to unusual path')); - } - nxt(); - }, - function strangePathRetr(nxt) { - var filename = trickyName + '.txt'; - client.get(filename, nxt); - }, - receiveAndCompare, - function strangePathRetr(nxt) { - var filename = 'cool-glasses.' + coolGlasses + '.txt'; - client.get(filename, nxt); - }, - receiveAndCompare, - ], function finished(error) { - common.should.not.exist(error); - done(); - }); + }, + function strangePathRetr(nxt) { + var filename = trickyName + '.txt'; + client.get(filename, nxt); + }, + receiveAndCompare, + function strangePathRetr(nxt) { + var filename = 'cool-glasses.' + coolGlasses + '.txt'; + client.get(filename, nxt); + }, + receiveAndCompare, + ], + // finished callback + (error) => { + common.should.not.exist(error); + done(); + } + ); }); - afterEach(function() { + afterEach(() => { server.close(); }); diff --git a/src/__tests__/unsupported.js b/src/__tests__/unsupported.js index 6c741b8..dcd4a1d 100644 --- a/src/__tests__/unsupported.js +++ b/src/__tests__/unsupported.js @@ -1,6 +1,6 @@ var common = require('./lib/common'); -describe('UNSUPPORTED commands', function() { +describe('UNSUPPORTED commands', () => { 'use strict'; var client; @@ -23,14 +23,14 @@ describe('UNSUPPORTED commands', function() { 'CD', ]; - beforeEach(function(done) { + beforeEach((done) => { server = common.server(); client = common.client(done); }); - commands.forEach(function(command) { - it('should reply 502 to ' + command, function(done) { - var callback = function(error) { + commands.forEach((command) => { + it('should reply 502 to ' + command, (done) => { + var callback = (error) => { error.code.should.eql(502); done(); }; @@ -43,7 +43,7 @@ describe('UNSUPPORTED commands', function() { }); }); - afterEach(function() { + afterEach(() => { server.close(); }); }); diff --git a/src/__tests__/user.js b/src/__tests__/user.js index e5447dc..703efb0 100644 --- a/src/__tests__/user.js +++ b/src/__tests__/user.js @@ -1,7 +1,7 @@ var common = require('./lib/common'); var Client = require('jsftp'); -describe('USER command', function() { +describe('USER command', () => { 'use strict'; var client; @@ -13,38 +13,38 @@ describe('USER command', function() { pass: 'esoj', }; - beforeEach(function(done) { + beforeEach((done) => { done(); }); - it('should reject non-secure USER when tlsOnly', function(done) { + it('should reject non-secure USER when tlsOnly', (done) => { server = common.server({ tlsOnly: true, }); client = new Client(options); - client.auth(options.user, options.pass, function(error) { + client.auth(options.user, options.pass, (error) => { error.code.should.eql(530); - client.raw.user(options.user, function(error) { + client.raw.user(options.user, (error) => { error.code.should.eql(530); done(); }); }); }); - it('should reject invalid username', function(done) { + it('should reject invalid username', (done) => { var badUser = options.user + '_invalid'; server = common.server(); client = new Client(options); - client.auth(badUser, options.pass, function(error) { + client.auth(badUser, options.pass, (error) => { error.code.should.eql(530); - client.raw.user(badUser, function(error) { + client.raw.user(badUser, (error) => { error.code.should.eql(530); done(); }); }); }); - afterEach(function() { + afterEach(() => { server.close(); }); }); diff --git a/src/__tests__/utf8.js b/src/__tests__/utf8.js index 4c70c8a..8189152 100644 --- a/src/__tests__/utf8.js +++ b/src/__tests__/utf8.js @@ -1,19 +1,19 @@ var common = require('./lib/common'); -describe('UTF8 support', function() { +describe('UTF8 support', () => { 'use strict'; var client; var server; - beforeEach(function(done) { + beforeEach((done) => { server = common.server(); client = common.client(done); }); - it('should support UTF8 in LIST command', function(done) { + it('should support UTF8 in LIST command', (done) => { var filename = 'привіт.txt'; - client.list('/' + filename, function(error, listing) { + client.list('/' + filename, (error, listing) => { error.should.equal(false); listing = common.splitResponseLines(listing, ' ' + filename); listing.should.have.lengthOf(1); @@ -22,14 +22,14 @@ describe('UTF8 support', function() { }); }); - it('should RETR file with UTF8 in filename', function(done) { + it('should RETR file with UTF8 in filename', (done) => { var filename = 'привіт.txt'; var str = ''; - client.get('/' + filename, function(error, socket) { + client.get('/' + filename, (error, socket) => { common.should.not.exist(error); - socket.on('data', function(data) { + socket.on('data', (data) => { str += data.toString(); - }).on('close', function(error) { + }).on('close', (error) => { error.should.not.equal(true); str.should.eql('1234\n'); done(); @@ -37,7 +37,7 @@ describe('UTF8 support', function() { }); }); - afterEach(function() { + afterEach(() => { server.close(); }); }); diff --git a/src/__tests__/whitelisted.js b/src/__tests__/whitelisted.js index 533e272..5e30cfe 100644 --- a/src/__tests__/whitelisted.js +++ b/src/__tests__/whitelisted.js @@ -1,6 +1,6 @@ var common = require('./lib/common'); -describe('Whitelisted commands', function() { +describe('Whitelisted commands', () => { 'use strict'; var client; @@ -15,41 +15,41 @@ describe('Whitelisted commands', function() { ], }; - beforeEach(function(done) { + beforeEach((done) => { server = common.server(options); client = common.client(done); }); - it('LIST should be allowed', function(done) { - client.list('/', function(error) { + it('LIST should be allowed', (done) => { + client.list('/', (error) => { common.should(error).not.be.ok; done(); }); }); - it('NOOP should be allowed', function(done) { - client.raw('NOOP', function(error, response) { + it('NOOP should be allowed', (done) => { + client.raw('NOOP', (error, response) => { common.should.not.exist(error); response.code.should.equal(200); done(); }); }); - it('DELE should reply 502', function(done) { - client.execute('DELE', function(error) { + it('DELE should reply 502', (done) => { + client.execute('DELE', (error) => { error.code.should.eql(502); done(); }); }); - it('RETR should reply 502', function(done) { - client.get('/myfile', function(error) { + it('RETR should reply 502', (done) => { + client.get('/myfile', (error) => { error.code.should.eql(502); done(); }); }); - afterEach(function() { + afterEach(() => { server.close(); }); }); From fdfd9cfe8c201136a0db58a6db549486235b071d Mon Sep 17 00:00:00 2001 From: Simon Sturmer Date: Sun, 17 Jan 2016 19:51:40 +0700 Subject: [PATCH 25/55] ES6: fix ftpd.js --- ftpd.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ftpd.js b/ftpd.js index b582826..c0588f8 100644 --- a/ftpd.js +++ b/ftpd.js @@ -1,4 +1,4 @@ -var Constants = require('./lib/Constants'); +var Constants = require('./lib/Constants').default; -exports.FtpServer = require('./lib/FtpServer'); +exports.FtpServer = require('./lib/FtpServer').default; exports.LOG_LEVELS = Constants.LOG_LEVELS; From e3a9524efad764be2622faf8123e7e28b1d48aae Mon Sep 17 00:00:00 2001 From: Simon Sturmer Date: Sun, 17 Jan 2016 17:42:20 +0700 Subject: [PATCH 26/55] remove "use strict" since ES6 has no concept of strict --- src/__tests__/acct.js | 2 -- src/__tests__/allo.js | 2 -- src/__tests__/appe.js | 2 -- src/__tests__/cwd-cdup.js | 2 -- src/__tests__/init.js | 2 -- src/__tests__/list.js | 2 -- src/__tests__/mdtm.js | 2 -- src/__tests__/mkd-rmd.js | 2 -- src/__tests__/noop.js | 2 -- src/__tests__/pass.js | 2 -- src/__tests__/pwd.js | 2 -- src/__tests__/retr.js | 2 -- src/__tests__/tricky-paths.js | 4 ---- src/__tests__/unsupported.js | 2 -- src/__tests__/user.js | 2 -- src/__tests__/utf8.js | 2 -- src/__tests__/whitelisted.js | 2 -- 17 files changed, 36 deletions(-) diff --git a/src/__tests__/acct.js b/src/__tests__/acct.js index be594f4..89c23a9 100644 --- a/src/__tests__/acct.js +++ b/src/__tests__/acct.js @@ -1,8 +1,6 @@ var common = require('./lib/common'); describe('ACCT command', () => { - 'use strict'; - var client; var server; diff --git a/src/__tests__/allo.js b/src/__tests__/allo.js index cd214c9..fbafab6 100644 --- a/src/__tests__/allo.js +++ b/src/__tests__/allo.js @@ -1,8 +1,6 @@ var common = require('./lib/common'); describe('ALLO command', () => { - 'use strict'; - var client; var server; diff --git a/src/__tests__/appe.js b/src/__tests__/appe.js index eb632ee..4054dd6 100644 --- a/src/__tests__/appe.js +++ b/src/__tests__/appe.js @@ -4,8 +4,6 @@ var path = require('path'); var fs = require('fs'); describe('APPE command', () => { - 'use strict'; - var client = new FtpClient(); var server; diff --git a/src/__tests__/cwd-cdup.js b/src/__tests__/cwd-cdup.js index 53e4f6d..2a449d7 100644 --- a/src/__tests__/cwd-cdup.js +++ b/src/__tests__/cwd-cdup.js @@ -1,8 +1,6 @@ var common = require('./lib/common'); describe('CWD/CDUP commands', () => { - 'use strict'; - var client; var server; var pathExisting = 'usr/local'; diff --git a/src/__tests__/init.js b/src/__tests__/init.js index d7ac183..c13d7af 100644 --- a/src/__tests__/init.js +++ b/src/__tests__/init.js @@ -2,8 +2,6 @@ var common = require('./lib/common'); var Client = require('jsftp'); describe('initialization', () => { - 'use strict'; - var client; var server; var options = { diff --git a/src/__tests__/list.js b/src/__tests__/list.js index 6b8bff4..b8a8ecd 100644 --- a/src/__tests__/list.js +++ b/src/__tests__/list.js @@ -1,8 +1,6 @@ var common = require('./lib/common'); describe('LIST command', () => { - 'use strict'; - var client; var server; diff --git a/src/__tests__/mdtm.js b/src/__tests__/mdtm.js index 89cd230..9a8236a 100644 --- a/src/__tests__/mdtm.js +++ b/src/__tests__/mdtm.js @@ -1,8 +1,6 @@ var common = require('./lib/common'); describe('MDTM command', () => { - 'use strict'; - var client; var server; diff --git a/src/__tests__/mkd-rmd.js b/src/__tests__/mkd-rmd.js index cab7739..200c00e 100644 --- a/src/__tests__/mkd-rmd.js +++ b/src/__tests__/mkd-rmd.js @@ -1,8 +1,6 @@ var common = require('./lib/common'); describe('MKD/RMD commands', () => { - 'use strict'; - var client; var server; var directory = '/testdir'; diff --git a/src/__tests__/noop.js b/src/__tests__/noop.js index 5dcea13..1d920ee 100644 --- a/src/__tests__/noop.js +++ b/src/__tests__/noop.js @@ -1,8 +1,6 @@ var common = require('./lib/common'); describe('NOOP command', () => { - 'use strict'; - var client; var server; diff --git a/src/__tests__/pass.js b/src/__tests__/pass.js index bf08f7b..d3c58b0 100644 --- a/src/__tests__/pass.js +++ b/src/__tests__/pass.js @@ -2,8 +2,6 @@ var common = require('./lib/common'); var Client = require('jsftp'); describe('PASS command', () => { - 'use strict'; - var client; var server; var options = { diff --git a/src/__tests__/pwd.js b/src/__tests__/pwd.js index 6a6bc1d..a811929 100644 --- a/src/__tests__/pwd.js +++ b/src/__tests__/pwd.js @@ -2,8 +2,6 @@ var common = require('./lib/common'); var path = require('path'); describe('PWD command', () => { - 'use strict'; - var client; var server; var directories = [ diff --git a/src/__tests__/retr.js b/src/__tests__/retr.js index d58c6b6..bd218ff 100644 --- a/src/__tests__/retr.js +++ b/src/__tests__/retr.js @@ -1,8 +1,6 @@ var common = require('./lib/common'); describe('RETR command', () => { - 'use strict'; - var client; var server; diff --git a/src/__tests__/tricky-paths.js b/src/__tests__/tricky-paths.js index 3bf286a..4936f82 100644 --- a/src/__tests__/tricky-paths.js +++ b/src/__tests__/tricky-paths.js @@ -1,7 +1,3 @@ -/*jslint indent: 2, maxlen: 80, node: true, white: true, vars: true */ -/*globals describe, it, beforeEach, afterEach */ -'use strict'; - var common = require('./lib/common'); var async = require('async'); var collectStream = require('collect-stream'); diff --git a/src/__tests__/unsupported.js b/src/__tests__/unsupported.js index dcd4a1d..180f9c3 100644 --- a/src/__tests__/unsupported.js +++ b/src/__tests__/unsupported.js @@ -1,8 +1,6 @@ var common = require('./lib/common'); describe('UNSUPPORTED commands', () => { - 'use strict'; - var client; var server; var commands = [ diff --git a/src/__tests__/user.js b/src/__tests__/user.js index 703efb0..5d4e928 100644 --- a/src/__tests__/user.js +++ b/src/__tests__/user.js @@ -2,8 +2,6 @@ var common = require('./lib/common'); var Client = require('jsftp'); describe('USER command', () => { - 'use strict'; - var client; var server; var options = { diff --git a/src/__tests__/utf8.js b/src/__tests__/utf8.js index 8189152..63be34c 100644 --- a/src/__tests__/utf8.js +++ b/src/__tests__/utf8.js @@ -1,8 +1,6 @@ var common = require('./lib/common'); describe('UTF8 support', () => { - 'use strict'; - var client; var server; diff --git a/src/__tests__/whitelisted.js b/src/__tests__/whitelisted.js index 5e30cfe..a77ef2d 100644 --- a/src/__tests__/whitelisted.js +++ b/src/__tests__/whitelisted.js @@ -1,8 +1,6 @@ var common = require('./lib/common'); describe('Whitelisted commands', () => { - 'use strict'; - var client; var server; var options = { From d9a6d62a2b2588b908483703f31ae4047f5719b5 Mon Sep 17 00:00:00 2001 From: Simon Sturmer Date: Sun, 17 Jan 2016 18:27:44 +0700 Subject: [PATCH 27/55] FtpConnection: [method names] replace _command_FOO with __FOO; explicit list of supporte commands --- src/Constants.js | 51 +++++++++++++++++-- src/FtpConnection.js | 117 ++++++++++++++++++++----------------------- 2 files changed, 101 insertions(+), 67 deletions(-) diff --git a/src/Constants.js b/src/Constants.js index 3de0ba7..8c4ba4c 100644 --- a/src/Constants.js +++ b/src/Constants.js @@ -1,8 +1,47 @@ const Constants = { CONCURRENT_STAT_CALLS: 5, - // Whitelist of commands which don't require authentication. + + // Alphabetized list of all commands that have a corresponding "__" prefixed + // method (basically, all commands we support). + COMMANDS_SUPPORTED: { + ALLO: true, + ACCT: true, + APPE: true, + AUTH: true, + CDUP: true, + CWD: true, + DELE: true, + EPRT: true, + EPSV: true, + FEAT: true, + LIST: true, + MDTM: true, + MKD: true, + NLST: true, + NOOP: true, + OPTS: true, + PASS: true, + PASV: true, + PBSZ: true, + PORT: true, + PROT: true, + PWD: true, + QUIT: true, + RETR: true, + RMD: true, + RNFR: true, + RNTO: true, + SIZE: true, + STAT: true, + STOR: true, + SYST: true, + TYPE: true, + USER: true, + }, + + // List of all commands which don't require authentication. // All other commands sent by unauthorized users will be rejected by default. - DOES_NOT_REQUIRE_AUTH: { + COMMANDS_NO_AUTH: { AUTH: true, FEAT: true, NOOP: true, @@ -14,14 +53,16 @@ const Constants = { SYST: true, USER: true, }, - // Commands which can't be issued until a PASV/PORT command has been sent - // without an intervening data connection error. - REQUIRES_CONFIGURED_DATA: { + + // List of all commands which can't be issued unless a PASV/PORT command has + // been received and the corresponding data socket is not in an error state. + COMMANDS_REQUIRE_DATA_SOCKET: { LIST: true, NLST: true, RETR: true, STOR: true, }, + LOG_LEVELS: { ERROR: 0, WARN: 1, diff --git a/src/FtpConnection.js b/src/FtpConnection.js index 5b1753a..27bbdfa 100644 --- a/src/FtpConnection.js +++ b/src/FtpConnection.js @@ -1,5 +1,3 @@ -/* eslint-disable camelcase */ - import net from 'net'; import util from 'util'; import {EventEmitter} from 'events'; @@ -21,8 +19,9 @@ import leftPad from './helpers/leftPad'; var { // Use LOG for brevity. LOG_LEVELS: LOG, - DOES_NOT_REQUIRE_AUTH, - REQUIRES_CONFIGURED_DATA, + COMMANDS_SUPPORTED, + COMMANDS_NO_AUTH, + COMMANDS_REQUIRE_DATA_SOCKET, } = Constants; class FtpConnection extends EventEmitter { @@ -249,51 +248,45 @@ class FtpConnection extends EventEmitter { commandArg = ''; } - const checkData = () => { - if (REQUIRES_CONFIGURED_DATA[command] && !this.dataConfigured) { - this.respond('425 Data connection not configured; send PASV or PORT'); - return; - } - this[m](commandArg, command); - }; - - var m = '_command_' + command; - if (this[m]) { - if (this.allowedCommands != null && this.allowedCommands[command] !== true) { - this.respond('502 ' + command + ' not implemented.'); - } else if (DOES_NOT_REQUIRE_AUTH[command]) { - this[m](commandArg, command); - } else { - // If 'tlsOnly' option is set, all commands which require user authentication will only - // be permitted over a secure connection. See RFC4217 regarding error code. - if (!this.secure && this.server.options.tlsOnly) { - this.respond('522 Protection level not sufficient; send AUTH TLS'); - } else if (this._authenticated()) { - checkData(); + var methodName = '__' + command; + if ( + COMMANDS_SUPPORTED[command] !== true || + (this.allowedCommands != null && this.allowedCommands[command] !== true) + ) { + this.respond('502 Command not implemented.'); + } else if (COMMANDS_NO_AUTH[command] === true) { + this[methodName](commandArg, command); + } else { + // If 'tlsOnly' option is set, all commands which require user authentication will only + // be permitted over a secure connection. See RFC4217 regarding error code. + if (!this.secure && this.server.options.tlsOnly) { + this.respond('522 Protection level not sufficient; send AUTH TLS'); + } else if (this._authenticated()) { + if (COMMANDS_REQUIRE_DATA_SOCKET[command] === true && !this.dataConfigured) { + this.respond('425 Data connection not configured; send PASV or PORT'); } else { - this.respond('530 Not logged in.'); + this[methodName](commandArg, command); } + } else { + this.respond('530 Not logged in.'); } - - } else { - this.respond('502 Command not implemented.'); } this.previousCommand = command; } // Specify the user's account (superfluous) - _command_ACCT() { + __ACCT() { this.respond('202 Command not implemented, superfluous at this site.'); return this; } // Allocate storage space (superfluous) - _command_ALLO() { + __ALLO() { this.respond('202 Command not implemented, superfluous at this site.'); return this; } - _command_AUTH(commandArg) { + __AUTH(commandArg) { if (!this.server.options.tlsOptions || commandArg !== 'TLS') { return this.respond('502 Command not implemented'); } @@ -329,7 +322,7 @@ class FtpConnection extends EventEmitter { } // Change working directory to parent directory - _command_CDUP() { + __CDUP() { var pathServer = pathModule.dirname(this.cwd); var pathEscaped = pathEscape(pathServer); this.cwd = pathServer; @@ -338,7 +331,7 @@ class FtpConnection extends EventEmitter { } // Change working directory - _command_CWD(pathRequest) { + __CWD(pathRequest) { var pathServer = withCwd(this.cwd, pathRequest); var pathFs = pathModule.join(this.root, pathServer); var pathEscaped = pathEscape(pathServer); @@ -357,7 +350,7 @@ class FtpConnection extends EventEmitter { return this; } - _command_DELE(commandArg) { + __DELE(commandArg) { var filename = withCwd(this.cwd, commandArg); this.fs.unlink(pathModule.join(this.root, filename), (err) => { if (err) { @@ -370,7 +363,7 @@ class FtpConnection extends EventEmitter { }); } - _command_FEAT() { + __FEAT() { // Get the feature list implemented by the server. (RFC 2389) this.respond( '211-Features\r\n' + @@ -387,7 +380,7 @@ class FtpConnection extends EventEmitter { ); } - _command_OPTS(commandArg) { + __OPTS(commandArg) { // http://tools.ietf.org/html/rfc2389#section-4 if (commandArg.toUpperCase() === 'UTF8 ON') { this.respond('200 OK'); @@ -397,7 +390,7 @@ class FtpConnection extends EventEmitter { } // Print the file modification time - _command_MDTM(file) { + __MDTM(file) { file = withCwd(this.cwd, file); file = pathModule.join(this.root, file); this.fs.stat(file, (err, stats) => { @@ -410,15 +403,15 @@ class FtpConnection extends EventEmitter { return this; } - _command_LIST(commandArg) { + __LIST(commandArg) { this._LIST(commandArg, true/*detailed*/, 'LIST'); } - _command_NLST(commandArg) { + __NLST(commandArg) { this._LIST(commandArg, false/*!detailed*/, 'NLST'); } - _command_STAT(commandArg) { + __STAT(commandArg) { if (commandArg) { this._LIST(commandArg, true/*detailed*/, 'STAT'); } else { @@ -612,7 +605,7 @@ class FtpConnection extends EventEmitter { } // Create a directory - _command_MKD(pathRequest) { + __MKD(pathRequest) { var pathServer = withCwd(this.cwd, pathRequest); var pathEscaped = pathEscape(pathServer); var pathFs = pathModule.join(this.root, pathServer); @@ -628,16 +621,16 @@ class FtpConnection extends EventEmitter { } // Perform a no-op (used to keep-alive connection) - _command_NOOP() { + __NOOP() { this.respond('200 OK'); return this; } - _command_PORT(x, y) { + __PORT(x, y) { this._PORT(x, y); } - _command_EPRT(x, y) { + __EPRT(x, y) { this._PORT(x, y); } @@ -694,11 +687,11 @@ class FtpConnection extends EventEmitter { this.respond('200 OK'); } - _command_PASV(x, y) { + __PASV(x, y) { this._PASV(x, y); } - _command_EPSV(x, y) { + __EPSV(x, y) { this._PASV(x, y); } @@ -796,7 +789,7 @@ class FtpConnection extends EventEmitter { }); } - _command_PBSZ(commandArg) { + __PBSZ(commandArg) { if (!this.server.options.tlsOptions) { return this.respond('202 Not supported'); } @@ -817,7 +810,7 @@ class FtpConnection extends EventEmitter { } } - _command_PROT(commandArg) { + __PROT(commandArg) { if (!this.server.options.tlsOptions) { return this.respond('202 Not supported'); } @@ -835,7 +828,7 @@ class FtpConnection extends EventEmitter { } // Print the current working directory. - _command_PWD(commandArg) { + __PWD(commandArg) { var pathEscaped = pathEscape(this.cwd); if (commandArg === '') { this.respond('257 "' + pathEscaped + '" is current directory'); @@ -845,7 +838,7 @@ class FtpConnection extends EventEmitter { return this; } - _command_QUIT() { + __QUIT() { this.hasQuit = true; this.respond('221 Goodbye', (err) => { if (err) { @@ -856,7 +849,7 @@ class FtpConnection extends EventEmitter { }); } - _command_RETR(commandArg) { + __RETR(commandArg) { var filename = pathModule.join(this.root, withCwd(this.cwd, commandArg)); if (this.server.options.useReadFile) { @@ -1000,7 +993,7 @@ class FtpConnection extends EventEmitter { } // Remove a directory - _command_RMD(pathRequest) { + __RMD(pathRequest) { var pathServer = withCwd(this.cwd, pathRequest); var pathFs = pathModule.join(this.root, pathServer); this.fs.rmdir(pathFs, (err) => { @@ -1014,13 +1007,13 @@ class FtpConnection extends EventEmitter { return this; } - _command_RNFR(commandArg) { + __RNFR(commandArg) { this.filefrom = withCwd(this.cwd, commandArg); this._logIf(LOG.DEBUG, 'Rename from ' + this.filefrom); this.respond('350 Ready for destination name'); } - _command_RNTO(commandArg) { + __RNTO(commandArg) { var fileto = withCwd(this.cwd, commandArg); this.fs.rename(pathModule.join(this.root, this.filefrom), pathModule.join(this.root, fileto), (err) => { if (err) { @@ -1032,7 +1025,7 @@ class FtpConnection extends EventEmitter { }); } - _command_SIZE(commandArg) { + __SIZE(commandArg) { var filename = withCwd(this.cwd, commandArg); this.fs.stat(pathModule.join(this.root, filename), (err, s) => { if (err) { @@ -1044,7 +1037,7 @@ class FtpConnection extends EventEmitter { }); } - _command_TYPE(commandArg) { + __TYPE(commandArg) { if (commandArg === 'I' || commandArg === 'A') { this.respond('200 OK'); } else { @@ -1052,11 +1045,11 @@ class FtpConnection extends EventEmitter { } } - _command_SYST() { + __SYST() { this.respond('215 UNIX Type: I'); } - _command_STOR(commandArg) { + __STOR(commandArg) { var filename = withCwd(this.cwd, commandArg); if (this.server.options.useWriteFile) { @@ -1262,7 +1255,7 @@ class FtpConnection extends EventEmitter { }); } - _command_APPE(commandArg) { + __APPE(commandArg) { var filename = withCwd(this.cwd, commandArg); if (this.server.options.useWriteFile) { this._STOR_usingWriteFile(filename, 'a'); @@ -1272,7 +1265,7 @@ class FtpConnection extends EventEmitter { } // Specify a username for login - _command_USER(username) { + __USER(username) { if (this.server.options.tlsOnly && !this.secure) { this.respond( '530 This server does not permit login over ' + @@ -1296,7 +1289,7 @@ class FtpConnection extends EventEmitter { } // Specify a password for login - _command_PASS(password) { + __PASS(password) { if (this.previousCommand !== 'USER') { this.respond('503 Bad sequence of commands.'); } else { From 69eb8b04e62789167361f72d00f3cf6e8d8116b5 Mon Sep 17 00:00:00 2001 From: Simon Sturmer Date: Sun, 17 Jan 2016 18:32:51 +0700 Subject: [PATCH 28/55] FtpConnection: rearrange methods so command methods are below private methods --- src/FtpConnection.js | 672 +++++++++++++++++++++---------------------- 1 file changed, 336 insertions(+), 336 deletions(-) diff --git a/src/FtpConnection.js b/src/FtpConnection.js index 27bbdfa..49a483e 100644 --- a/src/FtpConnection.js +++ b/src/FtpConnection.js @@ -274,151 +274,6 @@ class FtpConnection extends EventEmitter { this.previousCommand = command; } - // Specify the user's account (superfluous) - __ACCT() { - this.respond('202 Command not implemented, superfluous at this site.'); - return this; - } - - // Allocate storage space (superfluous) - __ALLO() { - this.respond('202 Command not implemented, superfluous at this site.'); - return this; - } - - __AUTH(commandArg) { - if (!this.server.options.tlsOptions || commandArg !== 'TLS') { - return this.respond('502 Command not implemented'); - } - - this.respond('234 Honored', () => { - this._logIf(LOG.INFO, 'Establishing secure connection...'); - starttls.starttlsServer(this.socket, this.server.options.tlsOptions, (err, cleartext) => { - const switchToSecure = () => { - this._logIf(LOG.INFO, 'Secure connection started'); - this.socket = cleartext; - this.socket.on('data', (data) => { - this._onData(data); - }); - this.secure = true; - }; - if (err) { - this._logIf(LOG.ERROR, 'Error upgrading connection to TLS: ' + util.inspect(err)); - this._closeSocket(this.socket, true); - } else if (!cleartext.authorized) { - this._logIf(LOG.INFO, 'Secure socket not authorized: ' + util.inspect(cleartext.authorizationError)); - if (this.server.options.allowUnauthorizedTls) { - this._logIf(LOG.INFO, 'Allowing unauthorized connection (allowUnauthorizedTls is on)'); - switchToSecure(); - } else { - this._logIf(LOG.INFO, 'Closing unauthorized connection (allowUnauthorizedTls is off)'); - this._closeSocket(this.socket, true); - } - } else { - switchToSecure(); - } - }); - }); - } - - // Change working directory to parent directory - __CDUP() { - var pathServer = pathModule.dirname(this.cwd); - var pathEscaped = pathEscape(pathServer); - this.cwd = pathServer; - this.respond('250 Directory changed to "' + pathEscaped + '"'); - return this; - } - - // Change working directory - __CWD(pathRequest) { - var pathServer = withCwd(this.cwd, pathRequest); - var pathFs = pathModule.join(this.root, pathServer); - var pathEscaped = pathEscape(pathServer); - this.fs.stat(pathFs, (err, stats) => { - if (err) { - this._logIf(LOG.ERROR, 'CWD ' + pathRequest + ': ' + err); - this.respond('550 Directory not found.'); - } else if (!stats.isDirectory()) { - this._logIf(LOG.WARN, 'Attempt to CWD to non-directory'); - this.respond('550 Not a directory'); - } else { - this.cwd = pathServer; - this.respond('250 CWD successful. "' + pathEscaped + '" is current directory'); - } - }); - return this; - } - - __DELE(commandArg) { - var filename = withCwd(this.cwd, commandArg); - this.fs.unlink(pathModule.join(this.root, filename), (err) => { - if (err) { - this._logIf(LOG.ERROR, 'Error deleting file: ' + filename + ', ' + err); - // write error to socket - this.respond('550 Permission denied'); - } else { - this.respond('250 File deleted'); - } - }); - } - - __FEAT() { - // Get the feature list implemented by the server. (RFC 2389) - this.respond( - '211-Features\r\n' + - ' SIZE\r\n' + - ' UTF8\r\n' + - ' MDTM\r\n' + - (!this.server.options.tlsOptions ? '' : - ' AUTH TLS\r\n' + - ' PBSZ\r\n' + - ' UTF8\r\n' + - ' PROT\r\n' - ) + - '211 end' - ); - } - - __OPTS(commandArg) { - // http://tools.ietf.org/html/rfc2389#section-4 - if (commandArg.toUpperCase() === 'UTF8 ON') { - this.respond('200 OK'); - } else { - this.respond('451 Not supported'); - } - } - - // Print the file modification time - __MDTM(file) { - file = withCwd(this.cwd, file); - file = pathModule.join(this.root, file); - this.fs.stat(file, (err, stats) => { - if (err) { - this.respond('550 File unavailable'); - } else { - this.respond('213 ' + dateformat(stats.mtime, 'yyyymmddhhMMss')); - } - }); - return this; - } - - __LIST(commandArg) { - this._LIST(commandArg, true/*detailed*/, 'LIST'); - } - - __NLST(commandArg) { - this._LIST(commandArg, false/*!detailed*/, 'NLST'); - } - - __STAT(commandArg) { - if (commandArg) { - this._LIST(commandArg, true/*detailed*/, 'STAT'); - } else { - this.respond('211 FTP Server Status OK'); - } - } - _LIST(commandArg, detailed, cmd) { /* Normally the server responds with a mark using code 150. It then stops accepting new connections, attempts to send the contents of the directory over the data connection, and closes the data connection. Finally it @@ -604,36 +459,6 @@ class FtpConnection extends EventEmitter { }); } - // Create a directory - __MKD(pathRequest) { - var pathServer = withCwd(this.cwd, pathRequest); - var pathEscaped = pathEscape(pathServer); - var pathFs = pathModule.join(this.root, pathServer); - this.fs.mkdir(pathFs, 0o755, (err) => { - if (err) { - this._logIf(LOG.ERROR, 'MKD ' + pathRequest + ': ' + err); - this.respond('550 "' + pathEscaped + '" directory NOT created'); - } else { - this.respond('257 "' + pathEscaped + '" directory created'); - } - }); - return this; - } - - // Perform a no-op (used to keep-alive connection) - __NOOP() { - this.respond('200 OK'); - return this; - } - - __PORT(x, y) { - this._PORT(x, y); - } - - __EPRT(x, y) { - this._PORT(x, y); - } - _PORT(commandArg, command) { var m; @@ -687,14 +512,6 @@ class FtpConnection extends EventEmitter { this.respond('200 OK'); } - __PASV(x, y) { - this._PASV(x, y); - } - - __EPSV(x, y) { - this._PASV(x, y); - } - _PASV(commandArg, command) { this.dataConfigured = false; @@ -789,76 +606,6 @@ class FtpConnection extends EventEmitter { }); } - __PBSZ(commandArg) { - if (!this.server.options.tlsOptions) { - return this.respond('202 Not supported'); - } - - // Protection Buffer Size (RFC 2228) - if (!this.secure) { - this.respond('503 Secure connection not established'); - } else if (parseInt(commandArg, 10) !== 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... - this.pbszReceived = true; - this.respond('200 buffer too big, PBSZ=0'); - } else { - this.pbszReceived = true; - this.respond('200 OK'); - } - } - - __PROT(commandArg) { - if (!this.server.options.tlsOptions) { - return this.respond('202 Not supported'); - } - - if (!this.pbszReceived) { - this.respond('503 No PBSZ command received'); - } else if (commandArg === 'S' || commandArg === 'E' || commandArg === 'C') { - this.respond('536 Not supported'); - } else if (commandArg === 'P') { - this.respond('200 OK'); - } else { - // Don't even recognize this one... - this.respond('504 Not recognized'); - } - } - - // Print the current working directory. - __PWD(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; - } - - __QUIT() { - this.hasQuit = true; - this.respond('221 Goodbye', (err) => { - if (err) { - this._logIf(LOG.ERROR, "Error writing 'Goodbye' message following QUIT"); - } - this._closeSocket(this.socket, true); - this._closeDataConnections(); - }); - } - - __RETR(commandArg) { - var filename = pathModule.join(this.root, withCwd(this.cwd, commandArg)); - - if (this.server.options.useReadFile) { - this._RETR_usingReadFile(commandArg, filename); - } else { - this._RETR_usingCreateReadStream(commandArg, filename); - } - } - _RETR_usingCreateReadStream(commandArg, filename) { var startTime = new Date(); @@ -992,73 +739,6 @@ class FtpConnection extends EventEmitter { }); } - // Remove a directory - __RMD(pathRequest) { - var pathServer = withCwd(this.cwd, pathRequest); - var pathFs = pathModule.join(this.root, pathServer); - this.fs.rmdir(pathFs, (err) => { - if (err) { - this._logIf(LOG.ERROR, 'RMD ' + pathRequest + ': ' + err); - this.respond('550 Delete operation failed'); - } else { - this.respond('250 "' + pathServer + '" directory removed'); - } - }); - return this; - } - - __RNFR(commandArg) { - this.filefrom = withCwd(this.cwd, commandArg); - this._logIf(LOG.DEBUG, 'Rename from ' + this.filefrom); - this.respond('350 Ready for destination name'); - } - - __RNTO(commandArg) { - var fileto = withCwd(this.cwd, commandArg); - this.fs.rename(pathModule.join(this.root, this.filefrom), pathModule.join(this.root, fileto), (err) => { - if (err) { - this._logIf(LOG.ERROR, 'Error renaming file from ' + this.filefrom + ' to ' + fileto); - this.respond('550 Rename failed' + (err.code === 'ENOENT' ? '; file does not exist' : '')); - } else { - this.respond('250 File renamed successfully'); - } - }); - } - - __SIZE(commandArg) { - var filename = withCwd(this.cwd, commandArg); - this.fs.stat(pathModule.join(this.root, filename), (err, s) => { - if (err) { - this._logIf(LOG.ERROR, "Error getting size of file '" + filename + "' "); - this.respond('450 Failed to get size of file'); - return; - } - this.respond('213 ' + s.size + ''); - }); - } - - __TYPE(commandArg) { - if (commandArg === 'I' || commandArg === 'A') { - this.respond('200 OK'); - } else { - this.respond('202 Not supported'); - } - } - - __SYST() { - this.respond('215 UNIX Type: I'); - } - - __STOR(commandArg) { - var filename = withCwd(this.cwd, commandArg); - - if (this.server.options.useWriteFile) { - this._STOR_usingWriteFile(filename, 'w'); - } else { - this._STOR_usingCreateWriteStream(filename, null, 'w'); - } - } - // 'initialBuffers' argument is set when this is called from _STOR_usingWriteFile. _STOR_usingCreateWriteStream(filename, initialBuffers, flag) { var wStreamFlags = {flags: flag || 'w', mode: 0o644}; @@ -1255,6 +935,342 @@ class FtpConnection extends EventEmitter { }); } + _closeSocket(socket, shouldDestroy) { + // TODO: Should we always use destroy() to avoid keeping sockets open longer + // than necessary (and possibly exceeding OS max open sockets)? + if (shouldDestroy || this.server.options.destroySockets) { + // Don't call destroy() more than once. + if (!socket.destroyed) { + socket.destroy(); + } + } else { + // Don't call `end()` more than once. + if (socket.writable) { + socket.end(); + } + } + } + + // Specify the user's account (superfluous) + __ACCT() { + this.respond('202 Command not implemented, superfluous at this site.'); + return this; + } + + // Allocate storage space (superfluous) + __ALLO() { + this.respond('202 Command not implemented, superfluous at this site.'); + return this; + } + + __AUTH(commandArg) { + if (!this.server.options.tlsOptions || commandArg !== 'TLS') { + return this.respond('502 Command not implemented'); + } + + this.respond('234 Honored', () => { + this._logIf(LOG.INFO, 'Establishing secure connection...'); + starttls.starttlsServer(this.socket, this.server.options.tlsOptions, (err, cleartext) => { + const switchToSecure = () => { + this._logIf(LOG.INFO, 'Secure connection started'); + this.socket = cleartext; + this.socket.on('data', (data) => { + this._onData(data); + }); + this.secure = true; + }; + if (err) { + this._logIf(LOG.ERROR, 'Error upgrading connection to TLS: ' + util.inspect(err)); + this._closeSocket(this.socket, true); + } else if (!cleartext.authorized) { + this._logIf(LOG.INFO, 'Secure socket not authorized: ' + util.inspect(cleartext.authorizationError)); + if (this.server.options.allowUnauthorizedTls) { + this._logIf(LOG.INFO, 'Allowing unauthorized connection (allowUnauthorizedTls is on)'); + switchToSecure(); + } else { + this._logIf(LOG.INFO, 'Closing unauthorized connection (allowUnauthorizedTls is off)'); + this._closeSocket(this.socket, true); + } + } else { + switchToSecure(); + } + }); + }); + } + + // Change working directory to parent directory + __CDUP() { + var pathServer = pathModule.dirname(this.cwd); + var pathEscaped = pathEscape(pathServer); + this.cwd = pathServer; + this.respond('250 Directory changed to "' + pathEscaped + '"'); + return this; + } + + // Change working directory + __CWD(pathRequest) { + var pathServer = withCwd(this.cwd, pathRequest); + var pathFs = pathModule.join(this.root, pathServer); + var pathEscaped = pathEscape(pathServer); + this.fs.stat(pathFs, (err, stats) => { + if (err) { + this._logIf(LOG.ERROR, 'CWD ' + pathRequest + ': ' + err); + this.respond('550 Directory not found.'); + } else if (!stats.isDirectory()) { + this._logIf(LOG.WARN, 'Attempt to CWD to non-directory'); + this.respond('550 Not a directory'); + } else { + this.cwd = pathServer; + this.respond('250 CWD successful. "' + pathEscaped + '" is current directory'); + } + }); + return this; + } + + __DELE(commandArg) { + var filename = withCwd(this.cwd, commandArg); + this.fs.unlink(pathModule.join(this.root, filename), (err) => { + if (err) { + this._logIf(LOG.ERROR, 'Error deleting file: ' + filename + ', ' + err); + // write error to socket + this.respond('550 Permission denied'); + } else { + this.respond('250 File deleted'); + } + }); + } + + __FEAT() { + // Get the feature list implemented by the server. (RFC 2389) + this.respond( + '211-Features\r\n' + + ' SIZE\r\n' + + ' UTF8\r\n' + + ' MDTM\r\n' + + (!this.server.options.tlsOptions ? '' : + ' AUTH TLS\r\n' + + ' PBSZ\r\n' + + ' UTF8\r\n' + + ' PROT\r\n' + ) + + '211 end' + ); + } + + __OPTS(commandArg) { + // http://tools.ietf.org/html/rfc2389#section-4 + if (commandArg.toUpperCase() === 'UTF8 ON') { + this.respond('200 OK'); + } else { + this.respond('451 Not supported'); + } + } + + // Print the file modification time + __MDTM(file) { + file = withCwd(this.cwd, file); + file = pathModule.join(this.root, file); + this.fs.stat(file, (err, stats) => { + if (err) { + this.respond('550 File unavailable'); + } else { + this.respond('213 ' + dateformat(stats.mtime, 'yyyymmddhhMMss')); + } + }); + return this; + } + + __LIST(commandArg) { + this._LIST(commandArg, true/*detailed*/, 'LIST'); + } + + __NLST(commandArg) { + this._LIST(commandArg, false/*!detailed*/, 'NLST'); + } + + __STAT(commandArg) { + if (commandArg) { + this._LIST(commandArg, true/*detailed*/, 'STAT'); + } else { + this.respond('211 FTP Server Status OK'); + } + } + + // Create a directory + __MKD(pathRequest) { + var pathServer = withCwd(this.cwd, pathRequest); + var pathEscaped = pathEscape(pathServer); + var pathFs = pathModule.join(this.root, pathServer); + this.fs.mkdir(pathFs, 0o755, (err) => { + if (err) { + this._logIf(LOG.ERROR, 'MKD ' + pathRequest + ': ' + err); + this.respond('550 "' + pathEscaped + '" directory NOT created'); + } else { + this.respond('257 "' + pathEscaped + '" directory created'); + } + }); + return this; + } + + // Perform a no-op (used to keep-alive connection) + __NOOP() { + this.respond('200 OK'); + return this; + } + + __PORT(x, y) { + this._PORT(x, y); + } + + __EPRT(x, y) { + this._PORT(x, y); + } + + __PASV(x, y) { + this._PASV(x, y); + } + + __EPSV(x, y) { + this._PASV(x, y); + } + + __PBSZ(commandArg) { + if (!this.server.options.tlsOptions) { + return this.respond('202 Not supported'); + } + + // Protection Buffer Size (RFC 2228) + if (!this.secure) { + this.respond('503 Secure connection not established'); + } else if (parseInt(commandArg, 10) !== 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... + this.pbszReceived = true; + this.respond('200 buffer too big, PBSZ=0'); + } else { + this.pbszReceived = true; + this.respond('200 OK'); + } + } + + __PROT(commandArg) { + if (!this.server.options.tlsOptions) { + return this.respond('202 Not supported'); + } + + if (!this.pbszReceived) { + this.respond('503 No PBSZ command received'); + } else if (commandArg === 'S' || commandArg === 'E' || commandArg === 'C') { + this.respond('536 Not supported'); + } else if (commandArg === 'P') { + this.respond('200 OK'); + } else { + // Don't even recognize this one... + this.respond('504 Not recognized'); + } + } + + // Print the current working directory. + __PWD(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; + } + + __QUIT() { + this.hasQuit = true; + this.respond('221 Goodbye', (err) => { + if (err) { + this._logIf(LOG.ERROR, "Error writing 'Goodbye' message following QUIT"); + } + this._closeSocket(this.socket, true); + this._closeDataConnections(); + }); + } + + __RETR(commandArg) { + var filename = pathModule.join(this.root, withCwd(this.cwd, commandArg)); + + if (this.server.options.useReadFile) { + this._RETR_usingReadFile(commandArg, filename); + } else { + this._RETR_usingCreateReadStream(commandArg, filename); + } + } + + // Remove a directory + __RMD(pathRequest) { + var pathServer = withCwd(this.cwd, pathRequest); + var pathFs = pathModule.join(this.root, pathServer); + this.fs.rmdir(pathFs, (err) => { + if (err) { + this._logIf(LOG.ERROR, 'RMD ' + pathRequest + ': ' + err); + this.respond('550 Delete operation failed'); + } else { + this.respond('250 "' + pathServer + '" directory removed'); + } + }); + return this; + } + + __RNFR(commandArg) { + this.filefrom = withCwd(this.cwd, commandArg); + this._logIf(LOG.DEBUG, 'Rename from ' + this.filefrom); + this.respond('350 Ready for destination name'); + } + + __RNTO(commandArg) { + var fileto = withCwd(this.cwd, commandArg); + this.fs.rename(pathModule.join(this.root, this.filefrom), pathModule.join(this.root, fileto), (err) => { + if (err) { + this._logIf(LOG.ERROR, 'Error renaming file from ' + this.filefrom + ' to ' + fileto); + this.respond('550 Rename failed' + (err.code === 'ENOENT' ? '; file does not exist' : '')); + } else { + this.respond('250 File renamed successfully'); + } + }); + } + + __SIZE(commandArg) { + var filename = withCwd(this.cwd, commandArg); + this.fs.stat(pathModule.join(this.root, filename), (err, s) => { + if (err) { + this._logIf(LOG.ERROR, "Error getting size of file '" + filename + "' "); + this.respond('450 Failed to get size of file'); + return; + } + this.respond('213 ' + s.size + ''); + }); + } + + __TYPE(commandArg) { + if (commandArg === 'I' || commandArg === 'A') { + this.respond('200 OK'); + } else { + this.respond('202 Not supported'); + } + } + + __SYST() { + this.respond('215 UNIX Type: I'); + } + + __STOR(commandArg) { + var filename = withCwd(this.cwd, commandArg); + + if (this.server.options.useWriteFile) { + this._STOR_usingWriteFile(filename, 'w'); + } else { + this._STOR_usingCreateWriteStream(filename, null, 'w'); + } + } + __APPE(commandArg) { var filename = withCwd(this.cwd, commandArg); if (this.server.options.useWriteFile) { @@ -1346,22 +1362,6 @@ class FtpConnection extends EventEmitter { } return this; } - - _closeSocket(socket, shouldDestroy) { - // TODO: Should we always use destroy() to avoid keeping sockets open longer - // than necessary (and possibly exceeding OS max open sockets)? - if (shouldDestroy || this.server.options.destroySockets) { - // Don't call destroy() more than once. - if (!socket.destroyed) { - socket.destroy(); - } - } else { - // Don't call `end()` more than once. - if (socket.writable) { - socket.end(); - } - } - } } export default FtpConnection; From a8814e55fee94912ebf540721009c2b9518444d8 Mon Sep 17 00:00:00 2001 From: Simon Sturmer Date: Sun, 17 Jan 2016 18:36:02 +0700 Subject: [PATCH 29/55] FtpConnection: rename some methods to use camel case --- src/FtpConnection.js | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/src/FtpConnection.js b/src/FtpConnection.js index 49a483e..5c70f36 100644 --- a/src/FtpConnection.js +++ b/src/FtpConnection.js @@ -606,7 +606,7 @@ class FtpConnection extends EventEmitter { }); } - _RETR_usingCreateReadStream(commandArg, filename) { + _retrieveUsingCreateReadStream(commandArg, filename) { var startTime = new Date(); this.emit('file:retr', 'open', { @@ -683,7 +683,7 @@ class FtpConnection extends EventEmitter { }); } - _RETR_usingReadFile(commandArg, filename) { + _retrieveUsingReadFile(commandArg, filename) { var startTime = new Date(); this.emit('file:retr', 'open', { @@ -739,8 +739,8 @@ class FtpConnection extends EventEmitter { }); } - // 'initialBuffers' argument is set when this is called from _STOR_usingWriteFile. - _STOR_usingCreateWriteStream(filename, initialBuffers, flag) { + // 'initialBuffers' argument is set when this is called from _storeUsingWriteFile. + _storeUsingCreateWriteStream(filename, initialBuffers, flag) { var wStreamFlags = {flags: flag || 'w', mode: 0o644}; var storeStream = this.fs.createWriteStream(pathModule.join(this.root, filename), wStreamFlags); var notErr = true; @@ -835,7 +835,7 @@ class FtpConnection extends EventEmitter { }); } - _STOR_usingWriteFile(filename, flag) { + _storeUsingWriteFile(filename, flag) { var erroredOut = false; var slurpBuf = new Buffer(1024); var totalBytes = 0; @@ -865,7 +865,7 @@ class FtpConnection extends EventEmitter { // Otherwise, we call _STOR_usingWriteStream, and tell it to prepend the stuff // that we've buffered so far to the file. this._logIf(LOG.WARN, 'uploadMaxSlurpSize exceeded; falling back to createWriteStream'); - this._STOR_usingCreateWriteStream(filename, [slurpBuf.slice(0, totalBytes), buf]); + this._storeUsingCreateWriteStream(filename, [slurpBuf.slice(0, totalBytes), buf]); this.dataSocket.removeListener('data', dataHandler); this.dataSocket.removeListener('error', errorHandler); this.dataSocket.removeListener('close', closeHandler); @@ -1198,9 +1198,9 @@ class FtpConnection extends EventEmitter { var filename = pathModule.join(this.root, withCwd(this.cwd, commandArg)); if (this.server.options.useReadFile) { - this._RETR_usingReadFile(commandArg, filename); + this._retrieveUsingReadFile(commandArg, filename); } else { - this._RETR_usingCreateReadStream(commandArg, filename); + this._retrieveUsingCreateReadStream(commandArg, filename); } } @@ -1265,18 +1265,18 @@ class FtpConnection extends EventEmitter { var filename = withCwd(this.cwd, commandArg); if (this.server.options.useWriteFile) { - this._STOR_usingWriteFile(filename, 'w'); + this._storeUsingWriteFile(filename, 'w'); } else { - this._STOR_usingCreateWriteStream(filename, null, 'w'); + this._storeUsingCreateWriteStream(filename, null, 'w'); } } __APPE(commandArg) { var filename = withCwd(this.cwd, commandArg); if (this.server.options.useWriteFile) { - this._STOR_usingWriteFile(filename, 'a'); + this._storeUsingWriteFile(filename, 'a'); } else { - this._STOR_usingCreateWriteStream(filename, null, 'a'); + this._storeUsingCreateWriteStream(filename, null, 'a'); } } From 18274ebe06df36752f383d185acec7c72d9e0682 Mon Sep 17 00:00:00 2001 From: Simon Sturmer Date: Sun, 17 Jan 2016 18:40:55 +0700 Subject: [PATCH 30/55] FtpConnection: don't need _PASV and __PASV --- src/FtpConnection.js | 51 ++++++++++++++++++++------------------------ 1 file changed, 23 insertions(+), 28 deletions(-) diff --git a/src/FtpConnection.js b/src/FtpConnection.js index 5c70f36..e6f3af2 100644 --- a/src/FtpConnection.js +++ b/src/FtpConnection.js @@ -512,30 +512,6 @@ class FtpConnection extends EventEmitter { this.respond('200 OK'); } - _PASV(commandArg, command) { - this.dataConfigured = false; - - if (command === 'EPSV' && commandArg && commandArg !== '1') { - this.respond('202 Not supported'); - return; - } - - // not sure whether the spec limits to 1 data connection at a time ... - if (this.dataSocket) { - this._closeSocket(this.dataSocket, true); - } - - if (this.dataListener) { - this._logIf(LOG.DEBUG, 'Telling client that they can connect now'); - this._writePASVReady(command); - } else { - this._logIf(LOG.DEBUG, 'Setting up listener for passive connections'); - this._setupNewPASV(commandArg, command); - } - - this.dataConfigured = true; - } - _writePASVReady(command) { var a = this.pasv.address(); var host = this.server.host; @@ -1126,12 +1102,31 @@ class FtpConnection extends EventEmitter { this._PORT(x, y); } - __PASV(x, y) { - this._PASV(x, y); + __PASV(commandArg, command) { + this.dataConfigured = false; + + // not sure whether the spec limits to 1 data connection at a time ... + if (this.dataSocket) { + this._closeSocket(this.dataSocket, true); + } + + if (this.dataListener) { + this._logIf(LOG.DEBUG, 'Telling client that they can connect now'); + this._writePASVReady(command); + } else { + this._logIf(LOG.DEBUG, 'Setting up listener for passive connections'); + this._setupNewPASV(commandArg, command); + } + + this.dataConfigured = true; } - __EPSV(x, y) { - this._PASV(x, y); + __EPSV(commandArg, command) { + if (commandArg && commandArg !== '1') { + this.respond('202 Not supported'); + } else { + this.__PASV(commandArg, command); + } } __PBSZ(commandArg) { From 0daf81e3404821c3bf206ef59290bc3efe4ea5e5 Mon Sep 17 00:00:00 2001 From: Simon Sturmer Date: Sun, 17 Jan 2016 18:49:49 +0700 Subject: [PATCH 31/55] FtpConnection: don't need _PORT and __PORT --- src/FtpConnection.js | 103 +++++++++++++++++++++---------------------- 1 file changed, 51 insertions(+), 52 deletions(-) diff --git a/src/FtpConnection.js b/src/FtpConnection.js index e6f3af2..bbedba9 100644 --- a/src/FtpConnection.js +++ b/src/FtpConnection.js @@ -459,57 +459,48 @@ class FtpConnection extends EventEmitter { }); } - _PORT(commandArg, command) { - var m; - - this.dataConfigured = false; - - 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})$/); - if (!m) { - this.respond('501 Bad argument to PORT'); - return; - } - - var host = m[1] + '.' + m[2] + '.' + m[3] + '.' + m[4]; - var port = (parseInt(m[5], 10) << 8) + parseInt(m[6], 10); - if (isNaN(port)) { - // The value should never be NaN because the relevant groups in the regex matche 1-3 digits. - throw new Error('Impossible NaN in FtpConnection.prototype._PORT'); - } - } else { // EPRT - if (commandArg.length >= 3 && commandArg.charAt(0) === '|' && - commandArg.charAt(2) === '|' && commandArg.charAt(1) === '2') { - // Only IPv4 is supported. - this.respond('522 Server cannot handle IPv6 EPRT commands, use (1)'); - return; - } - - m = commandArg.match(/^\|1\|([0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3})\|([0-9]{1,5})/); - if (!m) { - this.respond('501 Bad Argument to EPRT'); - return; - } - - var r = parseInt(m[2], 10); - if (isNaN(r)) { - // The value should never be NaN because the relevant group in the regex matches 1-5 digits. - throw new Error('Impossible NaN in FtpConnection.prototype._PORT (2)'); - } - if (r > 65535 || r <= 0) { - this.respond('501 Bad argument to EPRT (invalid port number)'); - return; - } + _parsePORT(commandArg) { + var 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})$/); + if (!m) { + this.respond('501 Bad argument to PORT'); + return; + } - host = m[1]; - port = r; + var host = m[1] + '.' + m[2] + '.' + m[3] + '.' + m[4]; + var port = (parseInt(m[5], 10) << 8) + parseInt(m[6], 10); + if (isNaN(port)) { + // The value should never be NaN because the relevant groups in the regex matche 1-3 digits. + throw new Error('Impossible NaN in FtpConnection.prototype._PORT'); } + return {host, port}; + } - this.dataConfigured = true; - this.dataHost = host; - this.dataPort = port; - this._logIf(LOG.DEBUG, 'self.dataHost, self.dataPort set to ' + this.dataHost + ':' + this.dataPort); - this.respond('200 OK'); + _parseEPRT(commandArg) { + if ( + commandArg.length >= 3 && + commandArg.charAt(0) === '|' && + commandArg.charAt(2) === '|' && + commandArg.charAt(1) === '2' + ) { + // Only IPv4 is supported. + this.respond('522 Server cannot handle IPv6 EPRT commands, use (1)'); + return; + } + var m = commandArg.match(/^\|1\|([0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3})\|([0-9]{1,5})/); + if (!m) { + this.respond('501 Bad Argument to EPRT'); + return; + } + var r = parseInt(m[2], 10); + if (isNaN(r)) { + // The value should never be NaN because the relevant group in the regex matches 1-5 digits. + throw new Error('Impossible NaN in FtpConnection.prototype._PORT (2)'); + } + if (r > 65535 || r <= 0) { + this.respond('501 Bad argument to EPRT (invalid port number)'); + return; + } + return {host: m[1], port: r}; } _writePASVReady(command) { @@ -1094,12 +1085,20 @@ class FtpConnection extends EventEmitter { return this; } - __PORT(x, y) { - this._PORT(x, y); + __PORT(commandArg, command) { + this.dataConfigured = false; + var {host, port} = (command === 'PORT') ? + this._parsePORT(commandArg) : + this._parsePORT(commandArg); + this.dataConfigured = true; + this.dataHost = host; + this.dataPort = port; + this._logIf(LOG.DEBUG, 'self.dataHost, self.dataPort set to ' + this.dataHost + ':' + this.dataPort); + this.respond('200 OK'); } - __EPRT(x, y) { - this._PORT(x, y); + __EPRT(commandArg, command) { + this.__PORT(commandArg, command); } __PASV(commandArg, command) { From b3ed02f5c2e7b3c343feda1bea63165ddc4a93b8 Mon Sep 17 00:00:00 2001 From: Simon Sturmer Date: Sun, 17 Jan 2016 19:11:10 +0700 Subject: [PATCH 32/55] FtpConnection: don't need _LIST and __LIST --- src/FtpConnection.js | 263 ++++++++++++++++++++++--------------------- 1 file changed, 137 insertions(+), 126 deletions(-) diff --git a/src/FtpConnection.js b/src/FtpConnection.js index bbedba9..b259797 100644 --- a/src/FtpConnection.js +++ b/src/FtpConnection.js @@ -274,129 +274,15 @@ class FtpConnection extends EventEmitter { this.previousCommand = command; } - _LIST(commandArg, detailed, cmd) { - /* - Normally the server responds with a mark using code 150. It then stops accepting new connections, attempts to send the contents of the directory over the data connection, and closes the data connection. Finally it - - accepts the LIST or NLST request with code 226 if the entire directory was successfully transmitted; - rejects the LIST or NLST request with code 425 if no TCP connection was established; - rejects the LIST or NLST request with code 426 if the TCP connection was established but then broken by the client or by network failure; or - rejects the LIST or NLST request with code 451 if the server had trouble reading the directory from disk. - - The server may reject the LIST or NLST request (with code 450 or 550) without first responding with a mark. In this case the server does not touch the data connection. - */ - - // LIST may be passed options (-a in particular). We just ignore any of these. - // (In the particular case of -a, we show hidden files anyway.) - var dirname = stripOptions(commandArg); - var dir = withCwd(this.cwd, dirname); - - // TODO: this is bad practice, use a class if options are required: new Glob({maxConcurrency: 5}).glob() - glob.setMaxStatsAtOnce(this.server.options.maxStatsAtOnce); - glob.glob(pathModule.join(this.root, dir), this.fs, (err, files) => { - if (err) { - this._logIf(LOG.ERROR, 'Error sending file list, reading directory: ' + err); - this.respond('550 Not a directory'); - return; - } - - const handleFile = (ii) => { - if (i >= files.length) { - return i === files.length + j ? finished() : null; - } - this.server.getUsernameFromUid(files[ii].stats.uid, (e1, uname) => { - this.server.getGroupFromGid(files[ii].stats.gid, (e2, gname) => { - if (e1 || e2) { - this._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, - }); - } - handleFile(++i); - }); - }); - }; - - const finished = () => { - // Sort file names. - if (!this.server.options.dontSortFilenames) { - if (this.server.options.filenameSortMap !== false) { - var sm = ( - this.server.options.filenameSortMap || - ((x) => x.toUpperCase()) - ); - for (var i = 0; i < fileInfos.length; ++i) { - fileInfos[i]._s = sm(detailed ? fileInfos[i].file.name : fileInfos[i].name); - } - } - - var sf = (this.server.options.filenameSortFunc || - ((x, y) => x.localeCompare(y)) - ); - fileInfos = fileInfos.sort((x, y) => { - if (this.server.options.filenameSortMap !== false) { - return sf(x._s, y._s); - } else if (detailed) { - return sf(x.file.name, y.file.name); - } else { - return sf(x.name, y.name); - } - }); - } - - this._listFiles(fileInfos, detailed, cmd); - }; - - if (this.server.options.hideDotFiles) { - files = files.filter((file) => ( - (file.name && file.name[0] !== '.') ? true : false - )); - } - - this._logIf(LOG.INFO, 'Directory has ' + files.length + ' files'); - if (files.length === 0) { - return this._listFiles([], detailed, cmd); - } - - var fileInfos; // To contain list of files with info for each. - - if (!detailed) { - // We're not doing a detailed listing, so we don't need to get username - // and group name. - fileInfos = files; - return finished(); - } - - // Now we need to get username and group name for each file from user/group ids. - fileInfos = []; - - var CONC = this.server.options.maxStatsAtOnce; - var j = 0; - for (var i = 0; i < files.length && i < CONC; ++i) { - handleFile(i); - } - j = --i; - - }, this.server.options.noWildcards); - } - - _listFiles(fileInfos, detailed, cmd) { + _listFiles(fileInfos, isDetailed, command) { const whenReady = (listconn) => { const success = (err) => { if (err) { this.respond('550 Error listing files'); } else { - this.respond(END_MSGS[cmd]); + this.respond(END_MSGS[command]); } - if (cmd !== 'STAT') { + if (command !== 'STAT') { this._closeSocket(listconn); } }; @@ -413,7 +299,7 @@ class FtpConnection extends EventEmitter { var line = ''; var file; - if (!detailed) { + if (!isDetailed) { file = fileInfo; line += file.name + '\r\n'; } else { @@ -449,8 +335,8 @@ class FtpConnection extends EventEmitter { LIST: m, NLST: m, STAT: '213 End of status', }; - this.respond(BEGIN_MSGS[cmd], () => { - if (cmd === 'STAT') { + this.respond(BEGIN_MSGS[command], () => { + if (command === 'STAT') { whenReady(this.socket); } else { this._whenDataReady(whenReady); @@ -1047,17 +933,142 @@ class FtpConnection extends EventEmitter { return this; } - __LIST(commandArg) { - this._LIST(commandArg, true/*detailed*/, 'LIST'); + __LIST(commandArg, command) { + let isDetailed = (command === 'LIST' || command === 'STAT'); + /* + Normally the server responds with a mark using code 150. It then stops + accepting new connections, attempts to send the contents of the directory + over the data connection, and closes the data connection. + Finally it: + - accepts the LIST or NLST request with code 226 if the entire directory + was successfully transmitted + - rejects the LIST or NLST request with code 425 if no TCP connection was + established + - rejects the LIST or NLST request with code 426 if the TCP connection was + established but then broken by the client or by network failure + - rejects the LIST or NLST request with code 451 if the server had trouble + reading the directory from disk + + The server may reject the LIST or NLST request (with code 450 or 550) + without first responding with a mark. In this case the server does not + touch the data connection. + */ + + // LIST may be passed options (-a in particular). We just ignore any of these. + // (In the particular case of -a, we show hidden files anyway.) + let dirname = stripOptions(commandArg); + let dir = withCwd(this.cwd, dirname); + + // TODO: this is bad practice, use a class if options are required: + // new Glob({maxConcurrency: 5}).glob() + glob.setMaxStatsAtOnce(this.server.options.maxStatsAtOnce); + glob.glob( + pathModule.join(this.root, dir), + this.fs, + (err, files) => { + if (err) { + this._logIf(LOG.ERROR, 'Error sending file list, reading directory: ' + err); + this.respond('550 Not a directory'); + return; + } + + const handleFile = (ii) => { + if (i >= files.length) { + return i === files.length + j ? finished() : null; + } + this.server.getUsernameFromUid(files[ii].stats.uid, (e1, uname) => { + this.server.getGroupFromGid(files[ii].stats.gid, (e2, gname) => { + if (e1 || e2) { + this._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, + }); + } + handleFile(++i); + }); + }); + }; + + const finished = () => { + // Sort file names. + if (!this.server.options.dontSortFilenames) { + if (this.server.options.filenameSortMap !== false) { + var sm = ( + this.server.options.filenameSortMap || + ((x) => x.toUpperCase()) + ); + for (var i = 0; i < fileInfos.length; ++i) { + fileInfos[i]._s = sm(isDetailed ? fileInfos[i].file.name : fileInfos[i].name); + } + } + + var sf = (this.server.options.filenameSortFunc || + ((x, y) => x.localeCompare(y)) + ); + fileInfos = fileInfos.sort((x, y) => { + if (this.server.options.filenameSortMap !== false) { + return sf(x._s, y._s); + } else if (isDetailed) { + return sf(x.file.name, y.file.name); + } else { + return sf(x.name, y.name); + } + }); + } + + this._listFiles(fileInfos, isDetailed, command); + }; + + if (this.server.options.hideDotFiles) { + files = files.filter((file) => ( + (file.name && file.name[0] !== '.') ? true : false + )); + } + + this._logIf(LOG.INFO, 'Directory has ' + files.length + ' files'); + if (files.length === 0) { + return this._listFiles([], isDetailed, command); + } + + var fileInfos; // To contain list of files with info for each. + + if (!isDetailed) { + // We're not doing a detailed listing, so we don't need to get username + // and group name. + fileInfos = files; + return finished(); + } + + // Now we need to get username and group name for each file from user/group ids. + fileInfos = []; + + var CONC = this.server.options.maxStatsAtOnce; + var j = 0; + for (var i = 0; i < files.length && i < CONC; ++i) { + handleFile(i); + } + j = --i; + + }, + this.server.options.noWildcards + ); } - __NLST(commandArg) { - this._LIST(commandArg, false/*!detailed*/, 'NLST'); + __NLST(commandArg, command) { + this.__LIST(commandArg, command); } - __STAT(commandArg) { + __STAT(commandArg, command) { if (commandArg) { - this._LIST(commandArg, true/*detailed*/, 'STAT'); + this.__LIST(commandArg, command); } else { this.respond('211 FTP Server Status OK'); } From 05b841d91ba78bd8a34da3108b4958556b08941b Mon Sep 17 00:00:00 2001 From: Simon Sturmer Date: Sun, 17 Jan 2016 19:23:15 +0700 Subject: [PATCH 33/55] refactor _logIf -> _log --- src/FtpConnection.js | 120 +++++++++++++++++++----------------- src/FtpServer.js | 9 +-- src/__tests__/lib/common.js | 9 ++- 3 files changed, 70 insertions(+), 68 deletions(-) diff --git a/src/FtpConnection.js b/src/FtpConnection.js index b259797..a4e85d7 100644 --- a/src/FtpConnection.js +++ b/src/FtpConnection.js @@ -37,8 +37,12 @@ class FtpConnection extends EventEmitter { return this._writeText(this.socket, message + '\r\n', callback); } - _logIf(verbosity, message) { - return this.server._logIf(verbosity, message, this); + _log(verbosity, message) { + var peerAddr = this.socket ? this.socket.remoteAddress : null; + return this.server._log( + verbosity, + peerAddr ? `<${peerAddr}> ${message}` : message + ); } // We don't want to use setEncoding because it screws up TLS, but we @@ -46,10 +50,10 @@ class FtpConnection extends EventEmitter { // with a string argument. _writeText(socket, data, callback) { if (!socket.writable) { - this._logIf(LOG.DEBUG, 'Attempted writing to a closed socket:\n>> ' + data.trim()); + this._log(LOG.DEBUG, 'Attempted writing to a closed socket:\n>> ' + data.trim()); return; } - this._logIf(LOG.TRACE, '>> ' + data.trim()); + this._log(LOG.TRACE, '>> ' + data.trim()); return socket.write(data, 'utf8', callback); } @@ -73,18 +77,18 @@ class FtpConnection extends EventEmitter { return net.createServer((psocket) => { // This is simply a connection listener. // TODO: Should we keep track of *all* connections, or enforce just one? - this._logIf(LOG.INFO, 'Passive data event: connect'); + this._log(LOG.INFO, 'Passive data event: connect'); const setupPassiveListener = () => { if (this.dataListener) { this.dataListener.emit('ready'); } else { - this._logIf(LOG.WARN, 'Passive connection initiated, but no data listener'); + this._log(LOG.WARN, 'Passive connection initiated, but no data listener'); } const allOver = (ename) => { return (err) => { - this._logIf( + this._log( (err ? LOG.ERROR : LOG.DEBUG), 'Passive data event: ' + ename + (err ? ' due to error' : '') ); @@ -99,7 +103,7 @@ class FtpConnection extends EventEmitter { this.dataSocket.on('end', allOver('end')); this.dataSocket.on('error', (err) => { - this._logIf(LOG.ERROR, 'Passive data event: error: ' + err); + this._log(LOG.ERROR, 'Passive data event: error: ' + err); // TODO: Can we can rely on self.dataSocket having been closed? this.dataSocket = null; this.dataConfigured = false; @@ -107,24 +111,24 @@ class FtpConnection extends EventEmitter { }; if (this.secure) { - this._logIf(LOG.INFO, 'Upgrading passive connection to TLS'); + this._log(LOG.INFO, 'Upgrading passive connection to TLS'); starttls.starttlsServer(psocket, this.server.options.tlsOptions, (err, cleartext) => { const switchToSecure = () => { - this._logIf(LOG.INFO, 'Secure passive connection started'); + this._log(LOG.INFO, 'Secure passive connection started'); // TODO: Check for existing dataSocket. this.dataSocket = cleartext; setupPassiveListener(); }; if (err) { - this._logIf(LOG.ERROR, 'Error upgrading passive connection to TLS:' + util.inspect(err)); + this._log(LOG.ERROR, 'Error upgrading passive connection to TLS:' + util.inspect(err)); this._closeSocket(psocket, true); this.dataConfigured = false; } else if (!cleartext.authorized) { if (this.server.options.allowUnauthorizedTls) { - this._logIf(LOG.INFO, 'Allowing unauthorized passive connection (allowUnauthorizedTls is on)'); + this._log(LOG.INFO, 'Allowing unauthorized passive connection (allowUnauthorizedTls is on)'); switchToSecure(); } else { - this._logIf(LOG.INFO, 'Closing unauthorized passive connection (allowUnauthorizedTls is off)'); + this._log(LOG.INFO, 'Closing unauthorized passive connection (allowUnauthorizedTls is off)'); this._closeSocket(this.socket, true); this.dataConfigured = false; } @@ -145,19 +149,19 @@ class FtpConnection extends EventEmitter { // how many data connections are allowed? // should still be listening since we created a server, right? if (this.dataSocket) { - this._logIf(LOG.DEBUG, 'A data connection exists'); + this._log(LOG.DEBUG, 'A data connection exists'); callback(this.dataSocket); } else { - this._logIf(LOG.DEBUG, 'Currently no data connection; expecting client to connect to pasv server shortly...'); + this._log(LOG.DEBUG, 'Currently no data connection; expecting client to connect to pasv server shortly...'); this.dataListener.once('ready', () => { - this._logIf(LOG.DEBUG, '...client has connected now'); + this._log(LOG.DEBUG, '...client has connected now'); callback(this.dataSocket); }); } } else { // Do we need to open the data connection? if (this.dataSocket) { // There really shouldn't be an existing connection - this._logIf(LOG.DEBUG, 'Using existing non-passive dataSocket'); + this._log(LOG.DEBUG, 'Using existing non-passive dataSocket'); callback(this.dataSocket); } else { this._initiateData((sock) => { @@ -179,7 +183,7 @@ class FtpConnection extends EventEmitter { }); const allOver = (err) => { this.dataSocket = null; - this._logIf( + this._log( err ? LOG.ERROR : LOG.DEBUG, 'Non-passive data connection ended' + (err ? 'due to error: ' + util.inspect(err) : '') ); @@ -189,19 +193,19 @@ class FtpConnection extends EventEmitter { sock.on('error', (err) => { this._closeSocket(sock, true); - this._logIf(LOG.ERROR, 'Data connection error: ' + util.inspect(err)); + this._log(LOG.ERROR, 'Data connection error: ' + util.inspect(err)); this.dataSocket = null; this.dataConfigured = false; }); } _onError(err) { - this._logIf(LOG.ERROR, 'Client connection error: ' + util.inspect(err)); + this._log(LOG.ERROR, 'Client connection error: ' + util.inspect(err)); this._closeSocket(this.socket, true); } _onEnd() { - this._logIf(LOG.DEBUG, 'Client connection ended'); + this._log(LOG.DEBUG, 'Client connection ended'); } _onClose(hadError) { @@ -221,7 +225,7 @@ class FtpConnection extends EventEmitter { this.pasv = null; } // TODO: LOG.DEBUG? - this._logIf(LOG.INFO, 'Client connection closed'); + this._log(LOG.INFO, 'Client connection closed'); } _onData(data) { @@ -230,9 +234,9 @@ class FtpConnection extends EventEmitter { } data = data.toString('utf-8').trim(); - this._logIf(LOG.TRACE, '<< ' + data); + this._log(LOG.TRACE, '<< ' + data); // Don't want to include passwords in logs. - this._logIf(LOG.INFO, 'FTP command: ' + + this._log(LOG.INFO, 'FTP command: ' + data.replace(/^PASS [\s\S]*$/i, 'PASS ***') ); @@ -291,7 +295,7 @@ class FtpConnection extends EventEmitter { return success(); } - this._logIf(LOG.DEBUG, 'Sending file list'); + this._log(LOG.DEBUG, 'Sending file list'); for (var i = 0; i < fileInfos.length; ++i) { var fileInfo = fileInfos[i]; @@ -407,7 +411,7 @@ class FtpConnection extends EventEmitter { var portRangeErrorHandler; const normalErrorHandler = (e) => { - this._logIf(LOG.WARN, 'Error with passive data listener: ' + util.inspect(e)); + this._log(LOG.WARN, 'Error with passive data listener: ' + util.inspect(e)); this.respond('421 Server was unable to open passive connection listener'); this.dataConfigured = false; this.dataListener = null; @@ -426,7 +430,7 @@ class FtpConnection extends EventEmitter { if (e.code === 'EADDRINUSE' && i < this.server.options.pasvPortRangeEnd) { pasv.listen(++i); } else { - this._logIf(LOG.DEBUG, 'Passing on error from portRangeErrorHandler to normalErrorHandler:' + JSON.stringify(e)); + this._log(LOG.DEBUG, 'Passing on error from portRangeErrorHandler to normalErrorHandler:' + JSON.stringify(e)); normalErrorHandler(e); } }; @@ -445,17 +449,17 @@ class FtpConnection extends EventEmitter { pasv.addListener('error', normalErrorHandler); } - this._logIf(LOG.DEBUG, 'Passive data connection beginning to listen'); + this._log(LOG.DEBUG, 'Passive data connection beginning to listen'); var port = pasv.address().port; this.dataListener = new PassiveListener(); - this._logIf(LOG.DEBUG, 'Passive data connection listening on port ' + port); + this._log(LOG.DEBUG, 'Passive data connection listening on port ' + port); this._writePASVReady(command); }); pasv.on('close', () => { this.pasv = null; this.dataListener = null; - this._logIf(LOG.DEBUG, 'Passive data listener closed'); + this._log(LOG.DEBUG, 'Passive data listener closed'); }); } @@ -488,7 +492,7 @@ class FtpConnection extends EventEmitter { this.respond('550 Not Found'); } else { // Who knows what's going on here... this.respond('550 Not Accessible'); - this._logIf(LOG.ERROR, "Error at read of '" + filename + "' other than ENOENT " + err); + this._log(LOG.ERROR, "Error at read of '" + filename + "' other than ENOENT " + err); } } else { afterOk(() => { @@ -565,7 +569,7 @@ class FtpConnection extends EventEmitter { this.respond('550 Not Found'); } else { // Who knows what's going on here... this.respond('550 Not Accessible'); - this._logIf(LOG.ERROR, "Error at read of '" + filename + "' other than ENOENT " + err); + this._log(LOG.ERROR, "Error at read of '" + filename + "' other than ENOENT " + err); } } else { afterOk(() => { @@ -640,8 +644,8 @@ class FtpConnection extends EventEmitter { this._whenDataReady(handleUpload); storeStream.on('open', () => { - this._logIf(LOG.DEBUG, 'File opened/created: ' + filename); - this._logIf(LOG.DEBUG, 'Told client ok to send file data'); + this._log(LOG.DEBUG, 'File opened/created: ' + filename); + this._log(LOG.DEBUG, 'Told client ok to send file data'); // Adding event emitter for upload start time this.emit('file:stor', 'open', { user: this.username, @@ -717,7 +721,7 @@ class FtpConnection extends EventEmitter { // Otherwise, we call _STOR_usingWriteStream, and tell it to prepend the stuff // that we've buffered so far to the file. - this._logIf(LOG.WARN, 'uploadMaxSlurpSize exceeded; falling back to createWriteStream'); + this._log(LOG.WARN, 'uploadMaxSlurpSize exceeded; falling back to createWriteStream'); this._storeUsingCreateWriteStream(filename, [slurpBuf.slice(0, totalBytes), buf]); this.dataSocket.removeListener('data', dataHandler); this.dataSocket.removeListener('error', errorHandler); @@ -758,7 +762,7 @@ class FtpConnection extends EventEmitter { }); if (err) { erroredOut = true; - this._logIf(LOG.ERROR, 'Error writing file. ' + err); + this._log(LOG.ERROR, 'Error writing file. ' + err); if (this.dataSocket) { this._closeSocket(this.dataSocket, true); } @@ -822,10 +826,10 @@ class FtpConnection extends EventEmitter { } this.respond('234 Honored', () => { - this._logIf(LOG.INFO, 'Establishing secure connection...'); + this._log(LOG.INFO, 'Establishing secure connection...'); starttls.starttlsServer(this.socket, this.server.options.tlsOptions, (err, cleartext) => { const switchToSecure = () => { - this._logIf(LOG.INFO, 'Secure connection started'); + this._log(LOG.INFO, 'Secure connection started'); this.socket = cleartext; this.socket.on('data', (data) => { this._onData(data); @@ -833,15 +837,15 @@ class FtpConnection extends EventEmitter { this.secure = true; }; if (err) { - this._logIf(LOG.ERROR, 'Error upgrading connection to TLS: ' + util.inspect(err)); + this._log(LOG.ERROR, 'Error upgrading connection to TLS: ' + util.inspect(err)); this._closeSocket(this.socket, true); } else if (!cleartext.authorized) { - this._logIf(LOG.INFO, 'Secure socket not authorized: ' + util.inspect(cleartext.authorizationError)); + this._log(LOG.INFO, 'Secure socket not authorized: ' + util.inspect(cleartext.authorizationError)); if (this.server.options.allowUnauthorizedTls) { - this._logIf(LOG.INFO, 'Allowing unauthorized connection (allowUnauthorizedTls is on)'); + this._log(LOG.INFO, 'Allowing unauthorized connection (allowUnauthorizedTls is on)'); switchToSecure(); } else { - this._logIf(LOG.INFO, 'Closing unauthorized connection (allowUnauthorizedTls is off)'); + this._log(LOG.INFO, 'Closing unauthorized connection (allowUnauthorizedTls is off)'); this._closeSocket(this.socket, true); } } else { @@ -867,10 +871,10 @@ class FtpConnection extends EventEmitter { var pathEscaped = pathEscape(pathServer); this.fs.stat(pathFs, (err, stats) => { if (err) { - this._logIf(LOG.ERROR, 'CWD ' + pathRequest + ': ' + err); + this._log(LOG.ERROR, 'CWD ' + pathRequest + ': ' + err); this.respond('550 Directory not found.'); } else if (!stats.isDirectory()) { - this._logIf(LOG.WARN, 'Attempt to CWD to non-directory'); + this._log(LOG.WARN, 'Attempt to CWD to non-directory'); this.respond('550 Not a directory'); } else { this.cwd = pathServer; @@ -884,7 +888,7 @@ class FtpConnection extends EventEmitter { var filename = withCwd(this.cwd, commandArg); this.fs.unlink(pathModule.join(this.root, filename), (err) => { if (err) { - this._logIf(LOG.ERROR, 'Error deleting file: ' + filename + ', ' + err); + this._log(LOG.ERROR, 'Error deleting file: ' + filename + ', ' + err); // write error to socket this.respond('550 Permission denied'); } else { @@ -967,7 +971,7 @@ class FtpConnection extends EventEmitter { this.fs, (err, files) => { if (err) { - this._logIf(LOG.ERROR, 'Error sending file list, reading directory: ' + err); + this._log(LOG.ERROR, 'Error sending file list, reading directory: ' + err); this.respond('550 Not a directory'); return; } @@ -979,7 +983,7 @@ class FtpConnection extends EventEmitter { this.server.getUsernameFromUid(files[ii].stats.uid, (e1, uname) => { this.server.getGroupFromGid(files[ii].stats.gid, (e2, gname) => { if (e1 || e2) { - this._logIf(LOG.WARN, 'Error getting user/group name for file: ' + util.inspect(e1 || e2)); + this._log(LOG.WARN, 'Error getting user/group name for file: ' + util.inspect(e1 || e2)); fileInfos.push({ file: files[ii], uname: null, @@ -1033,7 +1037,7 @@ class FtpConnection extends EventEmitter { )); } - this._logIf(LOG.INFO, 'Directory has ' + files.length + ' files'); + this._log(LOG.INFO, 'Directory has ' + files.length + ' files'); if (files.length === 0) { return this._listFiles([], isDetailed, command); } @@ -1081,7 +1085,7 @@ class FtpConnection extends EventEmitter { var pathFs = pathModule.join(this.root, pathServer); this.fs.mkdir(pathFs, 0o755, (err) => { if (err) { - this._logIf(LOG.ERROR, 'MKD ' + pathRequest + ': ' + err); + this._log(LOG.ERROR, 'MKD ' + pathRequest + ': ' + err); this.respond('550 "' + pathEscaped + '" directory NOT created'); } else { this.respond('257 "' + pathEscaped + '" directory created'); @@ -1104,7 +1108,7 @@ class FtpConnection extends EventEmitter { this.dataConfigured = true; this.dataHost = host; this.dataPort = port; - this._logIf(LOG.DEBUG, 'self.dataHost, self.dataPort set to ' + this.dataHost + ':' + this.dataPort); + this._log(LOG.DEBUG, 'self.dataHost, self.dataPort set to ' + this.dataHost + ':' + this.dataPort); this.respond('200 OK'); } @@ -1121,10 +1125,10 @@ class FtpConnection extends EventEmitter { } if (this.dataListener) { - this._logIf(LOG.DEBUG, 'Telling client that they can connect now'); + this._log(LOG.DEBUG, 'Telling client that they can connect now'); this._writePASVReady(command); } else { - this._logIf(LOG.DEBUG, 'Setting up listener for passive connections'); + this._log(LOG.DEBUG, 'Setting up listener for passive connections'); this._setupNewPASV(commandArg, command); } @@ -1192,7 +1196,7 @@ class FtpConnection extends EventEmitter { this.hasQuit = true; this.respond('221 Goodbye', (err) => { if (err) { - this._logIf(LOG.ERROR, "Error writing 'Goodbye' message following QUIT"); + this._log(LOG.ERROR, "Error writing 'Goodbye' message following QUIT"); } this._closeSocket(this.socket, true); this._closeDataConnections(); @@ -1215,7 +1219,7 @@ class FtpConnection extends EventEmitter { var pathFs = pathModule.join(this.root, pathServer); this.fs.rmdir(pathFs, (err) => { if (err) { - this._logIf(LOG.ERROR, 'RMD ' + pathRequest + ': ' + err); + this._log(LOG.ERROR, 'RMD ' + pathRequest + ': ' + err); this.respond('550 Delete operation failed'); } else { this.respond('250 "' + pathServer + '" directory removed'); @@ -1226,7 +1230,7 @@ class FtpConnection extends EventEmitter { __RNFR(commandArg) { this.filefrom = withCwd(this.cwd, commandArg); - this._logIf(LOG.DEBUG, 'Rename from ' + this.filefrom); + this._log(LOG.DEBUG, 'Rename from ' + this.filefrom); this.respond('350 Ready for destination name'); } @@ -1234,7 +1238,7 @@ class FtpConnection extends EventEmitter { var fileto = withCwd(this.cwd, commandArg); this.fs.rename(pathModule.join(this.root, this.filefrom), pathModule.join(this.root, fileto), (err) => { if (err) { - this._logIf(LOG.ERROR, 'Error renaming file from ' + this.filefrom + ' to ' + fileto); + this._log(LOG.ERROR, 'Error renaming file from ' + this.filefrom + ' to ' + fileto); this.respond('550 Rename failed' + (err.code === 'ENOENT' ? '; file does not exist' : '')); } else { this.respond('250 File renamed successfully'); @@ -1246,7 +1250,7 @@ class FtpConnection extends EventEmitter { var filename = withCwd(this.cwd, commandArg); this.fs.stat(pathModule.join(this.root, filename), (err, s) => { if (err) { - this._logIf(LOG.ERROR, "Error getting size of file '" + filename + "' "); + this._log(LOG.ERROR, "Error getting size of file '" + filename + "' "); this.respond('450 Failed to get size of file'); return; } @@ -1320,7 +1324,7 @@ class FtpConnection extends EventEmitter { // success callback (username, userFsModule) => { const panic = (error, method) => { - this._logIf(LOG.ERROR, method + ' signaled error ' + util.inspect(error)); + this._log(LOG.ERROR, method + ' signaled error ' + util.inspect(error)); this.respond('421 Service not available, closing control connection.', () => { this._closeSocket(this.socket, true); }); diff --git a/src/FtpServer.js b/src/FtpServer.js index 05a2b4e..bfb8c47 100644 --- a/src/FtpServer.js +++ b/src/FtpServer.js @@ -92,7 +92,7 @@ class FtpServer extends EventEmitter { socket.setTimeout(0); socket.setNoDelay(); - this._logIf(LOG.INFO, 'Accepted a new client connection'); + this._log(LOG.INFO, 'Accepted a new client connection'); conn.respond('220 FTP server (nodeftpd) ready'); socket.on('data', (buf) => { @@ -110,15 +110,10 @@ class FtpServer extends EventEmitter { }); } - _logIf(verbosity, message, conn) { + _log(verbosity, message) { if (verbosity > this.options.logLevel) { return; } - // TODO: Move this to FtpConnection.prototype._logIf. - var peerAddr = (conn && conn.socket && conn.socket.remoteAddress); - if (peerAddr) { - message = '<' + peerAddr + '> ' + message; - } if (verbosity === LOG.ERROR) { message = 'ERROR: ' + message; } else if (verbosity === LOG.WARN) { diff --git a/src/__tests__/lib/common.js b/src/__tests__/lib/common.js index 66714f1..2e619bf 100644 --- a/src/__tests__/lib/common.js +++ b/src/__tests__/lib/common.js @@ -78,9 +78,12 @@ const common = { } }); }); - var origLogIf = server._logIf; + var _log = server._log; server.suppressExpecteErrMsgs = []; - server._logIf = (verbosity, message, conn) => { + server._log = (...args) => { + var verbosity = args[0]; + // Remove the <0.0.0.0> prefix. + var message = args[1].replace(/^<.+?> /, ''); var expecteErrMsgs = server.suppressExpecteErrMsgs; message = String(message).split(fixturesPath).join('fixture:/'); if ((expecteErrMsgs.length > 0) && (verbosity < LOG_LEVELS.INFO)) { @@ -104,7 +107,7 @@ const common = { ); } } - return origLogIf.call(server, verbosity, message, conn); + return _log.apply(server, args); }; server.listen(customOptions.port); return server; From 83b49eb4d63d1342a35f113103ecd0087dd6b710 Mon Sep 17 00:00:00 2001 From: Simon Sturmer Date: Sun, 17 Jan 2016 19:30:06 +0700 Subject: [PATCH 34/55] minor updates to __FEAT --- src/FtpConnection.js | 21 +++++++-------------- 1 file changed, 7 insertions(+), 14 deletions(-) diff --git a/src/FtpConnection.js b/src/FtpConnection.js index a4e85d7..f4e119b 100644 --- a/src/FtpConnection.js +++ b/src/FtpConnection.js @@ -897,21 +897,14 @@ class FtpConnection extends EventEmitter { }); } + // Get the feature list implemented by the server. (RFC 2389) __FEAT() { - // Get the feature list implemented by the server. (RFC 2389) - this.respond( - '211-Features\r\n' + - ' SIZE\r\n' + - ' UTF8\r\n' + - ' MDTM\r\n' + - (!this.server.options.tlsOptions ? '' : - ' AUTH TLS\r\n' + - ' PBSZ\r\n' + - ' UTF8\r\n' + - ' PROT\r\n' - ) + - '211 end' - ); + let features = ['SIZE', 'UTF8', 'MDTM']; + if (this.server.options.tlsOptions) { + features.push('AUTH TLS', 'PBSZ', 'PROT'); + } + features = features.map((feature) => ' ' + feature + '\r\n'); + this.respond('211-Features\r\n' + features.join('') + '211 end'); } __OPTS(commandArg) { From 6fb5106fac9e1803839de52e7eeb9e78f16fc6ee Mon Sep 17 00:00:00 2001 From: Simon Sturmer Date: Sun, 17 Jan 2016 19:45:12 +0700 Subject: [PATCH 35/55] npm: add build script, prepublish, .npmignore --- .npmignore | 6 ++++++ package.json | 9 ++++++--- 2 files changed, 12 insertions(+), 3 deletions(-) create mode 100644 .npmignore diff --git a/.npmignore b/.npmignore new file mode 100644 index 0000000..4d32f4f --- /dev/null +++ b/.npmignore @@ -0,0 +1,6 @@ +_* +.* +fixture/ +src/ +testcert/ +test.js diff --git a/package.json b/package.json index 2b567ed..7048a74 100644 --- a/package.json +++ b/package.json @@ -7,8 +7,11 @@ "node": ">=0.10.0" }, "scripts": { - "test": "mocha --compilers js:babel-register **/__tests__/**.js", - "lint": "eslint --max-warnings 0 ." + "lint": "eslint --max-warnings 0 .", + "build": "babel src --out-dir lib", + "test-lib": "mocha lib/__tests__ lib/**/__tests__", + "test": "npm run lint && npm run build && npm run test-lib", + "prepublish": "npm run build" }, "repository": { "type": "git", @@ -116,7 +119,7 @@ }, "devDependencies": { "async": "~0.1.15", - "babel-core": "^6.4.0", + "babel-cli": "^6.4.5", "babel-eslint": "^4.1.6", "babel-plugin-transform-class-properties": "^6.4.0", "babel-polyfill": "^6.3.14", From 5e60540119f1346a14a4ccddd45f34883d503807 Mon Sep 17 00:00:00 2001 From: Simon Sturmer Date: Fri, 22 Jan 2016 05:40:59 +0700 Subject: [PATCH 36/55] [temp] update contributers in package.json --- package.json | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/package.json b/package.json index 7048a74..1db68f0 100644 --- a/package.json +++ b/package.json @@ -52,6 +52,10 @@ "name": "Indra Gunawan", "url": "https://github.com/coderbuzz" }, + { + "name": "Robert Daigle", + "url": "https://github.com/crunchytortoise" + }, { "name": "Eric Newton", "url": "https://github.com/eric-newton" From 4bfa4497ba2fece8929fd26cc5c03cc1bebb7643 Mon Sep 17 00:00:00 2001 From: Simon Sturmer Date: Thu, 21 Jan 2016 19:02:32 +0700 Subject: [PATCH 37/55] ES6: update gitignore to exclude compiled code --- .gitignore | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index dbf0821..07088ed 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,4 @@ -node_modules/* \ No newline at end of file +_* +!__tests__ +lib/ +node_modules/ From 1b8cf543fd27c3f84028f55a2855154e57c84fb5 Mon Sep 17 00:00:00 2001 From: Simon Sturmer Date: Sun, 17 Jan 2016 19:51:52 +0700 Subject: [PATCH 38/55] ES6: update eslintignore to exclude compiled code --- .eslintignore | 2 ++ .gitignore | 1 - lib/.gitignore | 2 ++ 3 files changed, 4 insertions(+), 1 deletion(-) create mode 100644 lib/.gitignore diff --git a/.eslintignore b/.eslintignore index 8d5c8e5..07088ed 100644 --- a/.eslintignore +++ b/.eslintignore @@ -1,2 +1,4 @@ _* +!__tests__ +lib/ node_modules/ diff --git a/.gitignore b/.gitignore index 07088ed..57a6da3 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,3 @@ _* !__tests__ -lib/ node_modules/ diff --git a/lib/.gitignore b/lib/.gitignore new file mode 100644 index 0000000..d6b7ef3 --- /dev/null +++ b/lib/.gitignore @@ -0,0 +1,2 @@ +* +!.gitignore From cd2800aad6b68e852476d90cb6f08251f986b71c Mon Sep 17 00:00:00 2001 From: Simon Sturmer Date: Mon, 25 Jan 2016 09:25:37 +0700 Subject: [PATCH 39/55] eslint: ignore files in /fixture [via @oleksiyk] --- .eslintignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.eslintignore b/.eslintignore index 07088ed..bfb478e 100644 --- a/.eslintignore +++ b/.eslintignore @@ -1,4 +1,5 @@ _* !__tests__ lib/ +fixture/ node_modules/ From 719931cf5dd95a2993ef8cc4c329b4981ce1baf2 Mon Sep 17 00:00:00 2001 From: Simon Sturmer Date: Tue, 19 Jan 2016 13:59:32 +0700 Subject: [PATCH 40/55] remove empty PassiveListener class --- src/FtpConnection.js | 3 +-- src/PassiveListener.js | 5 ----- 2 files changed, 1 insertion(+), 7 deletions(-) delete mode 100644 src/PassiveListener.js diff --git a/src/FtpConnection.js b/src/FtpConnection.js index f4e119b..37d28ae 100644 --- a/src/FtpConnection.js +++ b/src/FtpConnection.js @@ -9,7 +9,6 @@ import dateformat from 'dateformat'; import * as glob from './glob'; import starttls from './starttls'; import Constants from './Constants'; -import PassiveListener from './PassiveListener'; import pathEscape from './helpers/pathEscape'; import withCwd from './helpers/withCwd'; @@ -452,7 +451,7 @@ class FtpConnection extends EventEmitter { this._log(LOG.DEBUG, 'Passive data connection beginning to listen'); var port = pasv.address().port; - this.dataListener = new PassiveListener(); + this.dataListener = new EventEmitter(); this._log(LOG.DEBUG, 'Passive data connection listening on port ' + port); this._writePASVReady(command); }); diff --git a/src/PassiveListener.js b/src/PassiveListener.js deleted file mode 100644 index 0c0d17a..0000000 --- a/src/PassiveListener.js +++ /dev/null @@ -1,5 +0,0 @@ -import events from 'events'; - -const {EventEmitter} = events; - -export default class PassiveListener extends EventEmitter {} From 97ba57f6f03de4ebb2ece7e75e7a3e437a8c267d Mon Sep 17 00:00:00 2001 From: Simon Sturmer Date: Mon, 25 Jan 2016 09:29:16 +0700 Subject: [PATCH 41/55] Better APPE tests --- src/__tests__/appe.js | 86 ++++++++++++++++++++++++++++++------------- 1 file changed, 61 insertions(+), 25 deletions(-) diff --git a/src/__tests__/appe.js b/src/__tests__/appe.js index 4054dd6..7bed32d 100644 --- a/src/__tests__/appe.js +++ b/src/__tests__/appe.js @@ -6,42 +6,78 @@ var fs = require('fs'); describe('APPE command', () => { var client = new FtpClient(); var server; + var fileToUpload = __filename; + var savedFileName = path.basename(fileToUpload) + '.test.txt'; + var savedFilePublicPath = '/uploads/' + savedFileName; + var savedFilePrivatePath = path.join( + common.fixturesPath(), + common.defaultOptions().user, + 'uploads', + savedFileName + ); - //run tests both ways - [true, false].forEach((useWriteFile) => { + //run tests with various option combinations + let optionCombinations = [ + {useWriteFile: false}, + {useWriteFile: true}, + // This will cause writeFile to fall back to using a stream. + {useWriteFile: true, uploadMaxSlurpSize: 1}, + ]; + optionCombinations.forEach((options) => { - describe('with useWriteFile = ' + useWriteFile, () => { + describe('with ' + JSON.stringify(options), () => { beforeEach((done) => { - server = common.server({useWriteFile: useWriteFile}); + server = common.server(options); client.once('ready', () => { done(); }); - client.connect({ - host: common.defaultOptions().host, - port: common.defaultOptions().port, - user: common.defaultOptions().user, - password: common.defaultOptions().pass, - }); + let {host, port, user, pass} = common.defaultOptions(); + client.connect({host, port, user, password: pass}); }); it('should append data to existing file', (done) => { - var basename = path.basename(__filename); - var fileSize = fs.statSync(__filename).size; - client.put(__filename, '/uploads/' + basename, (err) => { - common.should.not.exist(err); - client.append(__filename, '/uploads/' + basename, (err) => { - common.should.not.exist(err); - var filePath = path.join( - common.fixturesPath(), - common.defaultOptions().user, - 'uploads', - basename - ); - var newSize = fs.statSync(filePath).size; - newSize.should.be.eql(fileSize * 2); - done(); + var removeFile = (callback) => { + fs.unlink(savedFilePrivatePath, (error) => { + common.should.not.exist(error); + callback(); + }); + }; + + var doTest = () => { + fs.stat(fileToUpload, (error, stat) => { + common.should.not.exist(error); + var fileSize = stat.size; + client.put(fileToUpload, savedFilePublicPath, (error) => { + common.should.not.exist(error); + fs.stat(savedFilePrivatePath, (error, stat) => { + common.should.not.exist(error); + stat.size.should.eql(fileSize); + client.append(fileToUpload, savedFilePublicPath, (error) => { + common.should.not.exist(error); + fs.stat(savedFilePrivatePath, (error, stat) => { + common.should.not.exist(error); + var newSize = stat.size; + newSize.should.eql(fileSize * 2); + removeFile(done); + }); + }); + }); + }); }); + }; + + fs.stat(savedFilePrivatePath, (error) => { + if (error) { + if (error.code !== 'ENOENT') { + throw error; + } + // The file doesn't exist. This is expected. Proceed with test. + doTest(); + } else { + // The file exists. Delete it and then proceed with test. + removeFile(doTest); + } }); }); From d467c8b13f40779ea0f0564f7d0ec95749350967 Mon Sep 17 00:00:00 2001 From: Simon Sturmer Date: Mon, 25 Jan 2016 10:32:59 +0700 Subject: [PATCH 42/55] AAPE: fix _storeUsingCreateWriteStream flags bug when uploadMaxSlurpSize is exceeded --- src/FtpConnection.js | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/FtpConnection.js b/src/FtpConnection.js index 37d28ae..13d14b6 100644 --- a/src/FtpConnection.js +++ b/src/FtpConnection.js @@ -693,7 +693,7 @@ class FtpConnection extends EventEmitter { _storeUsingWriteFile(filename, flag) { var erroredOut = false; - var slurpBuf = new Buffer(1024); + var slurpBuf = new Buffer(1024); // TODO: Why is there a magic number here? var totalBytes = 0; var startTime = new Date(); @@ -704,8 +704,10 @@ class FtpConnection extends EventEmitter { }); const dataHandler = (buf) => { - if (this.server.options.uploadMaxSlurpSize != null && - totalBytes + buf.length > this.server.options.uploadMaxSlurpSize) { + if ( + this.server.options.uploadMaxSlurpSize != null && + totalBytes + buf.length > this.server.options.uploadMaxSlurpSize + ) { // Give up trying to slurp it -- it's too big. // If the 'fs' module we've been given doesn't implement 'createWriteStream', then @@ -721,7 +723,7 @@ class FtpConnection extends EventEmitter { // Otherwise, we call _STOR_usingWriteStream, and tell it to prepend the stuff // that we've buffered so far to the file. this._log(LOG.WARN, 'uploadMaxSlurpSize exceeded; falling back to createWriteStream'); - this._storeUsingCreateWriteStream(filename, [slurpBuf.slice(0, totalBytes), buf]); + this._storeUsingCreateWriteStream(filename, [slurpBuf.slice(0, totalBytes), buf], flag); this.dataSocket.removeListener('data', dataHandler); this.dataSocket.removeListener('error', errorHandler); this.dataSocket.removeListener('close', closeHandler); From c41f64c8eed7f51adcf65633d6c7fe359f4429b1 Mon Sep 17 00:00:00 2001 From: Simon Sturmer Date: Tue, 19 Jan 2016 14:24:42 +0700 Subject: [PATCH 43/55] add helpers/toBase256 for PASV response --- src/helpers/__tests__/toBase256-test.js | 23 +++++++++++++++++++++++ src/helpers/toBase256.js | 22 ++++++++++++++++++++++ 2 files changed, 45 insertions(+) create mode 100644 src/helpers/__tests__/toBase256-test.js create mode 100644 src/helpers/toBase256.js diff --git a/src/helpers/__tests__/toBase256-test.js b/src/helpers/__tests__/toBase256-test.js new file mode 100644 index 0000000..e57d2c0 --- /dev/null +++ b/src/helpers/__tests__/toBase256-test.js @@ -0,0 +1,23 @@ +/*eslint-env node, mocha */ +import toBase256 from '../toBase256'; +import expect from 'expect'; + +describe('helpers/toBase256', () => { + it('should convert numbers to their base256 parts', () => { + expect(toBase256(1)).toEqual([1]); + expect(toBase256(256)).toEqual([1, 0]); + expect(toBase256(257)).toEqual([1, 1]); + }); + + it('should not allow negative numbers or overflow', () => { + expect(toBase256(-1)).toEqual([0]); + expect(toBase256(2147483648)).toEqual([0]); + expect(toBase256(Math.pow(2, 33))).toEqual([0]); + }); + + it('should accept second parameter `minLength`', () => { + expect(toBase256(-1, 2)).toEqual([0, 0]); + expect(toBase256(2147483647, 5)).toEqual([0, 127, 255, 255, 255]); + expect(toBase256(300, -1)).toEqual([1, 44]); + }); +}); diff --git a/src/helpers/toBase256.js b/src/helpers/toBase256.js new file mode 100644 index 0000000..af0f202 --- /dev/null +++ b/src/helpers/toBase256.js @@ -0,0 +1,22 @@ +const toBase256 = (number, minLength = 1) => { + let digits = []; + // This will truncate the number if it's larger than 2^31 - 1. + number = number | 0; + if (number < 0) { + number = 0; + } + while (number !== 0) { + let modulus = (number % 256); + digits.unshift(modulus); + number = (number - modulus) / 256; + } + if (minLength < 1) { + minLength = 1; + } + while (digits.length < minLength) { + digits.unshift(0); + } + return digits; +}; + +export default toBase256; From 037137c50374a6cc129687bde5d89f0251b291df Mon Sep 17 00:00:00 2001 From: Simon Sturmer Date: Tue, 19 Jan 2016 13:50:42 +0700 Subject: [PATCH 44/55] implement PassiveListenerPool --- src/PassiveListenerPool.js | 271 ++++++++++++++++++++++ src/__tests__/PassiveListenerPool-test.js | 103 ++++++++ 2 files changed, 374 insertions(+) create mode 100644 src/PassiveListenerPool.js create mode 100644 src/__tests__/PassiveListenerPool-test.js diff --git a/src/PassiveListenerPool.js b/src/PassiveListenerPool.js new file mode 100644 index 0000000..3c7dadd --- /dev/null +++ b/src/PassiveListenerPool.js @@ -0,0 +1,271 @@ +/* @//flow */ + +import net from 'net'; +import {EventEmitter} from 'events'; + +const DEFAULT_OPTIONS = { + BIND_ADDRESS: '0.0.0.0', + MIN_PORT: 44001, + MAX_PORT: 44010, +}; + +// Maximum time we will wait for client to connect after PASV command. +const WAIT_TIMEOUT = 9000; + +export const CONNECTION_STATE = { + WAITING: 0, // Listener is waiting for client to connect (initial state). + CONNECTED: 1, // Client is connected. + CLOSED: 2, // Connection is closed (error or normal connection end). +}; + +export const LISTENER_STATE = { + INITIALIZING: 0, // Initial state. + LISTENING: 1, // Listener is waiting for client to connect. + CLOSED: 2, // Listener has stopped listening (connections may still exist). +}; + +// For use constructing an error using `new Error()` +let listenError = (errorCode, address, port) => ({ + message: `listen ${errorCode} ${address}:${port}`, + props: {code: errorCode, address, port}, +}); + +export class DataConnection extends EventEmitter { + constructor(port, remoteAddress) { + super(); + // It's important to store the listening port here so the control connection + // can send: 227 Entering Passive Mode (,) + this.port = port; + this.remoteAddress = remoteAddress; + this.state = CONNECTION_STATE.WAITING; + this._socket = null; + this._timer = setTimeout(() => { + this._onError( + new Error(`Expected a connection within ${WAIT_TIMEOUT}ms`) + ); + }, WAIT_TIMEOUT); + // Auto-bind methods. + this._onError = this._onError.bind(this); + this._onData = this._onData.bind(this); + this._close = this._close.bind(this); + } + + addSocket(socket) { + clearTimeout(this._timer); + this._socket = socket; + this.state = CONNECTION_STATE.CONNECTED; + socket.on('error', this._onError); + socket.on('data', this._onData); + socket.on('close', this._close); + this.emit('ready', socket); + } + + destroy() { + if (this._socket) { + this._socket.destroy(); // Will automatically emit `close`; + } else { + this._close(); + } + } + + _onError(error) { + this.emit('error', error); + process.nextTick(this._close); + } + + _onData(...args) { + this.emit('data', ...args); + } + + _close() { + if (this.state === CONNECTION_STATE.CLOSED) { + return; + } + this.state = CONNECTION_STATE.CLOSED; + this.emit('close'); + } +} + +export class Listener extends EventEmitter { + constructor(port, bindAddress) { + super(); + this.port = port; + this.bindAddress = bindAddress; + this.state = LISTENER_STATE.CLOSED; + this._waitingConnections = new Map(); + this._allConnections = new Set(); + // Auto-bind methods. + this._onStartingError = this._onStartingError.bind(this); + this._onRunningError = this._onRunningError.bind(this); + this._onReady = this._onReady.bind(this); + this._onConnection = this._onConnection.bind(this); + } + + listenForClient(remoteAddress) { + let {bindAddress, port} = this; + let key = port + '|' + remoteAddress; + let connection = new DataConnection(port, remoteAddress); + if (this._waitingConnections.has(key)) { + // We cannot simultaneously have more than one waitingConnection for the + // same remote address or it would create ambiguity (we wouldn't know + // which DataConnection instance to associate the incoming connection + // with). Treat this as an EADDRINUSE error to force the calling function + // to try another port. + process.nextTick(() => { + let e = listenError('EADDRINUSE', bindAddress, port); + connection.emit( + 'listenerError', + Object.assign(new Error(e.message), e.props) + ); + }); + return connection; + } + this._allConnections.add(connection); + this._waitingConnections.set(key, connection); + // `close` will be emitted when: + // * Client doesn't connect within wait time. + // * Client connects and transfer is completed successfully. + // * Client connects and some error occurs. + // but it will *not* be emitted when: + // * Listener fails to bind to bindAddress. + connection.on('close', () => { + this._allConnections.delete(connection); + this._waitingConnections.delete(key); + this._stopIfDone(); + }); + // If we're already listening, emit listenerReady on next tick. + // If we're not yet listening, start the listening server now. + // If we're in the process of starting the listening server (INITIALIZING) + // then do nothing since we will emit listenerReady when necessary. + if (this.state === LISTENER_STATE.LISTENING) { + process.nextTick(() => connection.emit('listenerReady')); + } else if (this.state === LISTENER_STATE.CLOSED) { + this._startServer(); + } + return connection; + } + + _onStartingError(error) { + for (let [key, connection] of this._waitingConnections.entries()) { + connection.emit('listenerError', error); + this._waitingConnections.delete(key); + } + this.state = LISTENER_STATE.CLOSED; + } + + _onRunningError(error) { + this._stopServer(); + this.state = LISTENER_STATE.CLOSED; + this.emit('error', error); + } + + _onReady(...args) { + this._server.removeListener('error', this._onStartingError); + this._server.on('error', this._onRunningError); + + this.state = LISTENER_STATE.LISTENING; + this.emit('listening', ...args); + for (let connection of this._waitingConnections.values()) { + connection.emit('listenerReady'); + } + } + + _onConnection(socket) { + let remoteAddress = socket.address().address; + if (remoteAddress.indexOf(':') !== -1) { + remoteAddress = remoteAddress.split(':').pop(); + } + let key = this.port + '|' + remoteAddress; + let connection = this._waitingConnections.get(key); + if (connection == null) { + socket.destroy(); + return; + } + this._waitingConnections.delete(key); + connection.addSocket(socket); + } + + _stopIfDone() { + if (this._waitingConnections.size !== 0) { + return; + } + this._stop(); + } + + // It's safe to call _stop() multiple times. + _stop() { + if (this._stopping) { + return; + } + if (this.state === LISTENER_STATE.INITIALIZING) { + this._stopping = true; + this.on('listening', () => { + this._stopping = false; + this._stop(); + }); + return; + } + if (this.state === LISTENER_STATE.LISTENING) { + this._stopServer(); + } + this.state = LISTENER_STATE.CLOSED; + } + + _startServer() { + this._server = net.createServer(); + this._server.on('error', this._onStartingError); + this._server.on('listening', this._onReady); + this._server.on('connection', this._onConnection); + this._server.listen(this.port, this.bindAddress); + this.state = LISTENER_STATE.INITIALIZING; + } + + _stopServer() { + this._server.removeListener('error', this._onStartingError); + this._server.removeListener('error', this._onRunningError); + this._server.removeListener('listening', this._onReady); + this._server.removeListener('connection', this._onConnection); + this._server.close(); + this._server = null; + } +} + +export default class PassiveListenerPool extends EventEmitter { + constructor(options = {}) { + super(); + this._bindAddress = options.bindAddress || DEFAULT_OPTIONS.BIND_ADDRESS; + let portRange = options.portRange || []; + this._minPort = portRange[0] || DEFAULT_OPTIONS.MIN_PORT; + this._maxPort = portRange[1] || DEFAULT_OPTIONS.MAX_PORT; + this._listeners = new Map(); + } + + createDataConnection(remoteAddress, callback) { + let port = this._minPort; + let dataConnection; + let onError = (error) => { + if (error.code === 'EADDRINUSE' && port < this._maxPort) { + port += 1; + startListener(); + } else { + callback(error); + } + }; + let onSuccess = () => { + dataConnection.removeListener('listenerError', onError); + dataConnection.removeListener('listenerReady', onSuccess); + callback(null, dataConnection); + }; + let startListener = () => { + let listener = this._listeners.get(port); + if (listener == null) { + listener = new Listener(port, this._bindAddress); + this._listeners.set(port, listener); + } + dataConnection = listener.listenForClient(remoteAddress); + dataConnection.on('listenerError', onError); + dataConnection.on('listenerReady', onSuccess); + }; + startListener(); + } +} diff --git a/src/__tests__/PassiveListenerPool-test.js b/src/__tests__/PassiveListenerPool-test.js new file mode 100644 index 0000000..57092f5 --- /dev/null +++ b/src/__tests__/PassiveListenerPool-test.js @@ -0,0 +1,103 @@ +/*eslint-env node, mocha */ +import net from 'net'; +import {EventEmitter} from 'events'; +import PassiveListenerPool from '../PassiveListenerPool'; +import toBase256 from '../helpers/toBase256'; +import expect from 'expect'; + +const BIND_ADDRESS = '127.0.0.1'; + +class MockControlConnection extends EventEmitter { + constructor({remoteAddress}) { + super(); + this.remoteAddress = remoteAddress; + this.messages = []; + this.writable = false; + process.nextTick(() => { + this.writable = true; + }); + } + + respond(message) { + this.messages.push(message); + } + + address() { + return {address: this.remoteAddress}; + } + + destroy() { + this.writable = false; + this.emit('close'); + } +} + +const getMessageOK = (address, port) => { + let encoded = address.split('.').join(',') + ',' + toBase256(port).join(','); + return `227 Entering Passive Mode (${encoded})`; +}; + +describe('PassiveListenerPool', () => { + let MIN_PORT = 2000; + let MAX_PORT = 2002; + let listenerPool = new PassiveListenerPool({ + bindAddress: BIND_ADDRESS, + portRange: [MIN_PORT, MAX_PORT], + }); + + it('should listen on a port after a connection is requested', (done) => { + let eventLog = []; + let log = (message) => { + eventLog.push(message); + }; + let finished = () => { + expect(eventLog).toEqual([ + 'listening on port 2000', + 'data connection received on listening port', + 'data connection closed', + ]); + done(); + }; + let controlConnection = new MockControlConnection({ + remoteAddress: '127.0.0.1', + }); + let remoteAddress = controlConnection.address().address; + listenerPool.createDataConnection(remoteAddress, (error, dataConnection) => { + let {port} = dataConnection; + log(`listening on port ${port}`); + expect(port).toBe(MIN_PORT); + // TODO: test that listenerPool has stopped listening? + controlConnection.respond( + getMessageOK(BIND_ADDRESS, port) + ); + // TODO: This test is useless besides testing our mock control connection. + expect(controlConnection.messages).toEqual([ + getMessageOK(BIND_ADDRESS, port), + ]); + // TODO: Ensure that if the controlConnection closes, our associated data + // connection gets closed also. + // controlConnection.on('close', () => { + // dataConnection.destroy(); + // }); + dataConnection.on('error', (error) => { + throw error; + }); + dataConnection.on('ready', (socket) => { + log('data connection received on listening port'); + expect(socket.writable).toBe(true); + }); + dataConnection.on('close', () => { + log('data connection closed'); + finished(); + }); + let clientConnection = net.createConnection(port, BIND_ADDRESS); + clientConnection.on('connect', () => { + setTimeout(() => { + // This will cause a close event on dataConnection. + clientConnection.destroy(); + }, 10); + }); + }); + }); + +}); From 97db780685c96198eeeae40ffced3de684e2dc8d Mon Sep 17 00:00:00 2001 From: Simon Sturmer Date: Wed, 27 Jan 2016 07:12:18 +0700 Subject: [PATCH 45/55] PassiveListenerPool: DataConnection class should not simulate a read/write stream --- src/PassiveListenerPool.js | 6 ------ 1 file changed, 6 deletions(-) diff --git a/src/PassiveListenerPool.js b/src/PassiveListenerPool.js index 3c7dadd..b538356 100644 --- a/src/PassiveListenerPool.js +++ b/src/PassiveListenerPool.js @@ -46,7 +46,6 @@ export class DataConnection extends EventEmitter { }, WAIT_TIMEOUT); // Auto-bind methods. this._onError = this._onError.bind(this); - this._onData = this._onData.bind(this); this._close = this._close.bind(this); } @@ -55,7 +54,6 @@ export class DataConnection extends EventEmitter { this._socket = socket; this.state = CONNECTION_STATE.CONNECTED; socket.on('error', this._onError); - socket.on('data', this._onData); socket.on('close', this._close); this.emit('ready', socket); } @@ -73,10 +71,6 @@ export class DataConnection extends EventEmitter { process.nextTick(this._close); } - _onData(...args) { - this.emit('data', ...args); - } - _close() { if (this.state === CONNECTION_STATE.CLOSED) { return; From 9cc3921d6ffe651f4a579995b2f7f0a0150d4bf1 Mon Sep 17 00:00:00 2001 From: Simon Sturmer Date: Fri, 29 Jan 2016 07:53:20 +0700 Subject: [PATCH 46/55] PassiveListenerPool: addSocket -> setSocket --- src/PassiveListenerPool.js | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/src/PassiveListenerPool.js b/src/PassiveListenerPool.js index b538356..2dafd1b 100644 --- a/src/PassiveListenerPool.js +++ b/src/PassiveListenerPool.js @@ -49,7 +49,17 @@ export class DataConnection extends EventEmitter { this._close = this._close.bind(this); } - addSocket(socket) { + getSocket() { + return this._socket; + } + + // This is not really a public method, except for use from the code that + // created this DataConnection (Listener). + setSocket(socket) { + // Prevent calling twice. + if (this._socket) { + throw new Error('DataConnection: method setSocket() called more than once.'); + } clearTimeout(this._timer); this._socket = socket; this.state = CONNECTION_STATE.CONNECTED; @@ -176,7 +186,7 @@ export class Listener extends EventEmitter { return; } this._waitingConnections.delete(key); - connection.addSocket(socket); + connection.setSocket(socket); } _stopIfDone() { From c621cdba651c316d72a6b212b25b0a733ac93809 Mon Sep 17 00:00:00 2001 From: Simon Sturmer Date: Fri, 29 Jan 2016 12:13:08 +0700 Subject: [PATCH 47/55] [wip] Refactor data connection logic: initial implementation --- src/FtpConnection.js | 433 ++++++++++++++++++++----------------------- src/FtpServer.js | 6 +- 2 files changed, 206 insertions(+), 233 deletions(-) diff --git a/src/FtpConnection.js b/src/FtpConnection.js index 13d14b6..b160ce4 100644 --- a/src/FtpConnection.js +++ b/src/FtpConnection.js @@ -23,6 +23,12 @@ var { COMMANDS_REQUIRE_DATA_SOCKET, } = Constants; +const encodeAddress = (host, port) => { + var i1 = (port / 256) | 0; + var i2 = port % 256; + return host.split('.').join(',') + ',' + i1 + ',' + i2; +}; + class FtpConnection extends EventEmitter { constructor(properties) { super(); @@ -56,129 +62,63 @@ class FtpConnection extends EventEmitter { return socket.write(data, 'utf8', callback); } - _authenticated() { + _isAuthenticated() { return !!this.username; } - _closeDataConnections() { - if (this.dataSocket) { - // TODO: should the second arg be false here? - this._closeSocket(this.dataSocket, true); - this.dataSocket = null; - } - if (this.pasv) { - this.pasv.close(); - this.pasv = null; - } + _doesRequireDataConnection(command) { + return (COMMANDS_REQUIRE_DATA_SOCKET[command] === true); } - _createPassiveServer() { - return net.createServer((psocket) => { - // This is simply a connection listener. - // TODO: Should we keep track of *all* connections, or enforce just one? - this._log(LOG.INFO, 'Passive data event: connect'); - - const setupPassiveListener = () => { - if (this.dataListener) { - this.dataListener.emit('ready'); - } else { - this._log(LOG.WARN, 'Passive connection initiated, but no data listener'); - } - - const allOver = (ename) => { - return (err) => { - this._log( - (err ? LOG.ERROR : LOG.DEBUG), - 'Passive data event: ' + ename + (err ? ' due to error' : '') - ); - this.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. - this.dataSocket.on('close', allOver('close')); - this.dataSocket.on('end', allOver('end')); - - this.dataSocket.on('error', (err) => { - this._log(LOG.ERROR, 'Passive data event: error: ' + err); - // TODO: Can we can rely on self.dataSocket having been closed? - this.dataSocket = null; - this.dataConfigured = false; - }); - }; + _hasReceivedDataConnectionRequest() { + return ( + this.isEstablishingDataConnection || + this.dataConnection != null + ); + } - if (this.secure) { - this._log(LOG.INFO, 'Upgrading passive connection to TLS'); - starttls.starttlsServer(psocket, this.server.options.tlsOptions, (err, cleartext) => { - const switchToSecure = () => { - this._log(LOG.INFO, 'Secure passive connection started'); - // TODO: Check for existing dataSocket. - this.dataSocket = cleartext; - setupPassiveListener(); - }; - if (err) { - this._log(LOG.ERROR, 'Error upgrading passive connection to TLS:' + util.inspect(err)); - this._closeSocket(psocket, true); - this.dataConfigured = false; - } else if (!cleartext.authorized) { - if (this.server.options.allowUnauthorizedTls) { - this._log(LOG.INFO, 'Allowing unauthorized passive connection (allowUnauthorizedTls is on)'); - switchToSecure(); - } else { - this._log(LOG.INFO, 'Closing unauthorized passive connection (allowUnauthorizedTls is off)'); - this._closeSocket(this.socket, true); - this.dataConfigured = false; - } - } else { - switchToSecure(); - } - }); - } else { - // TODO: Check for existing dataSocket. - this.dataSocket = psocket; - setupPassiveListener(); - } - }); + // TODO: rename this method. + _closeDataConnections() { + if (this.dataConnection) { + this.dataConnection.destroy(); + this.dataConnection = null; + } } _whenDataReady(callback) { - if (this.dataListener) { - // how many data connections are allowed? - // should still be listening since we created a server, right? - if (this.dataSocket) { - this._log(LOG.DEBUG, 'A data connection exists'); - callback(this.dataSocket); - } else { - this._log(LOG.DEBUG, 'Currently no data connection; expecting client to connect to pasv server shortly...'); - this.dataListener.once('ready', () => { - this._log(LOG.DEBUG, '...client has connected now'); - callback(this.dataSocket); - }); - } + // TODO: Better to check which mode we are (Active or Passive) and then act accordingly. + if (this.isEstablishingDataConnection) { + // TODO: reword. + this._log(LOG.DEBUG, 'Currently no data connection; expecting client to connect to pasv server shortly...'); + this.on('dataConnectionEstablished', () => { + this._log(LOG.DEBUG, '...client has connected now'); + let socket = this.dataConnection.getSocket(); + callback(socket); + }); + } else if (this.dataConnection) { + this._log(LOG.DEBUG, 'A data connection exists'); + let socket = this.dataConnection.getSocket(); + process.nextTick(() => { + callback(socket); + }); } else { - // Do we need to open the data connection? - if (this.dataSocket) { // There really shouldn't be an existing connection - this._log(LOG.DEBUG, 'Using existing non-passive dataSocket'); - callback(this.dataSocket); - } else { - this._initiateData((sock) => { - callback(sock); - }); - } + // This makes a connection to the client (Active mode). + this._initiateData((socket) => { + callback(socket); + }); } } + // This makes a connection to the client (Active mode). + // TODO: fix some stuff here. _initiateData(callback) { if (this.dataSocket) { return callback(this.dataSocket); } - - var sock = net.connect(this.dataPort, this.dataHost || this.socket.remoteAddress); - sock.on('connect', () => { - this.dataSocket = sock; - callback(sock); + var socket = net.connect(this.dataPort, this.dataHost || this.socket.remoteAddress); + socket.on('connect', () => { + this.dataSocket = socket; + callback(socket); }); const allOver = (err) => { this.dataSocket = null; @@ -187,14 +127,13 @@ class FtpConnection extends EventEmitter { 'Non-passive data connection ended' + (err ? 'due to error: ' + util.inspect(err) : '') ); }; - sock.on('end', allOver); - sock.on('close', allOver); - - sock.on('error', (err) => { - this._closeSocket(sock, true); + // TODO: allOver will get executed twice here. + socket.on('end', allOver); + socket.on('close', allOver); + socket.on('error', (err) => { + this._closeSocket(socket, true); this._log(LOG.ERROR, 'Data connection error: ' + util.inspect(err)); this.dataSocket = null; - this.dataConfigured = false; }); } @@ -219,9 +158,9 @@ class FtpConnection extends EventEmitter { this._closeSocket(this.socket, hadError); this.socket = null; } - if (this.pasv) { - this.pasv.close(); - this.pasv = null; + if (this.dataConnection) { + this.dataConnection.destroy(); + this.dataConnection = null; } // TODO: LOG.DEBUG? this._log(LOG.INFO, 'Client connection closed'); @@ -231,50 +170,52 @@ class FtpConnection extends EventEmitter { if (this.hasQuit) { return; } - data = data.toString('utf-8').trim(); this._log(LOG.TRACE, '<< ' + data); - // Don't want to include passwords in logs. - this._log(LOG.INFO, 'FTP command: ' + - data.replace(/^PASS [\s\S]*$/i, 'PASS ***') + // Don't include passwords in logs. + this._log( + LOG.INFO, + 'FTP command: ' + data.replace(/^PASS [\s\S]*$/i, 'PASS ***') ); - - var command; - var commandArg; - var index = data.indexOf(' '); - if (index !== -1) { - var parts = data.split(' '); - command = parts.shift().toUpperCase(); - commandArg = parts.join(' ').trim(); - } else { - command = data.toUpperCase(); - commandArg = ''; - } - - var methodName = '__' + command; + var parts = data.split(' '); + var command = parts.shift().toUpperCase(); + var commandArg = parts.join(' ').trim(); if ( COMMANDS_SUPPORTED[command] !== true || (this.allowedCommands != null && this.allowedCommands[command] !== true) ) { this.respond('502 Command not implemented.'); - } else if (COMMANDS_NO_AUTH[command] === true) { - this[methodName](commandArg, command); - } else { - // If 'tlsOnly' option is set, all commands which require user authentication will only - // be permitted over a secure connection. See RFC4217 regarding error code. - if (!this.secure && this.server.options.tlsOnly) { - this.respond('522 Protection level not sufficient; send AUTH TLS'); - } else if (this._authenticated()) { - if (COMMANDS_REQUIRE_DATA_SOCKET[command] === true && !this.dataConfigured) { - this.respond('425 Data connection not configured; send PASV or PORT'); - } else { - this[methodName](commandArg, command); - } - } else { - this.respond('530 Not logged in.'); - } + return; } - this.previousCommand = command; + if (COMMANDS_NO_AUTH[command] === true) { + this._execCommand(command, commandArg); + return; + } + // If 'tlsOnly' option is set, all commands which require user authentication will only + // be permitted over a secure connection. See RFC4217 regarding error code. + if (!this.secure && this.server.options.tlsOnly) { + this.respond('522 Protection level not sufficient; send AUTH TLS'); + return; + } + if (!this._isAuthenticated()) { + this.respond('530 Not logged in.'); + return; + } + if ( + this._doesRequireDataConnection(command) && + !this._hasReceivedDataConnectionRequest() + ) { + this.respond('425 Data connection not configured; send PASV or PORT'); + return; + } + this._execCommand(command, commandArg); + } + + _execCommand(command, commandArg) { + var methodName = '__' + command; + // this.emit('pre-command:' + command, commandArg, command); + this[methodName](commandArg, command); + // this.emit('post-command:' + command, commandArg, command); } _listFiles(fileInfos, isDetailed, command) { @@ -348,13 +289,13 @@ class FtpConnection extends EventEmitter { }); } + // TODO: refactor this and write tests. _parsePORT(commandArg) { var 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})$/); if (!m) { this.respond('501 Bad argument to PORT'); return; } - var host = m[1] + '.' + m[2] + '.' + m[3] + '.' + m[4]; var port = (parseInt(m[5], 10) << 8) + parseInt(m[6], 10); if (isNaN(port)) { @@ -392,74 +333,75 @@ class FtpConnection extends EventEmitter { return {host: m[1], port: r}; } - _writePASVReady(command) { - var a = this.pasv.address(); - var host = this.server.host; - var port = a.port; - if (command === 'PASV') { - var i1 = (port / 256) | 0; - var i2 = port % 256; - this.respond('227 Entering Passive Mode (' + host.split('.').join(',') + ',' + i1 + ',' + i2 + ')'); - } else { // EPASV - this.respond('229 Entering Extended Passive Mode (|||' + port + '|)'); - } + // TODO: move this to PassiveListenerPool + _upgradePassiveConnectionToTLS(socket, callback) { + this._log(LOG.INFO, 'Upgrading passive connection to TLS'); + starttls.starttlsServer(socket, this.server.options.tlsOptions, (err, cleartext) => { + const switchToSecure = () => { + this._log(LOG.INFO, 'Passive connection secured'); + // TODO: Check for existing dataSocket. + this.dataSocket = cleartext; + callback(); + }; + if (err) { + this._log(LOG.ERROR, 'Error upgrading passive connection to TLS:' + util.inspect(err)); + this._closeSocket(socket, true); + } else if (!cleartext.authorized) { + if (this.server.options.allowUnauthorizedTls) { + this._log(LOG.INFO, 'Allowing unauthorized passive connection (allowUnauthorizedTls is on)'); + switchToSecure(); + } else { + this._log(LOG.INFO, 'Closing unauthorized passive connection (allowUnauthorizedTls is off)'); + this._closeSocket(this.socket, true); + } + } else { + switchToSecure(); + } + }); } - _setupNewPASV(commandArg, command) { - var pasv = this._createPassiveServer(); - var portRangeErrorHandler; + // TODO: fix some stuff. + _onPassiveDataConnection(socket) { + // TODO: reword + this._log(LOG.INFO, 'Passive data event: connect'); - const normalErrorHandler = (e) => { - this._log(LOG.WARN, 'Error with passive data listener: ' + util.inspect(e)); - this.respond('421 Server was unable to open passive connection listener'); - this.dataConfigured = false; - this.dataListener = null; - this.dataSocket = null; - this.pasv = null; - }; + const setupPassiveListener = () => { + if (this.isEstablishingDataConnection) { + this.emit('dataConnectionEstablished'); + this.isEstablishingDataConnection = false; + } - if (this.server.options.pasvPortRangeStart != null && this.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 = this.server.options.pasvPortRangeStart; - pasv.listen(i); - portRangeErrorHandler = (e) => { - if (e.code === 'EADDRINUSE' && i < this.server.options.pasvPortRangeEnd) { - pasv.listen(++i); - } else { - this._log(LOG.DEBUG, 'Passing on error from portRangeErrorHandler to normalErrorHandler:' + JSON.stringify(e)); - normalErrorHandler(e); - } + const allOver = (ename) => { + return (error) => { + this._log( + error ? LOG.ERROR : LOG.DEBUG, + 'Passive data event: ' + ename + (error ? ' due to error' : '') + ); + this.dataSocket = null; + }; }; - pasv.on('error', portRangeErrorHandler); - } else { - pasv.listen(0); - pasv.on('error', normalErrorHandler); - } - - // Once we're successfully listening, tell the client - pasv.on('listening', () => { - this.pasv = pasv; - if (portRangeErrorHandler) { - pasv.removeListener('error', portRangeErrorHandler); - pasv.addListener('error', normalErrorHandler); - } + // 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. + this.dataSocket.on('close', allOver('close')); + this.dataSocket.on('end', allOver('end')); - this._log(LOG.DEBUG, 'Passive data connection beginning to listen'); + this.dataSocket.on('error', (err) => { + this._log(LOG.ERROR, 'Passive data event: error: ' + err); + // TODO: Can we can rely on self.dataSocket having been closed? + this.dataSocket = null; + }); + }; - var port = pasv.address().port; - this.dataListener = new EventEmitter(); - this._log(LOG.DEBUG, 'Passive data connection listening on port ' + port); - this._writePASVReady(command); - }); - pasv.on('close', () => { - this.pasv = null; - this.dataListener = null; - this._log(LOG.DEBUG, 'Passive data listener closed'); - }); + // TODO: remove this. + if (this.secure) { + this._upgradePassiveConnectionToTLS(socket, setupPassiveListener); + } else { + // TODO: Check for existing dataSocket. + this.dataSocket = socket; + setupPassiveListener(); + } } _retrieveUsingCreateReadStream(commandArg, filename) { @@ -1094,12 +1036,8 @@ class FtpConnection extends EventEmitter { return this; } - __PORT(commandArg, command) { - this.dataConfigured = false; - var {host, port} = (command === 'PORT') ? - this._parsePORT(commandArg) : - this._parsePORT(commandArg); - this.dataConfigured = true; + __PORT(commandArg) { + var {host, port} = this._parsePORT(commandArg); this.dataHost = host; this.dataPort = port; this._log(LOG.DEBUG, 'self.dataHost, self.dataPort set to ' + this.dataHost + ':' + this.dataPort); @@ -1111,22 +1049,51 @@ class FtpConnection extends EventEmitter { } __PASV(commandArg, command) { - this.dataConfigured = false; - - // not sure whether the spec limits to 1 data connection at a time ... - if (this.dataSocket) { - this._closeSocket(this.dataSocket, true); - } - - if (this.dataListener) { - this._log(LOG.DEBUG, 'Telling client that they can connect now'); - this._writePASVReady(command); - } else { - this._log(LOG.DEBUG, 'Setting up listener for passive connections'); - this._setupNewPASV(commandArg, command); + // Client already sent PASV. + if (this._hasReceivedDataConnectionRequest()) { + // TODO: Is this the right message here? + this.respond('503 Bad sequence of commands.'); + return; } + var isExtendedPASV = (command === 'EPSV'); + this._log(LOG.DEBUG, 'Setting up listener for passive connections'); + this.isEstablishingDataConnection = true; + // TODO: find a better way to get the address on which to bind listener. + let host = this.server.host; + this.passiveListenerPool.createDataConnection( + host, + (error, dataConnection) => { + if (error) { + this.isEstablishingDataConnection = false; + this._log(LOG.WARN, 'Error with passive data listener: ' + util.inspect(error)); + this.respond('421 Server was unable to open passive connection listener'); + return; + } + this.dataConnection = dataConnection; + var port = dataConnection.port; + this._log(LOG.DEBUG, `Listening for passive data connection on port ${port}`); + // Now that we're successfully listening, tell the client. + if (isExtendedPASV) { + this.respond('229 Entering Extended Passive Mode (|||' + port + '|)'); + } else { + this.respond(`227 Entering Passive Mode (${encodeAddress(host, port)})`); + } - this.dataConfigured = true; + dataConnection.on('error', (error) => { + this.isEstablishingDataConnection = false; + // TODO: handle error + }); + dataConnection.on('ready', (socket) => { + this.isEstablishingDataConnection = false; + // TODO: inline this? or better yet, abstract the entire process of establishing a data connection. + this._onPassiveDataConnection(socket); + }); + dataConnection.on('close', () => { + // TODO: reword. + this._log(LOG.DEBUG, 'Passive data listener closed'); + }); + } + ); } __EPSV(commandArg, command) { @@ -1297,6 +1264,7 @@ class FtpConnection extends EventEmitter { // success callback () => { this.respond('331 User name okay, need password.'); + this.isWaitingForPassword = true; }, // failure callback () => { @@ -1309,9 +1277,10 @@ class FtpConnection extends EventEmitter { // Specify a password for login __PASS(password) { - if (this.previousCommand !== 'USER') { + if (!this.isWaitingForPassword) { this.respond('503 Bad sequence of commands.'); } else { + this.isWaitingForPassword = false; this.emit( 'command:pass', password, diff --git a/src/FtpServer.js b/src/FtpServer.js index bfb8c47..a941ba9 100644 --- a/src/FtpServer.js +++ b/src/FtpServer.js @@ -47,6 +47,10 @@ class FtpServer extends EventEmitter { this.server.on('close', () => { this.emit('close'); }); + this.passiveListenerPool = new PassiveListenerPool({ + bindAddress: BIND_ADDRESS, + portRange: [options.pasvPortRangeStart, options.pasvPortRangeEnd], + }); } _onConnection(socket) { @@ -62,7 +66,7 @@ class FtpServer extends EventEmitter { var conn = new FtpConnection({ server: this, socket: socket, - pasv: null, // passive listener server + passiveListenerPool: this.passiveListenerPool, allowedCommands: allowedCommands, // subset of allowed commands for this server dataPort: 20, dataHost: null, From d117f560af6881c96908989c8d66f41a3e98afc6 Mon Sep 17 00:00:00 2001 From: Simon Sturmer Date: Sun, 31 Jan 2016 07:21:34 +0700 Subject: [PATCH 48/55] [wip] listenerPool.createDataConnection should accept an option useTLS --- src/FtpConnection.js | 124 ++++++++++++++-------- src/FtpServer.js | 25 +---- src/PassiveListenerPool.js | 68 +++++++++--- src/__tests__/PassiveListenerPool-test.js | 3 +- 4 files changed, 137 insertions(+), 83 deletions(-) diff --git a/src/FtpConnection.js b/src/FtpConnection.js index b160ce4..addb472 100644 --- a/src/FtpConnection.js +++ b/src/FtpConnection.js @@ -15,7 +15,9 @@ import withCwd from './helpers/withCwd'; import stripOptions from './helpers/stripOptions'; import leftPad from './helpers/leftPad'; -var { +const ENCODED_ADDRESS = /^[0-9]{1,3}(,[0-9]{1,3}){5}$/; + +const { // Use LOG for brevity. LOG_LEVELS: LOG, COMMANDS_SUPPORTED, @@ -29,12 +31,52 @@ const encodeAddress = (host, port) => { return host.split('.').join(',') + ',' + i1 + ',' + i2; }; +const decodeAddress = (encoded) => { + if (ENCODED_ADDRESS.test(encoded) === false) { + return null; + } + let octets = encoded.split(',').map((value) => parseInt(value, 10)); + let isOutOfRange = octets.some((number) => number > 255); + if (isOutOfRange) { + return null; + } + return { + host: octets.slice(0, 4).join('.'), + port: (octets[5] << 8) + octets[6], + }; +}; + class FtpConnection extends EventEmitter { - constructor(properties) { + constructor(options = {}) { super(); - Object.keys(properties).forEach((key) => { - this[key] = properties[key]; - }); + // TODO: Throw if any required option not present. + this.server = options.server; + this.socket = options.socket; + this.passiveListenerPool = options.passiveListenerPool; + this.allowedCommands = options.allowedCommands; + this.tlsOptions = options.tlsOptions; + + // TODO: I don't think this should be 20. + this.dataPort = 20; + this.dataHost = null; + // The incoming (PASV) data connection. + this.dataConnection = null; + // the actual data socket + this.dataSocket = null; + this._isEstablishingDataConnection = false; + this._hasReceivedPASV = false; + this._hasReceivedPORT = false; + + this.mode = 'ascii'; + this.filefrom = ''; + this.username = null; + this.fs = null; + this.cwd = null; + this.root = null; + this.hasQuit = false; + // State for handling TLS upgrades. + this.secure = false; + this.pbszReceived = false; } // TODO: rename this to writeLine? @@ -72,7 +114,7 @@ class FtpConnection extends EventEmitter { _hasReceivedDataConnectionRequest() { return ( - this.isEstablishingDataConnection || + this._isEstablishingDataConnection || this.dataConnection != null ); } @@ -87,7 +129,7 @@ class FtpConnection extends EventEmitter { _whenDataReady(callback) { // TODO: Better to check which mode we are (Active or Passive) and then act accordingly. - if (this.isEstablishingDataConnection) { + if (this._isEstablishingDataConnection) { // TODO: reword. this._log(LOG.DEBUG, 'Currently no data connection; expecting client to connect to pasv server shortly...'); this.on('dataConnectionEstablished', () => { @@ -289,22 +331,6 @@ class FtpConnection extends EventEmitter { }); } - // TODO: refactor this and write tests. - _parsePORT(commandArg) { - var 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})$/); - if (!m) { - this.respond('501 Bad argument to PORT'); - return; - } - var host = m[1] + '.' + m[2] + '.' + m[3] + '.' + m[4]; - var port = (parseInt(m[5], 10) << 8) + parseInt(m[6], 10); - if (isNaN(port)) { - // The value should never be NaN because the relevant groups in the regex matche 1-3 digits. - throw new Error('Impossible NaN in FtpConnection.prototype._PORT'); - } - return {host, port}; - } - _parseEPRT(commandArg) { if ( commandArg.length >= 3 && @@ -336,7 +362,7 @@ class FtpConnection extends EventEmitter { // TODO: move this to PassiveListenerPool _upgradePassiveConnectionToTLS(socket, callback) { this._log(LOG.INFO, 'Upgrading passive connection to TLS'); - starttls.starttlsServer(socket, this.server.options.tlsOptions, (err, cleartext) => { + starttls.starttlsServer(socket, this.tlsOptions, (err, cleartext) => { const switchToSecure = () => { this._log(LOG.INFO, 'Passive connection secured'); // TODO: Check for existing dataSocket. @@ -366,9 +392,9 @@ class FtpConnection extends EventEmitter { this._log(LOG.INFO, 'Passive data event: connect'); const setupPassiveListener = () => { - if (this.isEstablishingDataConnection) { + if (this._isEstablishingDataConnection) { this.emit('dataConnectionEstablished'); - this.isEstablishingDataConnection = false; + this._isEstablishingDataConnection = false; } const allOver = (ename) => { @@ -764,13 +790,14 @@ class FtpConnection extends EventEmitter { } __AUTH(commandArg) { - if (!this.server.options.tlsOptions || commandArg !== 'TLS') { + let {tlsOptions} = this; + if (!tlsOptions || commandArg !== 'TLS') { return this.respond('502 Command not implemented'); } this.respond('234 Honored', () => { this._log(LOG.INFO, 'Establishing secure connection...'); - starttls.starttlsServer(this.socket, this.server.options.tlsOptions, (err, cleartext) => { + starttls.starttlsServer(this.socket, tlsOptions, (err, cleartext) => { const switchToSecure = () => { this._log(LOG.INFO, 'Secure connection started'); this.socket = cleartext; @@ -843,7 +870,7 @@ class FtpConnection extends EventEmitter { // Get the feature list implemented by the server. (RFC 2389) __FEAT() { let features = ['SIZE', 'UTF8', 'MDTM']; - if (this.server.options.tlsOptions) { + if (this.tlsOptions) { features.push('AUTH TLS', 'PBSZ', 'PROT'); } features = features.map((feature) => ' ' + feature + '\r\n'); @@ -1037,10 +1064,19 @@ class FtpConnection extends EventEmitter { } __PORT(commandArg) { - var {host, port} = this._parsePORT(commandArg); + if (this._hasReceivedPASV || this._hasReceivedPORT) { + this.respond('503 Bad sequence of commands.'); + return; + } + this._hasReceivedPORT = true; + let {host, port} = decodeAddress(commandArg) || {}; + if (host == null || port == null) { + this.respond('501 Bad argument to PORT'); + return; + } this.dataHost = host; this.dataPort = port; - this._log(LOG.DEBUG, 'self.dataHost, self.dataPort set to ' + this.dataHost + ':' + this.dataPort); + this._log(LOG.DEBUG, `self.dataHost, self.dataPort set to ${host}:${port}`); this.respond('200 OK'); } @@ -1049,22 +1085,22 @@ class FtpConnection extends EventEmitter { } __PASV(commandArg, command) { - // Client already sent PASV. - if (this._hasReceivedDataConnectionRequest()) { - // TODO: Is this the right message here? + if (this._hasReceivedPASV || this._hasReceivedPORT) { this.respond('503 Bad sequence of commands.'); return; } - var isExtendedPASV = (command === 'EPSV'); + this._hasReceivedPASV = true; + let isExtendedPASV = (command === 'EPSV'); this._log(LOG.DEBUG, 'Setting up listener for passive connections'); - this.isEstablishingDataConnection = true; + this._isEstablishingDataConnection = true; // TODO: find a better way to get the address on which to bind listener. let host = this.server.host; this.passiveListenerPool.createDataConnection( host, + {useTLS: this.secure}, (error, dataConnection) => { if (error) { - this.isEstablishingDataConnection = false; + this._isEstablishingDataConnection = false; this._log(LOG.WARN, 'Error with passive data listener: ' + util.inspect(error)); this.respond('421 Server was unable to open passive connection listener'); return; @@ -1080,11 +1116,11 @@ class FtpConnection extends EventEmitter { } dataConnection.on('error', (error) => { - this.isEstablishingDataConnection = false; + this._isEstablishingDataConnection = false; // TODO: handle error }); dataConnection.on('ready', (socket) => { - this.isEstablishingDataConnection = false; + this._isEstablishingDataConnection = false; // TODO: inline this? or better yet, abstract the entire process of establishing a data connection. this._onPassiveDataConnection(socket); }); @@ -1105,7 +1141,7 @@ class FtpConnection extends EventEmitter { } __PBSZ(commandArg) { - if (!this.server.options.tlsOptions) { + if (!this.tlsOptions) { return this.respond('202 Not supported'); } @@ -1126,7 +1162,7 @@ class FtpConnection extends EventEmitter { } __PROT(commandArg) { - if (!this.server.options.tlsOptions) { + if (!this.tlsOptions) { return this.respond('202 Not supported'); } @@ -1254,9 +1290,9 @@ class FtpConnection extends EventEmitter { __USER(username) { if (this.server.options.tlsOnly && !this.secure) { this.respond( - '530 This server does not permit login over ' + - 'a non-secure connection; ' + - 'connect using FTP-SSL with explicit AUTH TLS'); + '530 This server does not permit login over a non-secure connection; ' + + 'connect using FTP-SSL with explicit AUTH TLS' + ); } else { this.emit( 'command:user', diff --git a/src/FtpServer.js b/src/FtpServer.js index a941ba9..76caceb 100644 --- a/src/FtpServer.js +++ b/src/FtpServer.js @@ -67,28 +67,9 @@ class FtpServer extends EventEmitter { server: this, socket: socket, passiveListenerPool: this.passiveListenerPool, - 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 - // it specified. (This can therefore be true even if there - // is not currently an open data connection.) - dataConfigured: false, - mode: 'ascii', - filefrom: '', - username: null, - filename: '', - fs: null, - cwd: null, - root: null, - hasQuit: false, - - // State for handling TLS upgrades. - secure: false, - pbszReceived: false, + // subset of allowed commands for this server + allowedCommands: allowedCommands, + tlsOptions: this.options.tlsOptions, }); this.emit('client:connected', conn); // pass client info so they can listen for client-specific events diff --git a/src/PassiveListenerPool.js b/src/PassiveListenerPool.js index 2dafd1b..38ca59d 100644 --- a/src/PassiveListenerPool.js +++ b/src/PassiveListenerPool.js @@ -3,6 +3,8 @@ import net from 'net'; import {EventEmitter} from 'events'; +import starttls from './starttls'; + const DEFAULT_OPTIONS = { BIND_ADDRESS: '0.0.0.0', MIN_PORT: 44001, @@ -13,9 +15,10 @@ const DEFAULT_OPTIONS = { const WAIT_TIMEOUT = 9000; export const CONNECTION_STATE = { - WAITING: 0, // Listener is waiting for client to connect (initial state). - CONNECTED: 1, // Client is connected. - CLOSED: 2, // Connection is closed (error or normal connection end). + WAITING: 0, // Listener is waiting for client to connect (initial state). + INITIALIZING_TLS: 1, // Client is connected but we are negotiating TLS. + READY: 2, // Client is connected and socket is ready. + CLOSED: 3, // Connection is closed (error or normal connection end). }; export const LISTENER_STATE = { @@ -31,13 +34,14 @@ let listenError = (errorCode, address, port) => ({ }); export class DataConnection extends EventEmitter { - constructor(port, remoteAddress) { + constructor(port, remoteAddress, options) { super(); // It's important to store the listening port here so the control connection // can send: 227 Entering Passive Mode (,) this.port = port; this.remoteAddress = remoteAddress; this.state = CONNECTION_STATE.WAITING; + this._useTLS = options.useTLS; this._socket = null; this._timer = setTimeout(() => { this._onError( @@ -56,16 +60,48 @@ export class DataConnection extends EventEmitter { // This is not really a public method, except for use from the code that // created this DataConnection (Listener). setSocket(socket) { - // Prevent calling twice. if (this._socket) { throw new Error('DataConnection: method setSocket() called more than once.'); } clearTimeout(this._timer); - this._socket = socket; - this.state = CONNECTION_STATE.CONNECTED; - socket.on('error', this._onError); - socket.on('close', this._close); - this.emit('ready', socket); + if (!this._useTLS) { + this._socket = socket; + this.state = CONNECTION_STATE.READY; + socket.on('error', this._onError); + socket.on('close', this._close); + this.emit('ready', socket); + return; + } + this.state = CONNECTION_STATE.INITIALIZING_TLS; + this._upgradeConnection(socket, (error, cleartext) => { + this._socket = cleartext; + this.state = CONNECTION_STATE.READY; + cleartext.on('error', this._onError); + cleartext.on('close', this._close); + this.emit('ready', cleartext); + }); + } + + _upgradeConnection(rawSocket, callback) { + // this._log(LOG.INFO, 'Upgrading passive connection to TLS'); + let {tlsOptions} = this.options; + starttls.starttlsServer(rawSocket, tlsOptions, (error, cleartext) => { + if (error) { + // this._log(LOG.ERROR, 'Error upgrading passive connection to TLS:' + util.inspect(error)); + this._closeSocket(rawSocket, true); + callback(error); + return; + } + + if (cleartext.authorized || this.options.allowUnauthorizedTls) { + // this._log(LOG.INFO, 'Allowing unauthorized connection (allowUnauthorizedTls is on)'); + // this._log(LOG.INFO, 'Passive connection secured'); + callback(null, cleartext); + } else { + // this._log(LOG.INFO, 'Closing unauthorized connection (allowUnauthorizedTls is off)'); + this._closeSocket(rawSocket, true); + } + }); } destroy() { @@ -105,10 +141,10 @@ export class Listener extends EventEmitter { this._onConnection = this._onConnection.bind(this); } - listenForClient(remoteAddress) { + listenForClient(remoteAddress, options) { let {bindAddress, port} = this; let key = port + '|' + remoteAddress; - let connection = new DataConnection(port, remoteAddress); + let connection = new DataConnection(port, remoteAddress, options); if (this._waitingConnections.has(key)) { // We cannot simultaneously have more than one waitingConnection for the // same remote address or it would create ambiguity (we wouldn't know @@ -116,10 +152,10 @@ export class Listener extends EventEmitter { // with). Treat this as an EADDRINUSE error to force the calling function // to try another port. process.nextTick(() => { - let e = listenError('EADDRINUSE', bindAddress, port); + let {message, props} = listenError('EADDRINUSE', bindAddress, port); connection.emit( 'listenerError', - Object.assign(new Error(e.message), e.props) + Object.assign(new Error(message), props) ); }); return connection; @@ -244,7 +280,7 @@ export default class PassiveListenerPool extends EventEmitter { this._listeners = new Map(); } - createDataConnection(remoteAddress, callback) { + createDataConnection(remoteAddress, options, callback) { let port = this._minPort; let dataConnection; let onError = (error) => { @@ -266,7 +302,7 @@ export default class PassiveListenerPool extends EventEmitter { listener = new Listener(port, this._bindAddress); this._listeners.set(port, listener); } - dataConnection = listener.listenForClient(remoteAddress); + dataConnection = listener.listenForClient(remoteAddress, options); dataConnection.on('listenerError', onError); dataConnection.on('listenerReady', onSuccess); }; diff --git a/src/__tests__/PassiveListenerPool-test.js b/src/__tests__/PassiveListenerPool-test.js index 57092f5..5d3793b 100644 --- a/src/__tests__/PassiveListenerPool-test.js +++ b/src/__tests__/PassiveListenerPool-test.js @@ -62,7 +62,8 @@ describe('PassiveListenerPool', () => { remoteAddress: '127.0.0.1', }); let remoteAddress = controlConnection.address().address; - listenerPool.createDataConnection(remoteAddress, (error, dataConnection) => { + let options = {secure: false}; + listenerPool.createDataConnection(remoteAddress, options, (error, dataConnection) => { let {port} = dataConnection; log(`listening on port ${port}`); expect(port).toBe(MIN_PORT); From 44414bb54a79821af0a36de220ae094bc7dc1684 Mon Sep 17 00:00:00 2001 From: Simon Sturmer Date: Tue, 2 Feb 2016 14:04:04 +0700 Subject: [PATCH 49/55] [wip] Fix some stuff --- src/ActiveDataConnection.js | 106 ++++++++++++++++++++++++++++++++ src/FtpConnection.js | 118 +++++++++++++++--------------------- src/FtpServer.js | 42 +++++++++++-- src/PassiveListenerPool.js | 14 ++--- 4 files changed, 198 insertions(+), 82 deletions(-) create mode 100644 src/ActiveDataConnection.js diff --git a/src/ActiveDataConnection.js b/src/ActiveDataConnection.js new file mode 100644 index 0000000..7f1a3c7 --- /dev/null +++ b/src/ActiveDataConnection.js @@ -0,0 +1,106 @@ +/* @//flow */ + +import net from 'net'; +import {EventEmitter} from 'events'; + +import starttls from './starttls'; + +export const CONNECTION_STATE = { + INITIALIZING: 0, // Connection is not yet ready (initial state). + INITIALIZING_TLS: 1, // Client is connected but we are negotiating TLS. + READY: 2, // Client is connected and socket is ready. + CLOSED: 3, // Connection is closed (error or normal connection end). +}; + +export const LISTENER_STATE = { + INITIALIZING: 0, // Initial state. + LISTENING: 1, // Listener is waiting for client to connect. + CLOSED: 2, // Listener has stopped listening (connections may still exist). +}; + +export class ActiveDataConnection extends EventEmitter { + constructor(port, remoteAddress, options) { + super(); + // It's important to store the listening port here so the control connection + // can send: 227 Entering Passive Mode (,) + this.port = port; + this.remoteAddress = remoteAddress; + this.state = CONNECTION_STATE.WAITING; + this._useTLS = options.useTLS; + this._socket = null; + // Auto-bind methods. + this._onError = this._onError.bind(this); + this._close = this._close.bind(this); + } + + getSocket() { + return this._socket; + } + + // This is not really a public method, except for use from the code that + // created this instance. + setSocket(socket) { + if (this._socket) { + throw new Error('DataConnection: method setSocket() called more than once.'); + } + if (!this._useTLS) { + this._socket = socket; + this.state = CONNECTION_STATE.READY; + socket.on('error', this._onError); + socket.on('close', this._close); + this.emit('ready', socket); + return; + } + this.state = CONNECTION_STATE.INITIALIZING_TLS; + this._upgradeConnection(socket, (error, cleartext) => { + this._socket = cleartext; + this.state = CONNECTION_STATE.READY; + cleartext.on('error', this._onError); + cleartext.on('close', this._close); + this.emit('ready', cleartext); + }); + } + + _upgradeConnection(rawSocket, callback) { + // this._log(LOG.INFO, 'Upgrading passive connection to TLS'); + let {tlsOptions} = this.options; + starttls.starttlsServer(rawSocket, tlsOptions, (error, cleartext) => { + if (error) { + // this._log(LOG.ERROR, 'Error upgrading passive connection to TLS:' + util.inspect(error)); + this._closeSocket(rawSocket, true); + callback(error); + return; + } + + if (cleartext.authorized || this.options.allowUnauthorizedTls) { + // this._log(LOG.INFO, 'Allowing unauthorized connection (allowUnauthorizedTls is on)'); + // this._log(LOG.INFO, 'Passive connection secured'); + callback(null, cleartext); + } else { + // this._log(LOG.INFO, 'Closing unauthorized connection (allowUnauthorizedTls is off)'); + this._closeSocket(rawSocket, true); + } + }); + } + + destroy() { + if (this._socket) { + this._socket.destroy(); // Will automatically emit `close`; + } else { + this._close(); + } + } + + _onError(error) { + this.emit('error', error); + process.nextTick(this._close); + } + + _close() { + if (this.state === CONNECTION_STATE.CLOSED) { + return; + } + this.state = CONNECTION_STATE.CLOSED; + this.emit('close'); + } +} diff --git a/src/FtpConnection.js b/src/FtpConnection.js index addb472..c6a8639 100644 --- a/src/FtpConnection.js +++ b/src/FtpConnection.js @@ -359,75 +359,39 @@ class FtpConnection extends EventEmitter { return {host: m[1], port: r}; } - // TODO: move this to PassiveListenerPool - _upgradePassiveConnectionToTLS(socket, callback) { - this._log(LOG.INFO, 'Upgrading passive connection to TLS'); - starttls.starttlsServer(socket, this.tlsOptions, (err, cleartext) => { - const switchToSecure = () => { - this._log(LOG.INFO, 'Passive connection secured'); - // TODO: Check for existing dataSocket. - this.dataSocket = cleartext; - callback(); - }; - if (err) { - this._log(LOG.ERROR, 'Error upgrading passive connection to TLS:' + util.inspect(err)); - this._closeSocket(socket, true); - } else if (!cleartext.authorized) { - if (this.server.options.allowUnauthorizedTls) { - this._log(LOG.INFO, 'Allowing unauthorized passive connection (allowUnauthorizedTls is on)'); - switchToSecure(); - } else { - this._log(LOG.INFO, 'Closing unauthorized passive connection (allowUnauthorizedTls is off)'); - this._closeSocket(this.socket, true); - } - } else { - switchToSecure(); - } - }); - } - - // TODO: fix some stuff. - _onPassiveDataConnection(socket) { - // TODO: reword - this._log(LOG.INFO, 'Passive data event: connect'); - - const setupPassiveListener = () => { - if (this._isEstablishingDataConnection) { - this.emit('dataConnectionEstablished'); - this._isEstablishingDataConnection = false; - } - - const allOver = (ename) => { - return (error) => { - this._log( - error ? LOG.ERROR : LOG.DEBUG, - 'Passive data event: ' + ename + (error ? ' due to error' : '') - ); - this.dataSocket = null; - }; - }; + // TODO: this should work the same for active or passive. + _onDataConnection(socket) { + this.dataSocket = socket; + if (this._isEstablishingDataConnection) { + this.emit('dataConnectionEstablished'); + this._isEstablishingDataConnection = false; + } - // 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. - this.dataSocket.on('close', allOver('close')); - this.dataSocket.on('end', allOver('end')); + socket.on('error', (err) => { + this._log(LOG.ERROR, 'Data socket event: error: ' + err); + // TODO: Can we can rely on self.dataSocket having been closed? + this.dataSocket = null; + }); - this.dataSocket.on('error', (err) => { - this._log(LOG.ERROR, 'Passive data event: error: ' + err); - // TODO: Can we can rely on self.dataSocket having been closed? + const allOver = (name) => { + return (error) => { + // Prevent calling twice. + if (this.dataSocket == null) { + return; + } + this._log( + error ? LOG.ERROR : LOG.DEBUG, + 'Data socket event: ' + name + (error ? ' due to error' : '') + ); this.dataSocket = null; - }); + }; }; - // TODO: remove this. - if (this.secure) { - this._upgradePassiveConnectionToTLS(socket, setupPassiveListener); - } else { - // TODO: Check for existing dataSocket. - this.dataSocket = socket; - setupPassiveListener(); - } + // 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')); } _retrieveUsingCreateReadStream(commandArg, filename) { @@ -1084,6 +1048,23 @@ class FtpConnection extends EventEmitter { this.__PORT(commandArg, command); } + _getDataConnection() { + const DATA_CONNECTION_STATE = {}; + class DataConnection { + constructor(ftpConnection) { + this.state = DATA_CONNECTION_STATE.NOT_READY; + this.isActive = false; + this.isPassive = false; + this._ftpConnection = ftpConnection; + } + setActivePort(port) { + this.isActive = true; + this._activePort = port; + } + } + return new DataConnection(); + } + __PASV(commandArg, command) { if (this._hasReceivedPASV || this._hasReceivedPORT) { this.respond('503 Bad sequence of commands.'); @@ -1114,15 +1095,14 @@ class FtpConnection extends EventEmitter { } else { this.respond(`227 Entering Passive Mode (${encodeAddress(host, port)})`); } - dataConnection.on('error', (error) => { - this._isEstablishingDataConnection = false; + this._log(LOG.WARN, 'Error with passive data connection: ' + util.inspect(error)); // TODO: handle error }); dataConnection.on('ready', (socket) => { - this._isEstablishingDataConnection = false; - // TODO: inline this? or better yet, abstract the entire process of establishing a data connection. - this._onPassiveDataConnection(socket); + // TODO: reword + this._log(LOG.INFO, 'Passive data event: connect'); + this._onDataConnection(socket); }); dataConnection.on('close', () => { // TODO: reword. diff --git a/src/FtpServer.js b/src/FtpServer.js index 76caceb..feb12b9 100644 --- a/src/FtpServer.js +++ b/src/FtpServer.js @@ -2,6 +2,7 @@ import net from 'net'; import events from 'events'; import FtpConnection from './FtpConnection'; import Constants from './Constants'; +import PassiveListenerPool from './PassiveListenerPool'; var {EventEmitter} = events; @@ -47,10 +48,6 @@ class FtpServer extends EventEmitter { this.server.on('close', () => { this.emit('close'); }); - this.passiveListenerPool = new PassiveListenerPool({ - bindAddress: BIND_ADDRESS, - portRange: [options.pasvPortRangeStart, options.pasvPortRangeEnd], - }); } _onConnection(socket) { @@ -111,8 +108,41 @@ class FtpServer extends EventEmitter { } } - listen() { - this.server.listen(...arguments); + _getHost(arg1, arg2) { + let host; + let NUMERIC = /^[0-9]+$/; + let isObject = (arg1 === Object(arg1)); + let isNumeric = (typeof arg1 === 'number' || (typeof arg1 === 'string' || NUMERIC.test(arg1))) + if (isObject) { + host = arg1.host; + } else if (isNumeric && typeof arg2 === 'string') { + host = arg2; + } + if (host == null) { + host = '127.0.0.1'; + } + return host; + } + + listen(port, ...args) { + let NUMERIC = /^[0-9]+$/; + if (typeof port === 'string' && NUMERIC.test(port)) { + port = parseInt(port, 10); + } + if (typeof port !== 'number') { + throw new Error('Must specify port'); + } + let {host} = this; + let callback = (typeof args[args.length] === 'function') ? args.pop() : null; + this.server.listen(port, host, callback); + let {options} = this; + this.passiveListenerPool = new PassiveListenerPool({ + bindAddress: host, + portRange: [options.pasvPortRangeStart, options.pasvPortRangeEnd], + logger: (verbosity, message) => { + this._log(verbosity, '[PASV Listener Pool] >> ' + message); + }, + }); } close() { diff --git a/src/PassiveListenerPool.js b/src/PassiveListenerPool.js index 38ca59d..e1a75f6 100644 --- a/src/PassiveListenerPool.js +++ b/src/PassiveListenerPool.js @@ -33,7 +33,7 @@ let listenError = (errorCode, address, port) => ({ props: {code: errorCode, address, port}, }); -export class DataConnection extends EventEmitter { +export class PassiveDataConnection extends EventEmitter { constructor(port, remoteAddress, options) { super(); // It's important to store the listening port here so the control connection @@ -58,10 +58,10 @@ export class DataConnection extends EventEmitter { } // This is not really a public method, except for use from the code that - // created this DataConnection (Listener). + // created this instance (Listener). setSocket(socket) { if (this._socket) { - throw new Error('DataConnection: method setSocket() called more than once.'); + throw new Error('PassiveDataConnection: method setSocket() called more than once.'); } clearTimeout(this._timer); if (!this._useTLS) { @@ -144,13 +144,13 @@ export class Listener extends EventEmitter { listenForClient(remoteAddress, options) { let {bindAddress, port} = this; let key = port + '|' + remoteAddress; - let connection = new DataConnection(port, remoteAddress, options); + let connection = new PassiveDataConnection(port, remoteAddress, options); if (this._waitingConnections.has(key)) { // We cannot simultaneously have more than one waitingConnection for the // same remote address or it would create ambiguity (we wouldn't know - // which DataConnection instance to associate the incoming connection - // with). Treat this as an EADDRINUSE error to force the calling function - // to try another port. + // which instance to associate the incoming connection with). Treat this + // as an EADDRINUSE error to force the calling function to try another + // port. process.nextTick(() => { let {message, props} = listenError('EADDRINUSE', bindAddress, port); connection.emit( From fff660d4d15050316f4a42630bdf46e3b82af1ab Mon Sep 17 00:00:00 2001 From: Simon Sturmer Date: Thu, 4 Feb 2016 06:47:00 +0700 Subject: [PATCH 50/55] [wip] some work on data connections --- src/FtpConnection.js | 81 ++++++++++++++++++++------------------ src/FtpServer.js | 2 +- src/PassiveListenerPool.js | 30 +++++++++----- 3 files changed, 63 insertions(+), 50 deletions(-) diff --git a/src/FtpConnection.js b/src/FtpConnection.js index c6a8639..8ba2739 100644 --- a/src/FtpConnection.js +++ b/src/FtpConnection.js @@ -56,6 +56,7 @@ class FtpConnection extends EventEmitter { this.allowedCommands = options.allowedCommands; this.tlsOptions = options.tlsOptions; + // dataPort and dataHost are for active data connections (server connecting to client) // TODO: I don't think this should be 20. this.dataPort = 20; this.dataHost = null; @@ -131,32 +132,35 @@ class FtpConnection extends EventEmitter { // TODO: Better to check which mode we are (Active or Passive) and then act accordingly. if (this._isEstablishingDataConnection) { // TODO: reword. - this._log(LOG.DEBUG, 'Currently no data connection; expecting client to connect to pasv server shortly...'); + this._log(LOG.DEBUG, 'Currently no data connection; expecting client to connect shortly...'); this.on('dataConnectionEstablished', () => { this._log(LOG.DEBUG, '...client has connected now'); let socket = this.dataConnection.getSocket(); callback(socket); }); - } else if (this.dataConnection) { + return + } + if (this.dataConnection) { this._log(LOG.DEBUG, 'A data connection exists'); let socket = this.dataConnection.getSocket(); - process.nextTick(() => { - callback(socket); - }); - } else { - // This makes a connection to the client (Active mode). - this._initiateData((socket) => { - callback(socket); - }); + process.nextTick(() => callback(socket)); + return; + } + // At this point we know to use Active Mode. + let existingSocket = this.dataSocket; + if (existingSocket) { + process.nextTick(() => callback(existingSocket)); + return; } + // This makes a connection to the client (Active mode). + this._initiateActiveDataConnection((socket) => { + callback(socket); + }); } // This makes a connection to the client (Active mode). // TODO: fix some stuff here. - _initiateData(callback) { - if (this.dataSocket) { - return callback(this.dataSocket); - } + _initiateActiveDataConnection(callback) { var socket = net.connect(this.dataPort, this.dataHost || this.socket.remoteAddress); socket.on('connect', () => { this.dataSocket = socket; @@ -166,7 +170,7 @@ class FtpConnection extends EventEmitter { this.dataSocket = null; this._log( err ? LOG.ERROR : LOG.DEBUG, - 'Non-passive data connection ended' + (err ? 'due to error: ' + util.inspect(err) : '') + 'Active data connection ended' + (err ? 'due to error: ' + util.inspect(err) : '') ); }; // TODO: allOver will get executed twice here. @@ -261,7 +265,7 @@ class FtpConnection extends EventEmitter { } _listFiles(fileInfos, isDetailed, command) { - const whenReady = (listconn) => { + const whenReady = (dataSocket) => { const success = (err) => { if (err) { this.respond('550 Error listing files'); @@ -269,7 +273,7 @@ class FtpConnection extends EventEmitter { this.respond(END_MSGS[command]); } if (command !== 'STAT') { - this._closeSocket(listconn); + this._closeSocket(dataSocket); } }; @@ -305,7 +309,7 @@ class FtpConnection extends EventEmitter { line += '\r\n'; } this._writeText( - listconn, + dataSocket, line, (i === fileInfos.length - 1 ? success : undefined) ); @@ -361,11 +365,15 @@ class FtpConnection extends EventEmitter { // TODO: this should work the same for active or passive. _onDataConnection(socket) { + if (this.dataSocket) { + // TODO: better handle this. + throw new Error('Data socket already exists.'); + } this.dataSocket = socket; if (this._isEstablishingDataConnection) { - this.emit('dataConnectionEstablished'); this._isEstablishingDataConnection = false; } + this.emit('dataConnectionEstablished'); socket.on('error', (err) => { this._log(LOG.ERROR, 'Data socket event: error: ' + err); @@ -427,7 +435,7 @@ class FtpConnection extends EventEmitter { } } else { afterOk(() => { - this._whenDataReady((pasvconn) => { + this._whenDataReady((dataSocket) => { var readLength = 0; var now = new Date(); var rs = this.fs.createReadStream(null, {fd: fd}); @@ -463,7 +471,7 @@ class FtpConnection extends EventEmitter { this.respond('226 Closing data connection, sent ' + readLength + ' bytes'); }); - rs.pipe(pasvconn); + rs.pipe(dataSocket); rs.resume(); }); }); @@ -504,11 +512,11 @@ class FtpConnection extends EventEmitter { } } else { afterOk(() => { - this._whenDataReady((pasvconn) => { + this._whenDataReady((dataSocket) => { contents = {filename: filename, data: contents}; this.emit('file:retr:contents', contents); contents = contents.data; - pasvconn.write(contents); + dataSocket.write(contents); var contentLength = contents.length; this.respond('226 Closing data connection, sent ' + contentLength + ' bytes'); this.emit('file:retr', 'close', { @@ -520,7 +528,7 @@ class FtpConnection extends EventEmitter { duration: new Date() - startTime, errorState: false, }); - this._closeSocket(pasvconn); + this._closeSocket(dataSocket); }); }); } @@ -543,7 +551,7 @@ class FtpConnection extends EventEmitter { }); } - const handleUpload = (dataSocket) => { + this._whenDataReady((dataSocket) => { var isPaused = false; dataSocket.on('data', (buff) => { var result = storeStream.write(buff); @@ -570,21 +578,18 @@ class FtpConnection extends EventEmitter { storeStream.end(); } }); - }; - - this._whenDataReady(handleUpload); + }); storeStream.on('open', () => { this._log(LOG.DEBUG, 'File opened/created: ' + filename); - this._log(LOG.DEBUG, 'Told client ok to send file data'); // Adding event emitter for upload start time this.emit('file:stor', 'open', { user: this.username, file: filename, time: startTime, }); - this.respond('150 Ok to send data'); + this._log(LOG.DEBUG, 'Told client ok to send file data'); }); storeStream.on('error', () => { @@ -695,7 +700,7 @@ class FtpConnection extends EventEmitter { }); if (err) { erroredOut = true; - this._log(LOG.ERROR, 'Error writing file. ' + err); + this._log(LOG.ERROR, 'Error writing file.', err); if (this.dataSocket) { this._closeSocket(this.dataSocket, true); } @@ -714,14 +719,12 @@ class FtpConnection extends EventEmitter { erroredOut = true; }; - const handleUpload = () => { - this.dataSocket.on('data', dataHandler); - this.dataSocket.once('close', closeHandler); - this.dataSocket.once('error', errorHandler); - }; - this.respond('150 Ok to send data', () => { - this._whenDataReady(handleUpload); + this._whenDataReady((dataSocket) => { + dataSocket.on('data', dataHandler); + dataSocket.once('close', closeHandler); + dataSocket.once('error', errorHandler); + }); }); } @@ -1091,7 +1094,7 @@ class FtpConnection extends EventEmitter { this._log(LOG.DEBUG, `Listening for passive data connection on port ${port}`); // Now that we're successfully listening, tell the client. if (isExtendedPASV) { - this.respond('229 Entering Extended Passive Mode (|||' + port + '|)'); + this.respond(`229 Entering Extended Passive Mode (|||${port}|)`); } else { this.respond(`227 Entering Passive Mode (${encodeAddress(host, port)})`); } diff --git a/src/FtpServer.js b/src/FtpServer.js index feb12b9..3b57a9d 100644 --- a/src/FtpServer.js +++ b/src/FtpServer.js @@ -112,7 +112,7 @@ class FtpServer extends EventEmitter { let host; let NUMERIC = /^[0-9]+$/; let isObject = (arg1 === Object(arg1)); - let isNumeric = (typeof arg1 === 'number' || (typeof arg1 === 'string' || NUMERIC.test(arg1))) + let isNumeric = (typeof arg1 === 'number' || (typeof arg1 === 'string' || NUMERIC.test(arg1))); if (isObject) { host = arg1.host; } else if (isNumeric && typeof arg2 === 'string') { diff --git a/src/PassiveListenerPool.js b/src/PassiveListenerPool.js index e1a75f6..8608aa2 100644 --- a/src/PassiveListenerPool.js +++ b/src/PassiveListenerPool.js @@ -2,9 +2,12 @@ import net from 'net'; import {EventEmitter} from 'events'; +import Constants from './Constants'; import starttls from './starttls'; +const LOG = Constants.LOG_LEVELS; + const DEFAULT_OPTIONS = { BIND_ADDRESS: '0.0.0.0', MIN_PORT: 44001, @@ -34,13 +37,14 @@ let listenError = (errorCode, address, port) => ({ }); export class PassiveDataConnection extends EventEmitter { - constructor(port, remoteAddress, options) { + constructor(port, remoteAddress, options, logger) { super(); // It's important to store the listening port here so the control connection // can send: 227 Entering Passive Mode (,) this.port = port; this.remoteAddress = remoteAddress; this.state = CONNECTION_STATE.WAITING; + this._log = logger; this._useTLS = options.useTLS; this._socket = null; this._timer = setTimeout(() => { @@ -83,22 +87,21 @@ export class PassiveDataConnection extends EventEmitter { } _upgradeConnection(rawSocket, callback) { - // this._log(LOG.INFO, 'Upgrading passive connection to TLS'); + this._log(LOG.INFO, 'Upgrading connection to TLS'); let {tlsOptions} = this.options; starttls.starttlsServer(rawSocket, tlsOptions, (error, cleartext) => { if (error) { - // this._log(LOG.ERROR, 'Error upgrading passive connection to TLS:' + util.inspect(error)); + this._log(LOG.ERROR, 'Error upgrading connection to TLS', error); this._closeSocket(rawSocket, true); callback(error); return; } - if (cleartext.authorized || this.options.allowUnauthorizedTls) { - // this._log(LOG.INFO, 'Allowing unauthorized connection (allowUnauthorizedTls is on)'); - // this._log(LOG.INFO, 'Passive connection secured'); + this._log(LOG.INFO, 'Allowing unauthorized connection (allowUnauthorizedTls is on)'); + this._log(LOG.INFO, 'Connection secured'); callback(null, cleartext); } else { - // this._log(LOG.INFO, 'Closing unauthorized connection (allowUnauthorizedTls is off)'); + this._log(LOG.INFO, 'Closing unauthorized connection (allowUnauthorizedTls is off)'); this._closeSocket(rawSocket, true); } }); @@ -127,10 +130,11 @@ export class PassiveDataConnection extends EventEmitter { } export class Listener extends EventEmitter { - constructor(port, bindAddress) { + constructor(port, bindAddress, logger) { super(); this.port = port; this.bindAddress = bindAddress; + this._log = logger; this.state = LISTENER_STATE.CLOSED; this._waitingConnections = new Map(); this._allConnections = new Set(); @@ -144,7 +148,12 @@ export class Listener extends EventEmitter { listenForClient(remoteAddress, options) { let {bindAddress, port} = this; let key = port + '|' + remoteAddress; - let connection = new PassiveDataConnection(port, remoteAddress, options); + let connection = new PassiveDataConnection( + port, + remoteAddress, + options, + this._log + ); if (this._waitingConnections.has(key)) { // We cannot simultaneously have more than one waitingConnection for the // same remote address or it would create ambiguity (we wouldn't know @@ -277,6 +286,7 @@ export default class PassiveListenerPool extends EventEmitter { let portRange = options.portRange || []; this._minPort = portRange[0] || DEFAULT_OPTIONS.MIN_PORT; this._maxPort = portRange[1] || DEFAULT_OPTIONS.MAX_PORT; + this._log = options.logger || () => {}; this._listeners = new Map(); } @@ -299,7 +309,7 @@ export default class PassiveListenerPool extends EventEmitter { let startListener = () => { let listener = this._listeners.get(port); if (listener == null) { - listener = new Listener(port, this._bindAddress); + listener = new Listener(port, this._bindAddress, this._log); this._listeners.set(port, listener); } dataConnection = listener.listenForClient(remoteAddress, options); From 9671610a32e92361a9aed4ee1e891f23552d64e3 Mon Sep 17 00:00:00 2001 From: Simon Sturmer Date: Fri, 5 Feb 2016 08:35:53 +0700 Subject: [PATCH 51/55] remove `this.dataSocket` --- src/FtpConnection.js | 128 ++++++++++++++++++++----------------------- 1 file changed, 60 insertions(+), 68 deletions(-) diff --git a/src/FtpConnection.js b/src/FtpConnection.js index 8ba2739..def4327 100644 --- a/src/FtpConnection.js +++ b/src/FtpConnection.js @@ -62,8 +62,6 @@ class FtpConnection extends EventEmitter { this.dataHost = null; // The incoming (PASV) data connection. this.dataConnection = null; - // the actual data socket - this.dataSocket = null; this._isEstablishingDataConnection = false; this._hasReceivedPASV = false; this._hasReceivedPORT = false; @@ -128,6 +126,7 @@ class FtpConnection extends EventEmitter { } } + // TODO: this should call callback with first parameter `error`. _whenDataReady(callback) { // TODO: Better to check which mode we are (Active or Passive) and then act accordingly. if (this._isEstablishingDataConnection) { @@ -146,13 +145,8 @@ class FtpConnection extends EventEmitter { process.nextTick(() => callback(socket)); return; } - // At this point we know to use Active Mode. - let existingSocket = this.dataSocket; - if (existingSocket) { - process.nextTick(() => callback(existingSocket)); - return; - } - // This makes a connection to the client (Active mode). + // At this point we know to use Active Mode. Makes a connection + // to the client for data transfer. this._initiateActiveDataConnection((socket) => { callback(socket); }); @@ -163,11 +157,9 @@ class FtpConnection extends EventEmitter { _initiateActiveDataConnection(callback) { var socket = net.connect(this.dataPort, this.dataHost || this.socket.remoteAddress); socket.on('connect', () => { - this.dataSocket = socket; callback(socket); }); const allOver = (err) => { - this.dataSocket = null; this._log( err ? LOG.ERROR : LOG.DEBUG, 'Active data connection ended' + (err ? 'due to error: ' + util.inspect(err) : '') @@ -179,7 +171,6 @@ class FtpConnection extends EventEmitter { socket.on('error', (err) => { this._closeSocket(socket, true); this._log(LOG.ERROR, 'Data connection error: ' + util.inspect(err)); - this.dataSocket = null; }); } @@ -193,13 +184,8 @@ class FtpConnection extends EventEmitter { } _onClose(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; - } + // I feel like some of this might be redundant since we probably are doing + //this elsewhere. But it is fine to call _closeSocket more than once. if (this.socket) { this._closeSocket(this.socket, hadError); this.socket = null; @@ -365,11 +351,6 @@ class FtpConnection extends EventEmitter { // TODO: this should work the same for active or passive. _onDataConnection(socket) { - if (this.dataSocket) { - // TODO: better handle this. - throw new Error('Data socket already exists.'); - } - this.dataSocket = socket; if (this._isEstablishingDataConnection) { this._isEstablishingDataConnection = false; } @@ -377,21 +358,20 @@ class FtpConnection extends EventEmitter { socket.on('error', (err) => { this._log(LOG.ERROR, 'Data socket event: error: ' + err); - // TODO: Can we can rely on self.dataSocket having been closed? - this.dataSocket = null; }); const allOver = (name) => { + let finished = false; return (error) => { // Prevent calling twice. - if (this.dataSocket == null) { + if (finished) { return; } this._log( error ? LOG.ERROR : LOG.DEBUG, 'Data socket event: ' + name + (error ? ' due to error' : '') ); - this.dataSocket = null; + finished = true; }; }; @@ -536,22 +516,33 @@ class FtpConnection extends EventEmitter { } // 'initialBuffers' argument is set when this is called from _storeUsingWriteFile. - _storeUsingCreateWriteStream(filename, initialBuffers, flag) { + _storeUsingCreateWriteStream(filename, initialBuffers, flag, dataSocket) { var wStreamFlags = {flags: flag || 'w', mode: 0o644}; var storeStream = this.fs.createWriteStream(pathModule.join(this.root, filename), wStreamFlags); - var notErr = true; + var wasError = false; // Adding for event metadata for file upload (STOR) var startTime = new Date(); var uploadSize = 0; if (initialBuffers) { - //todo: handle back-pressure - initialBuffers.forEach((b) => { - storeStream.write(b); + // TODO: Handle back-pressure. + initialBuffers.forEach((b) => storeStream.write(b)); + } + + // If this is called from _storeUsingWriteFile (when the buffer got full), + // then we have a dataSocket that is already min-transfer. It is important + // we don't delay (do some async logic) before attaching event listeners or + // we might miss some data/events. + if (dataSocket) { + doStuff(dataSocket); + } else { + this._whenDataReady((socket) => { + dataSocket = socket; + doStuff(dataSocket); }); } - this._whenDataReady((dataSocket) => { + const doStuff = (dataSocket) => { var isPaused = false; dataSocket.on('data', (buff) => { var result = storeStream.write(buff); @@ -566,7 +557,7 @@ class FtpConnection extends EventEmitter { } }); dataSocket.once('error', () => { - notErr = false; + wasError = true; storeStream.end(); }); dataSocket.once('finish', () => { @@ -578,7 +569,7 @@ class FtpConnection extends EventEmitter { storeStream.end(); } }); - }); + }; storeStream.on('open', () => { this._log(LOG.DEBUG, 'File opened/created: ' + filename); @@ -600,18 +591,16 @@ class FtpConnection extends EventEmitter { sTime: startTime, eTime: new Date(), duration: new Date() - startTime, - errorState: !notErr, + errorState: wasError, }); storeStream.end(); - notErr = false; - if (this.dataSocket) { - this._closeSocket(this.dataSocket, true); - } + wasError = true; + // TODO: Close data socket. this.respond('426 Connection closed; transfer aborted'); }); storeStream.on('finish', () => { - // Adding event emitter for completed upload. + // Emit event for completed upload. this.emit('file:stor', 'close', { user: this.username, file: filename, @@ -619,20 +608,21 @@ class FtpConnection extends EventEmitter { sTime: startTime, eTime: new Date(), duration: new Date() - startTime, - errorState: !notErr, + errorState: wasError, }); - notErr ? this.respond('226 Closing data connection') : true; - if (this.dataSocket) { - this._closeSocket(this.dataSocket); + if (!wasError) { + this.respond('226 Closing data connection'); } + // TODO: Close data socket. }); } _storeUsingWriteFile(filename, flag) { - var erroredOut = false; + var wasError = false; var slurpBuf = new Buffer(1024); // TODO: Why is there a magic number here? var totalBytes = 0; var startTime = new Date(); + var dataSocket; this.emit('file:stor', 'open', { user: this.username, @@ -650,20 +640,24 @@ class FtpConnection extends EventEmitter { // If the 'fs' module we've been given doesn't implement 'createWriteStream', then // we give up and send the client an error. if (!this.fs.createWriteStream) { - if (this.dataSocket) { - this._closeSocket(this.dataSocket, true); - } this.respond('552 Requested file action aborted; file too big'); + dataSocket.destroy(); return; } // Otherwise, we call _STOR_usingWriteStream, and tell it to prepend the stuff // that we've buffered so far to the file. this._log(LOG.WARN, 'uploadMaxSlurpSize exceeded; falling back to createWriteStream'); - this._storeUsingCreateWriteStream(filename, [slurpBuf.slice(0, totalBytes), buf], flag); - this.dataSocket.removeListener('data', dataHandler); - this.dataSocket.removeListener('error', errorHandler); - this.dataSocket.removeListener('close', closeHandler); + // TODO: pause dataSocket first? + this._storeUsingCreateWriteStream( + filename, + [slurpBuf.slice(0, totalBytes), buf], + flag, + dataSocket + ); + dataSocket.removeListener('data', dataHandler); + dataSocket.removeListener('error', errorHandler); + dataSocket.removeListener('close', closeHandler); } else { if (totalBytes + buf.length > slurpBuf.length) { var newLength = slurpBuf.length * 2; @@ -681,10 +675,13 @@ class FtpConnection extends EventEmitter { }; const closeHandler = () => { - if (erroredOut) { + // TODO: Should this writeFile logic be done in the `end` event? That + // event will happen at the graceful end of a file transfer onlye, + // whereas the close event happens *always*. Including if there was an + // error (hence the `wasError` logic). + if (wasError) { return; } - var wOptions = {flag: flag || 'w', mode: 0o644}; var contents = {filename: filename, data: slurpBuf.slice(0, totalBytes)}; this.emit('file:stor:contents', contents); @@ -699,28 +696,23 @@ class FtpConnection extends EventEmitter { errorState: err ? true : false, }); if (err) { - erroredOut = true; + wasError = true; this._log(LOG.ERROR, 'Error writing file.', err); - if (this.dataSocket) { - this._closeSocket(this.dataSocket, true); - } this.respond('426 Connection closed; transfer aborted'); - return; - } - - this.respond('226 Closing data connection'); - if (this.dataSocket) { - this._closeSocket(this.dataSocket); + } else { + // Technically the connection is already closed at this point. + this.respond('226 Closing data connection'); } }); }; const errorHandler = () => { - erroredOut = true; + wasError = true; }; this.respond('150 Ok to send data', () => { - this._whenDataReady((dataSocket) => { + this._whenDataReady((socket) => { + dataSocket = socket; dataSocket.on('data', dataHandler); dataSocket.once('close', closeHandler); dataSocket.once('error', errorHandler); From f3a98d351e880366b0621e31dfc46ea7aa2334b2 Mon Sep 17 00:00:00 2001 From: Simon Sturmer Date: Sat, 6 Feb 2016 07:47:15 +0700 Subject: [PATCH 52/55] _storeUsingCreateWriteStream: change argument order --- src/FtpConnection.js | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/FtpConnection.js b/src/FtpConnection.js index def4327..2f1c3e7 100644 --- a/src/FtpConnection.js +++ b/src/FtpConnection.js @@ -516,7 +516,7 @@ class FtpConnection extends EventEmitter { } // 'initialBuffers' argument is set when this is called from _storeUsingWriteFile. - _storeUsingCreateWriteStream(filename, initialBuffers, flag, dataSocket) { + _storeUsingCreateWriteStream(filename, flag, initialBuffers, dataSocket) { var wStreamFlags = {flags: flag || 'w', mode: 0o644}; var storeStream = this.fs.createWriteStream(pathModule.join(this.root, filename), wStreamFlags); var wasError = false; @@ -525,7 +525,7 @@ class FtpConnection extends EventEmitter { var uploadSize = 0; if (initialBuffers) { - // TODO: Handle back-pressure. + // TODO: Use `writeToStreamAsync` (handle back-pressure). initialBuffers.forEach((b) => storeStream.write(b)); } @@ -651,8 +651,8 @@ class FtpConnection extends EventEmitter { // TODO: pause dataSocket first? this._storeUsingCreateWriteStream( filename, - [slurpBuf.slice(0, totalBytes), buf], flag, + [slurpBuf.slice(0, totalBytes), buf], dataSocket ); dataSocket.removeListener('data', dataHandler); @@ -1248,7 +1248,7 @@ class FtpConnection extends EventEmitter { if (this.server.options.useWriteFile) { this._storeUsingWriteFile(filename, 'w'); } else { - this._storeUsingCreateWriteStream(filename, null, 'w'); + this._storeUsingCreateWriteStream(filename, 'w'); } } @@ -1257,7 +1257,7 @@ class FtpConnection extends EventEmitter { if (this.server.options.useWriteFile) { this._storeUsingWriteFile(filename, 'a'); } else { - this._storeUsingCreateWriteStream(filename, null, 'a'); + this._storeUsingCreateWriteStream(filename, 'a'); } } From 04b0af462354cde66ac2ceb686e3bdca86bf7b74 Mon Sep 17 00:00:00 2001 From: Simon Sturmer Date: Sat, 6 Feb 2016 08:55:30 +0700 Subject: [PATCH 53/55] add flow to babel --- .babelrc | 4 +++- .flowconfig | 8 ++++++++ package.json | 2 ++ 3 files changed, 13 insertions(+), 1 deletion(-) create mode 100644 .flowconfig diff --git a/.babelrc b/.babelrc index d1a2f52..f05df50 100644 --- a/.babelrc +++ b/.babelrc @@ -24,6 +24,8 @@ "transform-es2015-template-literals", "transform-es2015-typeof-symbol", "transform-es2015-unicode-regex", - "transform-regenerator" + "transform-regenerator", + "transform-flow-strip-types", + "syntax-flow", ] } diff --git a/.flowconfig b/.flowconfig new file mode 100644 index 0000000..9d629f7 --- /dev/null +++ b/.flowconfig @@ -0,0 +1,8 @@ +[ignore] +.*node_modules/babel.* + +[include] + +[libs] + +[options] diff --git a/package.json b/package.json index 1db68f0..fffd0af 100644 --- a/package.json +++ b/package.json @@ -125,7 +125,9 @@ "async": "~0.1.15", "babel-cli": "^6.4.5", "babel-eslint": "^4.1.6", + "babel-plugin-syntax-flow": "^6.3.13", "babel-plugin-transform-class-properties": "^6.4.0", + "babel-plugin-transform-flow-strip-types": "^6.4.0", "babel-polyfill": "^6.3.14", "babel-preset-es2015": "^6.3.13", "babel-preset-stage-2": "^6.3.13", From aa8d529c1a6b37a4cbc5f051bc13e28b9f40dad6 Mon Sep 17 00:00:00 2001 From: Simon Sturmer Date: Mon, 8 Feb 2016 05:41:02 +0700 Subject: [PATCH 54/55] add writeToStreamAsync --- .../__tests__/writeToStreamAsync-test.js | 81 +++++++++++++++++++ src/helpers/writeToStreamAsync.js | 25 ++++++ 2 files changed, 106 insertions(+) create mode 100644 src/helpers/__tests__/writeToStreamAsync-test.js create mode 100644 src/helpers/writeToStreamAsync.js diff --git a/src/helpers/__tests__/writeToStreamAsync-test.js b/src/helpers/__tests__/writeToStreamAsync-test.js new file mode 100644 index 0000000..a60fc96 --- /dev/null +++ b/src/helpers/__tests__/writeToStreamAsync-test.js @@ -0,0 +1,81 @@ +/* @//flow */ +/*eslint-env node, mocha */ +const {describe, it} = global; +import writeToStreamAsync from '../writeToStreamAsync'; +import expect from 'expect'; + +class MockWriteStream { + // _data: Array; + // _bytesWritten: number; + // _isBufferFull: number; + // _internalBufferSize: number; + + constructor(bufferSize = 10) { + this._data = []; + this._bytesWritten = 0; + this._bytesFlushed = 0; + this._isBufferFull = 0; + this._internalBufferSize = bufferSize; + } + + write(data, callback) { + this._data.push(data); + this._bytesWritten += data.length; + if (this._isBufferFull || data.length > this._internalBufferSize) { + this._isBufferFull += 1; + setTimeout(() => { + this._bytesFlushed += data.length; + this._isBufferFull -= 1; + callback && callback(); + }, 20); + return false; + } else { + this._bytesFlushed += data.length; + callback && process.nextTick(callback); + return true; + } + } + + getBytesWritten() { + return this._bytesWritten; + } + + getBytesFlushed() { + return this._bytesFlushed; + } + + isBufferFull() { + return this._isBufferFull !== 0; + } +} + +describe('helpers/writeToStreamAsync', () => { + it('should handle empty array', (done) => { + let writeStream = new MockWriteStream(); + writeToStreamAsync([], writeStream, () => { + expect(writeStream.getBytesWritten()).toBe(0); + done(); + }); + }); + + it('should write data to stream', (done) => { + let writeStream = new MockWriteStream(10); + let data = [ + new Buffer('abcdefghijkl', 'utf8'), + new Buffer('abcdefghijkl', 'utf8'), + new Buffer('abcdefghijkl', 'utf8'), + ]; + let result = writeStream.write(new Buffer('a', 'utf8')); + expect(result).toBe(true); + result = writeStream.write(data.shift()); + expect(result).toBe(false); + expect(writeStream.isBufferFull()).toBe(true); + expect(writeStream.getBytesWritten()).toBe(13); + writeToStreamAsync(data, writeStream, () => { + expect(writeStream.isBufferFull()).toBe(false); + expect(writeStream.getBytesWritten()).toBe(37); + expect(writeStream.getBytesFlushed()).toBe(37); + done(); + }); + }); +}); diff --git a/src/helpers/writeToStreamAsync.js b/src/helpers/writeToStreamAsync.js new file mode 100644 index 0000000..6f48bb5 --- /dev/null +++ b/src/helpers/writeToStreamAsync.js @@ -0,0 +1,25 @@ +/* @flow */ + +type WriteStream = { + write: (data: Buffer) => boolean; + once: (name: string, handler: Function) => any; +}; + +// This will take an array of data and write it to a write stream, waiting +// for it to flush to the underlying socket. +export default function writeToStreamAsync( + buffers: Array, + writeStream: WriteStream, + callback: (error: ?Error) => any +) { + let index = 0; + const writeData = () => { + if (index >= buffers.length) { + callback(); + } else { + let data = buffers[index++]; + writeStream.write(data, writeData); + } + }; + process.nextTick(writeData); +} From d9b7e782b2c9d595457de3f08a9160a410252873 Mon Sep 17 00:00:00 2001 From: Simon Sturmer Date: Tue, 9 Feb 2016 06:11:05 +0700 Subject: [PATCH 55/55] [wip] updates to FtpConnection --- src/FtpConnection.js | 109 ++++++++++++++++++++++--------------------- 1 file changed, 55 insertions(+), 54 deletions(-) diff --git a/src/FtpConnection.js b/src/FtpConnection.js index 2f1c3e7..76697e7 100644 --- a/src/FtpConnection.js +++ b/src/FtpConnection.js @@ -14,6 +14,7 @@ import pathEscape from './helpers/pathEscape'; import withCwd from './helpers/withCwd'; import stripOptions from './helpers/stripOptions'; import leftPad from './helpers/leftPad'; +import writeToStreamAsync from './helpers/writeToStreamAsync'; const ENCODED_ADDRESS = /^[0-9]{1,3}(,[0-9]{1,3}){5}$/; @@ -515,62 +516,15 @@ class FtpConnection extends EventEmitter { }); } - // 'initialBuffers' argument is set when this is called from _storeUsingWriteFile. - _storeUsingCreateWriteStream(filename, flag, initialBuffers, dataSocket) { - var wStreamFlags = {flags: flag || 'w', mode: 0o644}; - var storeStream = this.fs.createWriteStream(pathModule.join(this.root, filename), wStreamFlags); + // `initialBuffers` is used when called from _storeUsingWriteFile. + _storeUsingCreateWriteStream(filename, flags, initialBuffers, dataSocket) { + var wOptions = {flags: flags || 'w', mode: 0o644}; + var storeStream = this.fs.createWriteStream(pathModule.join(this.root, filename), wOptions); var wasError = false; // Adding for event metadata for file upload (STOR) var startTime = new Date(); var uploadSize = 0; - if (initialBuffers) { - // TODO: Use `writeToStreamAsync` (handle back-pressure). - initialBuffers.forEach((b) => storeStream.write(b)); - } - - // If this is called from _storeUsingWriteFile (when the buffer got full), - // then we have a dataSocket that is already min-transfer. It is important - // we don't delay (do some async logic) before attaching event listeners or - // we might miss some data/events. - if (dataSocket) { - doStuff(dataSocket); - } else { - this._whenDataReady((socket) => { - dataSocket = socket; - doStuff(dataSocket); - }); - } - - const doStuff = (dataSocket) => { - var isPaused = false; - dataSocket.on('data', (buff) => { - var result = storeStream.write(buff); - // Handle back-pressure - if (result === false) { - dataSocket.pause(); - isPaused = true; - storeStream.once('drain', () => { - dataSocket.resume(); - isPaused = false; - }); - } - }); - dataSocket.once('error', () => { - wasError = true; - storeStream.end(); - }); - dataSocket.once('finish', () => { - if (isPaused) { - storeStream.once('drain', () => { - storeStream.end(); - }); - } else { - storeStream.end(); - } - }); - }; - storeStream.on('open', () => { this._log(LOG.DEBUG, 'File opened/created: ' + filename); // Adding event emitter for upload start time @@ -615,9 +569,54 @@ class FtpConnection extends EventEmitter { } // TODO: Close data socket. }); + + const pipeFromSocket = () => { + var isBufferFull = false; + dataSocket.on('data', (data) => { + var result = storeStream.write(data); + // Handle back-pressure + if (result === false) { + isBufferFull = true; + dataSocket.pause(); + storeStream.once('drain', () => { + isBufferFull = false; + dataSocket.resume(); + }); + } + }); + dataSocket.once('error', () => { + wasError = true; + storeStream.end(); + }); + dataSocket.once('finish', () => { + if (isBufferFull) { + storeStream.once('drain', () => { + storeStream.end(); + }); + } else { + storeStream.end(); + } + }); + }; + + // If this is called from _storeUsingWriteFile (slurp size exceeded), + // then we have a dataSocket that is already mid-transfer. + if (initialBuffers) { + dataSocket.pause(); + writeToStreamAsync(initialBuffers, storeStream, () => { + pipeFromSocket(); + dataSocket.resume(); + }); + } else { + this._whenDataReady((socket) => { + dataSocket = socket; + pipeFromSocket(); + }); + } + } - _storeUsingWriteFile(filename, flag) { + _storeUsingWriteFile(filename, flags) { var wasError = false; var slurpBuf = new Buffer(1024); // TODO: Why is there a magic number here? var totalBytes = 0; @@ -651,7 +650,7 @@ class FtpConnection extends EventEmitter { // TODO: pause dataSocket first? this._storeUsingCreateWriteStream( filename, - flag, + flags, [slurpBuf.slice(0, totalBytes), buf], dataSocket ); @@ -682,7 +681,9 @@ class FtpConnection extends EventEmitter { if (wasError) { return; } - var wOptions = {flag: flag || 'w', mode: 0o644}; + // This is not a typo: `writeFile` uses `flag`, but `createWriteStream` + // uses `flags`. + var wOptions = {flag: flags || 'w', mode: 0o644}; var contents = {filename: filename, data: slurpBuf.slice(0, totalBytes)}; this.emit('file:stor:contents', contents); this.fs.writeFile(pathModule.join(this.root, filename), contents.data, wOptions, (err) => {