From 42acbf809b67ff5015e12f2df12515342b223261 Mon Sep 17 00:00:00 2001 From: Fedor Indutny Date: Tue, 24 Sep 2013 16:53:49 +0400 Subject: [PATCH] tls: wrap tls inside tls using legacy API Allow wrapping TLSSocket inside another TLSSocket, emulate it using SecurePair in legacy APIs. fix #6204 --- lib/_tls_legacy.js | 60 ++++++++++++- lib/_tls_wrap.js | 134 +++++++++++++++++++++--------- src/node_crypto.cc | 4 +- test/simple/test-tls-inception.js | 87 +++++++++++++++++++ 4 files changed, 242 insertions(+), 43 deletions(-) create mode 100644 test/simple/test-tls-inception.js diff --git a/lib/_tls_legacy.js b/lib/_tls_legacy.js index 8c22ff4b9b3..17d193f2e84 100644 --- a/lib/_tls_legacy.js +++ b/lib/_tls_legacy.js @@ -34,7 +34,7 @@ try { throw new Error('node.js not compiled with openssl crypto support.'); } -var debug = util.debuglog('tls'); +var debug = util.debuglog('tls-legacy'); function SlabBuffer() { this.create(); @@ -820,3 +820,61 @@ SecurePair.prototype.error = function(returnOnly) { } return err; }; + + +exports.pipe = function pipe(pair, socket) { + pair.encrypted.pipe(socket); + socket.pipe(pair.encrypted); + + pair.encrypted.on('close', function() { + process.nextTick(function() { + // Encrypted should be unpiped from socket to prevent possible + // write after destroy. + pair.encrypted.unpipe(socket); + socket.destroy(); + }); + }); + + pair.fd = socket.fd; + var cleartext = pair.cleartext; + cleartext.socket = socket; + cleartext.encrypted = pair.encrypted; + cleartext.authorized = false; + + // cycle the data whenever the socket drains, so that + // we can pull some more into it. normally this would + // be handled by the fact that pipe() triggers read() calls + // on writable.drain, but CryptoStreams are a bit more + // complicated. Since the encrypted side actually gets + // its data from the cleartext side, we have to give it a + // light kick to get in motion again. + socket.on('drain', function() { + if (pair.encrypted._pending) + pair.encrypted._writePending(); + if (pair.cleartext._pending) + pair.cleartext._writePending(); + pair.encrypted.read(0); + pair.cleartext.read(0); + }); + + function onerror(e) { + if (cleartext._controlReleased) { + cleartext.emit('error', e); + } + } + + function onclose() { + socket.removeListener('error', onerror); + socket.removeListener('timeout', ontimeout); + } + + function ontimeout() { + cleartext.emit('timeout'); + } + + socket.on('error', onerror); + socket.on('close', onclose); + socket.on('timeout', ontimeout); + + return cleartext; +} diff --git a/lib/_tls_wrap.js b/lib/_tls_wrap.js index 32e9a257a94..3ffc804af39 100644 --- a/lib/_tls_wrap.js +++ b/lib/_tls_wrap.js @@ -29,6 +29,9 @@ var util = require('util'); var Timer = process.binding('timer_wrap').Timer; var tls_wrap = process.binding('tls_wrap'); +// Lazy load +var tls_legacy; + var debug = util.debuglog('tls'); function onhandshakestart() { @@ -145,6 +148,9 @@ function onnewsession(key, session) { */ function TLSSocket(socket, options) { + // Disallow wrapping TLSSocket in TLSSocket + assert(!(socket instanceof TLSSocket)); + net.Socket.call(this, socket && { handle: socket._handle, allowHalfOpen: socket.allowHalfOpen, @@ -645,6 +651,28 @@ function normalizeConnectArgs(listArgs) { return (cb) ? [options, cb] : [options]; } +function legacyConnect(hostname, options, NPN, credentials) { + assert(options.socket); + if (!tls_legacy) + tls_legacy = require('_tls_legacy'); + + var pair = tls_legacy.createSecurePair(credentials, + false, + true, + !!options.rejectUnauthorized, + { + NPNProtocols: NPN.NPNProtocols, + servername: hostname + }); + tls_legacy.pipe(pair, options.socket); + pair.cleartext._controlReleased = true; + pair.on('error', function(err) { + pair.cleartext.emit('error', err); + }); + + return pair; +} + exports.connect = function(/* [port, host], options, cb */) { var args = normalizeConnectArgs(arguments); var options = args[0]; @@ -656,34 +684,77 @@ exports.connect = function(/* [port, host], options, cb */) { options = util._extend(defaults, options || {}); var hostname = options.servername || options.host || 'localhost', - NPN = {}; + NPN = {}, + credentials = crypto.createCredentials(options); tls.convertNPNProtocols(options.NPNProtocols, NPN); - var socket = new TLSSocket(options.socket, { - credentials: crypto.createCredentials(options), - isServer: false, - requestCert: true, - rejectUnauthorized: options.rejectUnauthorized, - NPNProtocols: NPN.NPNProtocols - }); + // Wrapping TLS socket inside another TLS socket was requested - + // create legacy secure pair + var socket; + var legacy; + var result; + if (options.socket instanceof TLSSocket) { + debug('legacy connect'); + legacy = true; + socket = legacyConnect(hostname, options, NPN, credentials); + result = socket.cleartext; + } else { + legacy = false; + socket = new TLSSocket(options.socket, { + credentials: credentials, + isServer: false, + requestCert: true, + rejectUnauthorized: options.rejectUnauthorized, + NPNProtocols: NPN.NPNProtocols + }); + result = socket; + } + + if (socket._handle) + onHandle(); + else + socket.once('connect', onHandle); + + if (cb) + result.once('secureConnect', cb); + + if (!options.socket) { + assert(!legacy); + var connect_opt; + if (options.path && !options.port) { + connect_opt = { path: options.path }; + } else { + connect_opt = { + port: options.port, + host: options.host, + localAddress: options.localAddress + }; + }; + socket.connect(connect_opt); + } + + return result; function onHandle() { - socket._releaseControl(); + if (!legacy) + socket._releaseControl(); if (options.session) socket.setSession(options.session); - if (options.servername) - socket.setServername(options.servername); + if (!legacy) { + if (options.servername) + socket.setServername(options.servername); - socket._start(); + socket._start(); + } socket.on('secure', function() { var verifyError = socket.ssl.verifyError(); // Verify that server's identity matches it's certificate's names if (!verifyError) { - var validCert = tls.checkServerIdentity(hostname, - socket.getPeerCertificate()); + var cert = result.getPeerCertificate(); + var validCert = tls.checkServerIdentity(hostname, cert); if (!validCert) { verifyError = new Error('Hostname/IP doesn\'t match certificate\'s ' + 'altnames'); @@ -691,22 +762,23 @@ exports.connect = function(/* [port, host], options, cb */) { } if (verifyError) { - socket.authorizationError = verifyError.message; + result.authorized = false; + result.authorizationError = verifyError.message; if (options.rejectUnauthorized) { - socket.emit('error', verifyError); - socket.destroy(); + result.emit('error', verifyError); + result.destroy(); return; } else { - socket.emit('secureConnect'); + result.emit('secureConnect'); } } else { - socket.authorized = true; - socket.emit('secureConnect'); + result.authorized = true; + result.emit('secureConnect'); } // Uncork incoming data - socket.removeListener('end', onHangUp); + result.removeListener('end', onHangUp); }); function onHangUp() { @@ -719,24 +791,6 @@ exports.connect = function(/* [port, host], options, cb */) { socket.emit('error', error); } } - socket.once('end', onHangUp); - } - if (socket._handle) - onHandle(); - else - socket.once('connect', onHandle); - - if (cb) - socket.once('secureConnect', cb); - - if (!options.socket) { - var connect_opt = (options.path && !options.port) ? {path: options.path} : { - port: options.port, - host: options.host, - localAddress: options.localAddress - }; - socket.connect(connect_opt); + result.once('end', onHangUp); } - - return socket; }; diff --git a/src/node_crypto.cc b/src/node_crypto.cc index ee1b7db149a..6de073e6d1d 100644 --- a/src/node_crypto.cc +++ b/src/node_crypto.cc @@ -1689,14 +1689,14 @@ void Connection::New(const FunctionCallbackInfo& args) { SSL_CTX_set_next_protos_advertised_cb( sc->ctx_, SSLWrap::AdvertiseNextProtoCallback, - NULL); + conn); } else { // Client should select protocol from advertised // If server supports NPN SSL_CTX_set_next_proto_select_cb( sc->ctx_, SSLWrap::SelectNextProtoCallback, - NULL); + conn); } #endif diff --git a/test/simple/test-tls-inception.js b/test/simple/test-tls-inception.js new file mode 100644 index 00000000000..d15d1ebd43a --- /dev/null +++ b/test/simple/test-tls-inception.js @@ -0,0 +1,87 @@ +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +if (!process.versions.openssl) { + console.error('Skipping because node compiled without OpenSSL.'); + process.exit(0); +} + +var common = require('../common'); +var fs = require('fs'); +var path = require('path'); +var net = require('net'); +var tls = require('tls'); +var assert = require('assert'); + +var options, a, b, portA, portB; +var gotHello = false; + +options = { + key: fs.readFileSync(path.join(common.fixturesDir, 'test_key.pem')), + cert: fs.readFileSync(path.join(common.fixturesDir, 'test_cert.pem')) +}; + +// the "proxy" server +a = tls.createServer(options, function (socket) { + var options = { + host: '127.0.0.1', + port: b.address().port, + rejectUnauthorized: false + }; + var dest = net.connect(options); + dest.pipe(socket); + socket.pipe(dest); +}); + +// the "target" server +b = tls.createServer(options, function (socket) { + socket.end('hello'); +}); + +process.on('exit', function () { + assert(gotHello); +}); + +a.listen(common.PORT, function () { + b.listen(common.PORT + 1, function () { + options = { + host: '127.0.0.1', + port: a.address().port, + rejectUnauthorized: false + }; + var socket = tls.connect(options); + var ssl; + ssl = tls.connect({ + socket: socket, + rejectUnauthorized: false + }); + ssl.setEncoding('utf8'); + ssl.once('data', function (data) { + assert.equal('hello', data); + gotHello = true; + }); + ssl.on('end', function () { + ssl.end(); + a.close(); + b.close(); + }); + }); +});