mirror of https://github.com/lukechilds/node.git
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
657 lines
17 KiB
657 lines
17 KiB
var tls = require('tls');
|
|
var fs = require('fs');
|
|
var path = require('path');
|
|
var fork = require('child_process').fork;
|
|
var assert = require('assert');
|
|
var constants = require('constants');
|
|
var os = require('os');
|
|
|
|
var async = require('async');
|
|
var debug = require('debug')('test-node-ssl');
|
|
|
|
var common = require('../../common');
|
|
|
|
var CMD_LINE_OPTIONS = [ null, "--enable-ssl3" ];
|
|
|
|
var SERVER_SSL_PROTOCOLS = [
|
|
null,
|
|
'SSLv3_method', 'SSLv3_server_method',
|
|
'TLSv1_method', 'TLSv1_server_method',
|
|
'SSLv23_method','SSLv23_server_method'
|
|
];
|
|
|
|
var CLIENT_SSL_PROTOCOLS = [
|
|
null,
|
|
'SSLv3_method', 'SSLv3_client_method',
|
|
'TLSv1_method', 'TLSv1_client_method',
|
|
'SSLv23_method','SSLv23_client_method'
|
|
];
|
|
|
|
var SECURE_OPTIONS = [
|
|
null,
|
|
0,
|
|
constants.SSL_OP_NO_SSLv3,
|
|
];
|
|
|
|
function xtend(source) {
|
|
var clone = {};
|
|
|
|
for (var property in source) {
|
|
if (source.hasOwnProperty(property)) {
|
|
clone[property] = source[property];
|
|
}
|
|
}
|
|
|
|
return clone;
|
|
}
|
|
|
|
function isAutoNegotiationProtocol(sslProtocol) {
|
|
assert(sslProtocol === null || typeof sslProtocol === 'string');
|
|
|
|
return sslProtocol == null ||
|
|
sslProtocol === 'SSLv23_method' ||
|
|
sslProtocol === 'SSLv23_client_method' ||
|
|
sslProtocol === 'SSLv23_server_method';
|
|
}
|
|
|
|
function isSameSslProtocolVersion(serverSecureProtocol, clientSecureProtocol) {
|
|
assert(serverSecureProtocol === null || typeof serverSecureProtocol === 'string');
|
|
assert(clientSecureProtocol === null || typeof clientSecureProtocol === 'string');
|
|
|
|
if (serverSecureProtocol === clientSecureProtocol) {
|
|
return true;
|
|
}
|
|
|
|
var serverProtocolPrefix = '';
|
|
if (serverSecureProtocol)
|
|
serverProtocolPrefix = serverSecureProtocol.split('_')[0];
|
|
|
|
var clientProtocolPrefix = '';
|
|
if (clientSecureProtocol)
|
|
clientProtocolPrefix = clientSecureProtocol.split('_')[0];
|
|
|
|
if (serverProtocolPrefix === clientProtocolPrefix) {
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
function secureProtocolsCompatible(serverSecureProtocol, clientSecureProtocol) {
|
|
if (isAutoNegotiationProtocol(serverSecureProtocol) ||
|
|
isAutoNegotiationProtocol(clientSecureProtocol)) {
|
|
return true;
|
|
}
|
|
|
|
if (isSameSslProtocolVersion(serverSecureProtocol,
|
|
clientSecureProtocol)) {
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
function isSsl3Protocol(secureProtocol) {
|
|
assert(secureProtocol === null || typeof secureProtocol === 'string');
|
|
|
|
return secureProtocol === 'SSLv3_method' ||
|
|
secureProtocol === 'SSLv3_client_method' ||
|
|
secureProtocol === 'SSLv3_server_method';
|
|
}
|
|
|
|
function secureProtocolCompatibleWithSecureOptions(secureProtocol, secureOptions, cmdLineOption) {
|
|
if (secureOptions == null) {
|
|
if (isSsl3Protocol(secureProtocol) &&
|
|
(!cmdLineOption || cmdLineOption.indexOf('--enable-ssl3') === -1)) {
|
|
return false;
|
|
}
|
|
} else {
|
|
if (secureOptions & constants.SSL_OP_NO_SSLv3 && isSsl3Protocol(secureProtocol)) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
function testSetupsCompatible(serverSetup, clientSetup) {
|
|
debug('Determing test result for:');
|
|
debug(serverSetup);
|
|
debug(clientSetup);
|
|
|
|
/*
|
|
* If the protocols specified by the client and server are
|
|
* not compatible (e.g SSLv2 vs SSLv3), then the test should fail.
|
|
*/
|
|
if (!secureProtocolsCompatible(serverSetup.secureProtocol,
|
|
clientSetup.secureProtocol)) {
|
|
debug('secureProtocols not compatible! server secureProtocol: ' +
|
|
serverSetup.secureProtocol + ', client secureProtocol: ' +
|
|
clientSetup.secureProtocol);
|
|
return false;
|
|
}
|
|
|
|
/*
|
|
* If the client's options are not compatible with the server's protocol,
|
|
* then the test should fail. Same if server's options are not compatible
|
|
* with the client's protocol.
|
|
*/
|
|
if (!secureProtocolCompatibleWithSecureOptions(serverSetup.secureProtocol,
|
|
clientSetup.secureOptions,
|
|
clientSetup.cmdLine) ||
|
|
!secureProtocolCompatibleWithSecureOptions(clientSetup.secureProtocol,
|
|
serverSetup.secureOptions,
|
|
serverSetup.cmdLine)) {
|
|
debug('Secure protocol not compatible with secure options!');
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
function sslSetupMakesSense(cmdLineOption, secureProtocol, secureOption) {
|
|
if (isSsl3Protocol(secureProtocol)) {
|
|
if (secureOption & constants.SSL_OP_NO_SSLv3 ||
|
|
(secureOption == null && (!cmdLineOption || cmdLineOption.indexOf('--enable-ssl3') === -1))) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
function createTestsSetups() {
|
|
|
|
var serversSetup = [];
|
|
var clientsSetup = [];
|
|
|
|
CMD_LINE_OPTIONS.forEach(function (cmdLineOption) {
|
|
SERVER_SSL_PROTOCOLS.forEach(function (serverSecureProtocol) {
|
|
SECURE_OPTIONS.forEach(function (secureOption) {
|
|
if (sslSetupMakesSense(cmdLineOption,
|
|
serverSecureProtocol,
|
|
secureOption)) {
|
|
var serverSetup = {
|
|
cmdLine: cmdLineOption,
|
|
secureProtocol: serverSecureProtocol,
|
|
secureOptions: secureOption
|
|
};
|
|
|
|
serversSetup.push(serverSetup);
|
|
}
|
|
});
|
|
});
|
|
|
|
CLIENT_SSL_PROTOCOLS.forEach(function (clientSecureProtocol) {
|
|
SECURE_OPTIONS.forEach(function (secureOption) {
|
|
if (sslSetupMakesSense(cmdLineOption,
|
|
clientSecureProtocol,
|
|
secureOption)) {
|
|
var clientSetup = {
|
|
cmdLine: cmdLineOption,
|
|
secureProtocol: clientSecureProtocol,
|
|
secureOptions: secureOption
|
|
};
|
|
|
|
clientsSetup.push(clientSetup);
|
|
}
|
|
});
|
|
});
|
|
});
|
|
|
|
var testSetups = [];
|
|
var testId = 0;
|
|
serversSetup.forEach(function (serverSetup) {
|
|
clientsSetup.forEach(function (clientSetup) {
|
|
var testSetup = {
|
|
server: serverSetup,
|
|
client: clientSetup,
|
|
ID: testId++
|
|
};
|
|
|
|
var successExpected = false;
|
|
if (testSetupsCompatible(serverSetup, clientSetup)) {
|
|
successExpected = true;
|
|
}
|
|
testSetup.successExpected = successExpected;
|
|
|
|
testSetups.push(testSetup);
|
|
});
|
|
});
|
|
|
|
return testSetups;
|
|
}
|
|
|
|
function runServer(port, secureProtocol, secureOptions, ciphers) {
|
|
debug('Running server!');
|
|
debug('port: ' + port);
|
|
debug('secureProtocol: ' + secureProtocol);
|
|
debug('secureOptions: ' + secureOptions);
|
|
debug('ciphers: ' + ciphers);
|
|
|
|
var keyPath = path.join(common.fixturesDir, 'agent.key');
|
|
var certPath = path.join(common.fixturesDir, 'agent.crt');
|
|
|
|
var key = fs.readFileSync(keyPath).toString();
|
|
var cert = fs.readFileSync(certPath).toString();
|
|
|
|
var server = new tls.Server({ key: key,
|
|
cert: cert,
|
|
ca: [],
|
|
ciphers: ciphers,
|
|
secureProtocol: secureProtocol,
|
|
secureOptions: secureOptions
|
|
});
|
|
|
|
server.listen(port, function() {
|
|
process.on('message', function onChildMsg(msg) {
|
|
if (msg === 'close') {
|
|
server.close();
|
|
process.exit(0);
|
|
}
|
|
});
|
|
|
|
process.send('server_listening');
|
|
});
|
|
|
|
server.on('error', function onServerError(err) {
|
|
debug('Server error: ' + err);
|
|
process.exit(1);
|
|
});
|
|
|
|
server.on('clientError', function onClientError(err) {
|
|
debug('Client error on server: ' + err);
|
|
process.exit(1);
|
|
});
|
|
}
|
|
|
|
function runClient(port, secureProtocol, secureOptions, ciphers) {
|
|
debug('Running client!');
|
|
debug('port: ' + port);
|
|
debug('secureProtocol: ' + secureProtocol);
|
|
debug('secureOptions: ' + secureOptions);
|
|
debug('ciphers: ' + ciphers);
|
|
|
|
var con = tls.connect(port,
|
|
{
|
|
rejectUnauthorized: false,
|
|
secureProtocol: secureProtocol,
|
|
secureOptions: secureOptions
|
|
},
|
|
function() {
|
|
|
|
// TODO jgilli: test that sslProtocolUsed is at least as "secure" as
|
|
// "secureProtocol"
|
|
/*
|
|
* var sslProtocolUsed = con.getVersion();
|
|
* debug('Protocol used: ' + sslProtocolUsed);
|
|
*/
|
|
|
|
process.send('client_done');
|
|
});
|
|
|
|
con.on('error', function(err) {
|
|
debug('Client could not connect:' + err);
|
|
process.exit(1);
|
|
});
|
|
}
|
|
|
|
function stringToSecureOptions(secureOptionsString) {
|
|
assert(typeof secureOptionsString === 'string');
|
|
|
|
var secureOptions;
|
|
|
|
var optionStrings = secureOptionsString.split('|');
|
|
optionStrings.forEach(function (option) {
|
|
if (option === 'SSL_OP_NO_SSLv3') {
|
|
secureOptions |= constants.SSL_OP_NO_SSLv3;
|
|
}
|
|
|
|
if (option === '0') {
|
|
secureOptions = 0;
|
|
}
|
|
});
|
|
|
|
return secureOptions;
|
|
}
|
|
|
|
function processTestCmdLineOptions(argv){
|
|
var options = {};
|
|
|
|
argv.forEach(function (arg) {
|
|
var key;
|
|
var value;
|
|
|
|
var keyValue = arg.split(':');
|
|
var key = keyValue[0];
|
|
|
|
if (keyValue.length == 2 && keyValue[1].length > 0) {
|
|
value = keyValue[1];
|
|
|
|
if (key === 'secureOptions') {
|
|
value = stringToSecureOptions(value);
|
|
}
|
|
|
|
if (key === 'port') {
|
|
value = +value;
|
|
}
|
|
}
|
|
|
|
options[key] = value;
|
|
});
|
|
|
|
return options;
|
|
}
|
|
|
|
function checkTestExitCode(testSetup, serverExitCode, clientExitCode) {
|
|
if (testSetup.successExpected) {
|
|
if (serverExitCode === 0 && clientExitCode === 0) {
|
|
debug('Test succeeded as expected!');
|
|
return true;
|
|
}
|
|
} else {
|
|
if (serverExitCode !== 0 || clientExitCode !== 0) {
|
|
debug('Test failed as expected!');
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
function secureOptionsToString(secureOptions) {
|
|
var secureOptsString = '';
|
|
|
|
if (secureOptions & constants.SSL_OP_NO_SSLv3) {
|
|
secureOptsString += '|SSL_OP_NO_SSLv3';
|
|
}
|
|
|
|
if (secureOptions === 0) {
|
|
secureOptsString = '0';
|
|
}
|
|
|
|
return secureOptsString;
|
|
}
|
|
|
|
function forkTestProcess(processType, testSetup, port) {
|
|
var argv = [ processType ];
|
|
|
|
if (testSetup.secureProtocol) {
|
|
argv.push('secureProtocol:' + testSetup.secureProtocol);
|
|
} else {
|
|
argv.push('secureProtocol:');
|
|
}
|
|
|
|
argv.push('secureOptions:' + secureOptionsToString(testSetup.secureOptions));
|
|
|
|
if (testSetup.ciphers) {
|
|
argv.push('ciphers:' + testSetup.ciphers);
|
|
} else {
|
|
argv.push('ciphers:');
|
|
}
|
|
|
|
argv.push('port:' + port);
|
|
|
|
var forkOptions;
|
|
if (testSetup.cmdLine) {
|
|
forkOptions = {
|
|
execArgv: [ testSetup.cmdLine ]
|
|
}
|
|
}
|
|
|
|
return fork(process.argv[1],
|
|
argv,
|
|
forkOptions);
|
|
}
|
|
|
|
function runTest(testSetup, testDone) {
|
|
var clientSetup = testSetup.client;
|
|
var serverSetup = testSetup.server;
|
|
|
|
assert(clientSetup);
|
|
assert(serverSetup);
|
|
|
|
debug('Starting new test on port: ' + testSetup.port);
|
|
|
|
debug('client setup:');
|
|
debug(clientSetup);
|
|
|
|
debug('server setup:');
|
|
debug(serverSetup);
|
|
|
|
debug('Success expected:' + testSetup.successExpected);
|
|
|
|
var serverExitCode;
|
|
|
|
var clientStarted = false;
|
|
var clientExitCode;
|
|
|
|
var serverChild = forkTestProcess('server', serverSetup, testSetup.port);
|
|
assert(serverChild);
|
|
|
|
serverChild.on('message', function onServerMsg(msg) {
|
|
if (msg === 'server_listening') {
|
|
debug('Starting client!');
|
|
clientStarted = true;
|
|
|
|
var clientChild = forkTestProcess('client', clientSetup, testSetup.port);
|
|
assert(clientChild);
|
|
|
|
clientChild.on('exit', function onClientExited(exitCode) {
|
|
debug('Client exited with code:' + exitCode);
|
|
|
|
clientExitCode = exitCode;
|
|
if (serverExitCode != null) {
|
|
var err;
|
|
if (!checkTestExitCode(testSetup, serverExitCode, clientExitCode))
|
|
err = new Error("Test failed!");
|
|
|
|
return testDone(err);
|
|
} else {
|
|
if (serverChild.connected) {
|
|
serverChild.send('close');
|
|
}
|
|
}
|
|
});
|
|
|
|
clientChild.on('message', function onClientMsg(msg) {
|
|
if (msg === 'client_done' && serverChild.connected) {
|
|
serverChild.send('close');
|
|
}
|
|
})
|
|
}
|
|
});
|
|
|
|
serverChild.on('exit', function onServerExited(exitCode) {
|
|
debug('Server exited with code:' + exitCode);
|
|
|
|
serverExitCode = exitCode;
|
|
if (clientExitCode != null || !clientStarted) {
|
|
var err;
|
|
if (!checkTestExitCode(testSetup, serverExitCode, clientExitCode))
|
|
err = new Error("Test failed!");
|
|
|
|
return testDone(err);
|
|
}
|
|
});
|
|
}
|
|
|
|
function usage() {
|
|
console.log('Usage: test-node-ssl [-j N] [--list-tests] [-s startIndex] ' +
|
|
'[-e endIndex] [-o outputFile]');
|
|
process.exit(1);
|
|
}
|
|
|
|
function processDriverCmdLineOptions(argv) {
|
|
var options = {
|
|
parallelTests: 1
|
|
};
|
|
|
|
for (var i = 1; i < argv.length; ++i) {
|
|
if (argv[i] === '-j') {
|
|
|
|
var nbParallelTests = +argv[i + 1];
|
|
if (!nbParallelTests) {
|
|
usage();
|
|
} else {
|
|
options.parallelTests = argv[++i];
|
|
}
|
|
}
|
|
|
|
if (argv[i] === '-s') {
|
|
var start = +argv[i + 1];
|
|
if (!start) {
|
|
usage();
|
|
} else {
|
|
options.start = argv[++i];
|
|
}
|
|
}
|
|
|
|
if (argv[i] === '-e') {
|
|
var end = +argv[i + 1];
|
|
if (!end) {
|
|
usage();
|
|
} else {
|
|
options.end = argv[++i];
|
|
}
|
|
}
|
|
|
|
if (argv[i] === '--list-tests') {
|
|
options.listTests = true;
|
|
}
|
|
|
|
if (argv[i] === '-o') {
|
|
var outputFile = argv[i + 1];
|
|
if (!outputFile) {
|
|
usage();
|
|
} else {
|
|
options.outputFile = argv[++i];
|
|
}
|
|
}
|
|
}
|
|
|
|
return options;
|
|
}
|
|
|
|
function outputTestResult(test, err, output) {
|
|
output.write(os.EOL);
|
|
output.write('Test:' + os.EOL);
|
|
output.write(JSON.stringify(test, null, " "));
|
|
output.write(os.EOL);
|
|
output.write('Result:');
|
|
output.write(err ? 'failure' : 'success');
|
|
output.write(os.EOL);
|
|
}
|
|
|
|
var agentType = process.argv[2];
|
|
if (agentType === 'client' || agentType === 'server') {
|
|
var options = processTestCmdLineOptions(process.argv);
|
|
debug('secureProtocol: ' + options.secureProtocol);
|
|
debug('secureOptions: ' + options.secureOptions);
|
|
debug('ciphers:' + options.ciphers);
|
|
debug('port:' + options.port);
|
|
|
|
if (agentType === 'client') {
|
|
runClient(options.port,
|
|
options.secureProtocol,
|
|
options.secureOptions,
|
|
options.ciphers);
|
|
} else if (agentType === 'server') {
|
|
runServer(options.port,
|
|
options.secureProtocol,
|
|
options.secureOptions,
|
|
options.ciphers);
|
|
}
|
|
} else {
|
|
var driverOptions = processDriverCmdLineOptions(process.argv);
|
|
debug('Tests driver options:');
|
|
debug(driverOptions);
|
|
/*
|
|
* This is the tests driver process.
|
|
*
|
|
* It forks itself twice for each test. Each of the two forked processees are
|
|
* respectfully used as an SSL client and an SSL server. The client and
|
|
* server setup their SSL connection as generated by the "createTestsSetups"
|
|
* function. Once both processes have exited, the tests driver process
|
|
* compare both client and server exit codes with the expected test result
|
|
* of the test setup. If they match, the test is successful, otherwise it
|
|
* failed.
|
|
*/
|
|
|
|
var testSetups = createTestsSetups();
|
|
|
|
if (driverOptions.listTests) {
|
|
console.log(testSetups);
|
|
process.exit(0);
|
|
}
|
|
|
|
var testOutput = process.stdout;
|
|
if (driverOptions.outputFile) {
|
|
testOutput = fs.createWriteStream(driverOptions.outputFile)
|
|
.on('error', function onError(err) {
|
|
console.error(err);
|
|
process.exit(1);
|
|
});
|
|
}
|
|
|
|
debug('Tests setups:');
|
|
debug('Number of tests: ' + testSetups.length);
|
|
debug(JSON.stringify(testSetups, null, " "));
|
|
debug();
|
|
|
|
var nbTestsStarted = 0;
|
|
|
|
function runTests(tests, callback) {
|
|
var nbTests = tests.length;
|
|
if (nbTests === 0) {
|
|
return callback();
|
|
}
|
|
var error;
|
|
var nbTestsDone = 0;
|
|
|
|
debug('Starting new batch of tests...');
|
|
|
|
var port = common.PORT;
|
|
async.each(tests, function (test, testDone) {
|
|
test.port = port++;
|
|
|
|
++nbTestsStarted;
|
|
debug('Starting test nb: ' + nbTestsStarted);
|
|
|
|
runTest(test, function onTestDone(err) {
|
|
++nbTestsDone;
|
|
if (err && error === undefined) {
|
|
error = new Error('Test with ID ' + test.ID + ' failed: ' + err);
|
|
}
|
|
|
|
outputTestResult(test, err, testOutput);
|
|
|
|
if (nbTestsDone === nbTests)
|
|
return testDone(error);
|
|
return testDone();
|
|
});
|
|
|
|
}, function testsDone(err, results) {
|
|
if (err) {
|
|
assert(false,
|
|
"At least one test in the most recent batch failed: " + err);
|
|
}
|
|
|
|
return callback(err);
|
|
});
|
|
}
|
|
|
|
function runAllTests(allTests, allTestsDone) {
|
|
if (allTests.length === 0) {
|
|
return allTestsDone();
|
|
}
|
|
|
|
return runTests(allTests.splice(0, driverOptions.parallelTests),
|
|
runAllTests.bind(global, allTests, allTestsDone));
|
|
}
|
|
|
|
runAllTests(testSetups.slice(driverOptions.start, driverOptions.end),
|
|
function allDone(err) {
|
|
console.log('All tests done!');
|
|
});
|
|
}
|
|
|