summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--test/external/ssl-options/.gitignore1
-rw-r--r--test/external/ssl-options/package.json15
-rw-r--r--test/external/ssl-options/test.js729
3 files changed, 745 insertions, 0 deletions
diff --git a/test/external/ssl-options/.gitignore b/test/external/ssl-options/.gitignore
new file mode 100644
index 000000000..c2658d7d1
--- /dev/null
+++ b/test/external/ssl-options/.gitignore
@@ -0,0 +1 @@
+node_modules/
diff --git a/test/external/ssl-options/package.json b/test/external/ssl-options/package.json
new file mode 100644
index 000000000..114dce6af
--- /dev/null
+++ b/test/external/ssl-options/package.json
@@ -0,0 +1,15 @@
+{
+ "name": "ssl-options-tests",
+ "version": "1.0.0",
+ "description": "",
+ "main": "test.js",
+ "scripts": {
+ "test": "node test.js"
+ },
+ "author": "",
+ "license": "MIT",
+ "dependencies": {
+ "async": "^0.9.0",
+ "debug": "^2.1.0"
+ }
+}
diff --git a/test/external/ssl-options/test.js b/test/external/ssl-options/test.js
new file mode 100644
index 000000000..f7e06c93d
--- /dev/null
+++ b/test/external/ssl-options/test.js
@@ -0,0 +1,729 @@
+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 SSL2_COMPATIBLE_CIPHERS = 'RC4-MD5';
+
+var CMD_LINE_OPTIONS = [ null, "--enable-ssl2", "--enable-ssl3" ];
+
+var SERVER_SSL_PROTOCOLS = [
+ null,
+ 'SSLv2_method', 'SSLv2_server_method',
+ 'SSLv3_method', 'SSLv3_server_method',
+ 'TLSv1_method', 'TLSv1_server_method',
+ 'SSLv23_method','SSLv23_server_method'
+];
+
+var CLIENT_SSL_PROTOCOLS = [
+ null,
+ 'SSLv2_method', 'SSLv2_client_method',
+ 'SSLv3_method', 'SSLv3_client_method',
+ 'TLSv1_method', 'TLSv1_client_method',
+ 'SSLv23_method','SSLv23_client_method'
+];
+
+var SECURE_OPTIONS = [
+ null,
+ 0,
+ constants.SSL_OP_NO_SSLv2,
+ constants.SSL_OP_NO_SSLv3,
+ constants.SSL_OP_NO_SSLv2 | 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 isSsl2Protocol(secureProtocol) {
+ assert(secureProtocol === null || typeof secureProtocol === 'string');
+
+ return secureProtocol === 'SSLv2_method' ||
+ secureProtocol === 'SSLv2_client_method' ||
+ secureProtocol === 'SSLv2_server_method';
+}
+
+function secureProtocolCompatibleWithSecureOptions(secureProtocol, secureOptions, cmdLineOption) {
+ if (secureOptions == null) {
+ if (isSsl2Protocol(secureProtocol) &&
+ (!cmdLineOption || cmdLineOption.indexOf('--enable-ssl2') === -1)) {
+ return false;
+ }
+
+ if (isSsl3Protocol(secureProtocol) &&
+ (!cmdLineOption || cmdLineOption.indexOf('--enable-ssl3') === -1)) {
+ return false;
+ }
+ } else {
+ if (secureOptions & constants.SSL_OP_NO_SSLv2 && isSsl2Protocol(secureProtocol)) {
+ return false;
+ }
+
+ 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;
+ }
+
+ if (isSsl2Protocol(serverSetup.secureProtocol) ||
+ isSsl2Protocol(clientSetup.secureProtocol)) {
+
+ /*
+ * It seems that in order to be able to use SSLv2, at least the server
+ * *needs* to advertise at least one cipher compatible with it.
+ */
+ if (serverSetup.ciphers !== SSL2_COMPATIBLE_CIPHERS) {
+ return false;
+ }
+
+ /*
+ * If only either one of the client or server specify SSLv2 as their
+ * protocol, then *both* of them *need* to advertise at least one cipher
+ * that is compatible with SSLv2.
+ */
+ if ((!isSsl2Protocol(serverSetup.secureProtocol) || !isSsl2Protocol(clientSetup.secureProtocol)) &&
+ (clientSetup.ciphers !== SSL2_COMPATIBLE_CIPHERS || serverSetup.ciphers !== SSL2_COMPATIBLE_CIPHERS)) {
+ return false;
+ }
+ }
+
+ return true;
+}
+
+function sslSetupMakesSense(cmdLineOption, secureProtocol, secureOption) {
+ if (isSsl2Protocol(secureProtocol)) {
+ if (secureOption & constants.SSL_OP_NO_SSLv2 ||
+ (secureOption == null && (!cmdLineOption || cmdLineOption.indexOf('--enable-ssl2') === -1))) {
+ return false;
+ }
+ }
+
+ 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);
+
+ if (isSsl2Protocol(serverSecureProtocol)) {
+ var setupWithSsl2Ciphers = xtend(serverSetup);
+ setupWithSsl2Ciphers.ciphers = SSL2_COMPATIBLE_CIPHERS;
+ serversSetup.push(setupWithSsl2Ciphers);
+ }
+ }
+ });
+ });
+
+ 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);
+
+ if (isSsl2Protocol(clientSecureProtocol)) {
+ var setupWithSsl2Ciphers = xtend(clientSetup);
+ setupWithSsl2Ciphers.ciphers = SSL2_COMPATIBLE_CIPHERS;
+ clientsSetup.push(setupWithSsl2Ciphers);
+ }
+ }
+ });
+ });
+ });
+
+ 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_SSLv2') {
+ secureOptions |= constants.SSL_OP_NO_SSLv2;
+ }
+
+ 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_SSLv2) {
+ secureOptsString += 'SSL_OP_NO_SSLv2';
+ }
+
+ 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!');
+ });
+}