From e6842c3729ef057bf63126bc0e1fc70aa4582c3f Mon Sep 17 00:00:00 2001 From: Jan Kryl Date: Fri, 6 Jan 2017 08:32:45 +0100 Subject: [PATCH] Incomplete file when uploaded over TLS data connection --- README.md | 3 +- lib/FtpConnection.js | 20 +++++--- lib/FtpServer.js | 6 +++ lib/starttls.js | 117 +++++++------------------------------------ 4 files changed, 39 insertions(+), 107 deletions(-) diff --git a/README.md b/README.md index f83ef2a..9964350 100644 --- a/README.md +++ b/README.md @@ -163,9 +163,10 @@ The user is not able to escape this directory. - `tlsOptions`: _(default: undefined)_ - If this is set, the server will allow explicit TLS authentication. - Value should be a dictionary which is suitable as the `options` argument of `tls.createServer`. + - `tlsOptions.requestCert` can be set to true to require a certificate from clients. + - `tlsOptions.rejectUnauthorized` is ignored unless `tlsOptions.requestCert` is true and if it is set then only clients with certificates signed by trusted CA are allowed to connect. - `tlsOnly`: _(default: false)_ - If this is set to `true`, and `tlsOptions` is also set, then the server will not allow logins over non-secure connections. -- `allowUnauthorizedTls`: ?? I obviously set this to true when tlsOnly is on -someone needs to update this. - `pasvPortRangeStart`: _(default: random?)_ - Integer, specifies the lower-bound port (min port) for creating PASV connections - `pasvPortRangeEnd`: _(default: random?)_ diff --git a/lib/FtpConnection.js b/lib/FtpConnection.js index 828e1a8..439b3ce 100644 --- a/lib/FtpConnection.js +++ b/lib/FtpConnection.js @@ -87,15 +87,20 @@ FtpConnection.prototype._createPassiveServer = function() { 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)'); + if (!self.server.options.tlsOptions.rejectUnauthorized) { + self._logIf(LOG.INFO, 'Allowing unauthorized passive connection (tlsOptions.rejectUnauthorized is off)'); switchToSecure(); } else { - self._logIf(LOG.INFO, 'Closing unauthorized passive connection (allowUnauthorizedTls is off)'); + self._logIf(LOG.INFO, 'Closing unauthorized passive connection (tlsOptions.rejectUnauthorized is on)'); self._closeSocket(self.socket, true); self.dataConfigured = false; } } else { + // TODO: Verify that cert used for data connection is the same as + // the one used for control connection. + // TODO: Implement callback for client identity check based on its + // certificate, which would verify that client's certificate is not + // only valid, but also appropriate for that particular client. switchToSecure(); } @@ -317,14 +322,17 @@ FtpConnection.prototype._command_AUTH = function(commandArg) { 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)'); + if (!self.server.options.tlsOptions.rejectUnauthorized) { + self._logIf(LOG.INFO, 'Allowing unauthorized connection (tlsOptions.rejectUnauthorized is off)'); switchToSecure(); } else { - self._logIf(LOG.INFO, 'Closing unauthorized connection (allowUnauthorizedTls is off)'); + self._logIf(LOG.INFO, 'Closing unauthorized connection (tlsOptions.rejectUnauthorized is on)'); self._closeSocket(self.socket, true); } } else { + // TODO: Implement callback for client identity check based on its + // certificate, which would verify that client's certificate is not + // only valid, but also appropriate for that particular client. switchToSecure(); } diff --git a/lib/FtpServer.js b/lib/FtpServer.js index 2ff206e..6cf54fa 100644 --- a/lib/FtpServer.js +++ b/lib/FtpServer.js @@ -27,6 +27,12 @@ function FtpServer(host, options) { if (!options.getRoot) { throw new Error("'getRoot' option of FtpServer must be set"); } + if (options.tlsOptions) { + // rejectUnauthorized is meaningful only if there is a client certificate + if (!options.tlsOptions.requestCert) { + options.tlsOptions.rejectUnauthorized = false; + } + } self.getInitialCwd = options.getInitialCwd; self.getRoot = options.getRoot; diff --git a/lib/starttls.js b/lib/starttls.js index ede6b39..c657cd1 100644 --- a/lib/starttls.js +++ b/lib/starttls.js @@ -2,25 +2,7 @@ // https://github.com/andris9/rai/blob/master/lib/starttls.js // (This code is MIT licensed.) -// -// Target API: -// -// var s = require('net').createStream(25, 'smtp.example.com'); -// s.on('connect', function() { -// require('starttls')(s, options, function() { -// if (!s.authorized) { -// s.destroy(); -// return; -// } -// -// s.end("hello world\n"); -// }); -// }); -// -// - var tls = require('tls'); -var crypto = require('crypto'); // From Node docs for TLS module. var RECOMMENDED_CIPHERS = 'ECDHE-RSA-AES256-SHA:AES256-SHA:RC4-SHA:RC4:HIGH:!MD5:!aNULL:!EDH:!AESGCM'; @@ -33,111 +15,46 @@ function starttlsClient(socket, options, callback) { } function starttls(socket, options, callback, isServer) { - var sslcontext; - var opts = {}; + Object.keys(options).forEach(function(key) { opts[key] = options[key]; }); if (!opts.ciphers) { opts.ciphers = RECOMMENDED_CIPHERS; } + opts.isServer = isServer; + opts.secureContext = tls.createSecureContext(opts); - socket.removeAllListeners('data'); - if (tls.createSecureContext) { - sslcontext = tls.createSecureContext(opts); - } else { - sslcontext = crypto.createCredentials(opts); - } - var pair = tls.createSecurePair(sslcontext, isServer); - var cleartext = pipe(pair, socket); - + var secureSocket = new tls.TLSSocket(socket, opts); var erroredOut = false; - pair.on('secure', function() { + + // NodeJS documentation bug: secure vs secureConnect + // https://github.com/nodejs/node/issues/10555 + secureSocket.on('secure', function() { if (erroredOut) { - pair.end(); + secureSocket.end(); return; } - var verifyError = (pair._ssl || pair.ssl).verifyError(); - - if (verifyError) { - cleartext.authorized = false; - cleartext.authorizationError = verifyError; + var authError = secureSocket.ssl.verifyError(); + if (authError) { + secureSocket.authorized = false; + secureSocket.authorizationError = authError; } else { - cleartext.authorized = true; + secureSocket.authorized = true; } - - callback(null, cleartext); + callback(null, secureSocket); }); - pair.once('error', function(err) { + secureSocket.once('error', function(err) { if (!erroredOut) { erroredOut = true; callback(err); } }); - - cleartext._controlReleased = true; - pair; -} - -function forwardEvents(events, emitterSource, emitterDestination) { - var map = []; - - for (var i = 0, len = events.length; i < len; i++) { - var name = events[i]; - - var handler = forwardEvent.bind(emitterDestination, name); - - map.push(name); - emitterSource.on(name, handler); - } - - return map; -} - -function forwardEvent() { - this.emit.apply(this, arguments); -} - -function removeEvents(map, emitterSource) { - for (var i = 0, len = map.length; i < len; i++) { - emitterSource.removeAllListeners(map[i]); - } -} - -function pipe(pair, socket) { - pair.encrypted.pipe(socket); - socket.pipe(pair.encrypted); - - pair.fd = socket.fd; - - var cleartext = pair.cleartext; - - cleartext.socket = socket; - cleartext.encrypted = pair.encrypted; - cleartext.authorized = false; - - function onerror(e) { - if (cleartext._controlReleased) { - cleartext.emit('error', e); - } - } - - var map = forwardEvents(['timeout', 'end', 'close', 'drain', 'error'], socket, cleartext); - - function onclose() { - socket.removeListener('error', onerror); - socket.removeListener('close', onclose); - removeEvents(map, socket); - } - - socket.on('error', onerror); - socket.on('close', onclose); - - return cleartext; } exports.starttlsServer = starttlsServer; exports.starttlsClient = starttlsClient; exports.RECOMMENDED_CIPHERS = RECOMMENDED_CIPHERS; +