Browse Source

tls: wrap tls inside tls using legacy API

Allow wrapping TLSSocket inside another TLSSocket, emulate it using
SecurePair in legacy APIs.

fix #6204
v0.11.8-release
Fedor Indutny 11 years ago
parent
commit
42acbf809b
  1. 60
      lib/_tls_legacy.js
  2. 134
      lib/_tls_wrap.js
  3. 4
      src/node_crypto.cc
  4. 87
      test/simple/test-tls-inception.js

60
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;
}

134
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;
};

4
src/node_crypto.cc

@ -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

87
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();
});
});
});
Loading…
Cancel
Save