tls: wrap tls inside tls using legacy API
authorFedor Indutny <fedor.indutny@gmail.com>
Tue, 24 Sep 2013 12:53:49 +0000 (16:53 +0400)
committerFedor Indutny <fedor.indutny@gmail.com>
Tue, 24 Sep 2013 16:46:59 +0000 (20:46 +0400)
Allow wrapping TLSSocket inside another TLSSocket, emulate it using
SecurePair in legacy APIs.

fix #6204

lib/_tls_legacy.js
lib/_tls_wrap.js
src/node_crypto.cc
test/simple/test-tls-inception.js [new file with mode: 0644]

index 8c22ff4..17d193f 100644 (file)
@@ -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;
+}
index 32e9a25..3ffc804 100644 (file)
@@ -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;
 };
index ee1b7db..6de073e 100644 (file)
@@ -1689,14 +1689,14 @@ void Connection::New(const FunctionCallbackInfo<Value>& args) {
     SSL_CTX_set_next_protos_advertised_cb(
         sc->ctx_,
         SSLWrap<Connection>::AdvertiseNextProtoCallback,
-        NULL);
+        conn);
   } else {
     // Client should select protocol from advertised
     // If server supports NPN
     SSL_CTX_set_next_proto_select_cb(
         sc->ctx_,
         SSLWrap<Connection>::SelectNextProtoCallback,
-        NULL);
+        conn);
   }
 #endif
 
diff --git a/test/simple/test-tls-inception.js b/test/simple/test-tls-inception.js
new file mode 100644 (file)
index 0000000..d15d1eb
--- /dev/null
@@ -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();
+    });
+  });
+});