diff --git a/test/conf/.gitignore b/test/conf/.gitignore new file mode 100644 index 0000000000..4718e64298 --- /dev/null +++ b/test/conf/.gitignore @@ -0,0 +1,4 @@ +stunnel.conf +stunnel.log +stunnel.pid + diff --git a/test/conf/redis.js.org.cert b/test/conf/redis.js.org.cert new file mode 100644 index 0000000000..6747b744ac --- /dev/null +++ b/test/conf/redis.js.org.cert @@ -0,0 +1,19 @@ +-----BEGIN CERTIFICATE----- +MIIDATCCAemgAwIBAgIJALkMmVkQOERnMA0GCSqGSIb3DQEBBQUAMBcxFTATBgNV +BAMMDHJlZGlzLmpzLm9yZzAeFw0xNTEwMTkxMjIzMjRaFw0yNTEwMTYxMjIzMjRa +MBcxFTATBgNVBAMMDHJlZGlzLmpzLm9yZzCCASIwDQYJKoZIhvcNAQEBBQADggEP +ADCCAQoCggEBAJ/DmMTJHf7kyspxI1A/JmOc+KI9vxEcN5qn7IiZuGN7ghE43Q3q +XB2GUkMAuW1POkmM5yi3SuT1UXDR/4Gk7KlbHKMs37AV6PgJXX6oX0zu12LTAT7V +5byNrYtehSo42l1188dGEMCGaaf0cDntc7A3aW0ZtzrJt+2pu31Uatl2SEJCMra6 ++v6O0c9aHMF1cArKeawGqR+jHw6vXFZQbUd05nW5nQlUA6wVt1JjlLPwBwYsWLsi +YQxMC8NqpgAIg5tULSCpKwx5isL/CeotVVGDNZ/G8R1nTrxuygPlc3Qskj57hmV4 +tZK4JJxQFi7/9ehvjAvHohKrEPeqV5XL87cCAwEAAaNQME4wHQYDVR0OBBYEFCn/ +5hB+XY4pVOnaqvrmZMxrLFjLMB8GA1UdIwQYMBaAFCn/5hB+XY4pVOnaqvrmZMxr +LFjLMAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcNAQEFBQADggEBAEduPyTHpXkCVZRQ +v6p+Ug4iVeXpxGCVr34y7EDUMgmuDdqsz1SrmqeDd0VmjZT8htbWw7QBKDPEBsbi +wl606aAn01iM+oUrwbtXxid1xfZj/j6pIhQVkGu7e/8A7Pr4QOP4OMdHB7EmqkAo +d/OLHa9LdKv2UtJHD6U7oVQbdBHrRV62125GMmotpQuSkEfZM6edKNzHPlqV/zJc +2kGCw3lZC21mTrsSMIC/FQiobPnig4kAvfh0of2rK/XAntlwT8ie1v1aK+jERsfm +uzMihl6XXBdzheq6KdIlf+5STHBIIRcvBoRKr5Va7EhnO03tTzeJowtqDv47yPC6 +w4kLcP8= +-----END CERTIFICATE----- diff --git a/test/conf/redis.js.org.key b/test/conf/redis.js.org.key new file mode 100644 index 0000000000..9376fc1eee --- /dev/null +++ b/test/conf/redis.js.org.key @@ -0,0 +1,27 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIEowIBAAKCAQEAn8OYxMkd/uTKynEjUD8mY5z4oj2/ERw3mqfsiJm4Y3uCETjd +DepcHYZSQwC5bU86SYznKLdK5PVRcNH/gaTsqVscoyzfsBXo+AldfqhfTO7XYtMB +PtXlvI2ti16FKjjaXXXzx0YQwIZpp/RwOe1zsDdpbRm3Osm37am7fVRq2XZIQkIy +trr6/o7Rz1ocwXVwCsp5rAapH6MfDq9cVlBtR3TmdbmdCVQDrBW3UmOUs/AHBixY +uyJhDEwLw2qmAAiDm1QtIKkrDHmKwv8J6i1VUYM1n8bxHWdOvG7KA+VzdCySPnuG +ZXi1krgknFAWLv/16G+MC8eiEqsQ96pXlcvztwIDAQABAoIBAGx4kLCLHCKDlGv+ +hMtnFNltKiJ9acxkLByFBsN4GwjwQk8PHIbmJ8Sj/hYf18WvlRN65zdtuxvYs4K2 +EZQkNcqGYdsoDHexaIt/UEs+ZfYF85bVTHMtJt3uE3Ycpq0UDK6H9wvFNnqAyBuQ +iuHJplJuTNYWL6Fqc8aZBwMA3crmwWTelgS+IXLH06E298+KIxbYrWSgrbcmV/Pj +Iwek4CPS0apoJnXxbZDDhAEYGOTxDNXGm+r7BaX/ePM2x1PPib2X9F2XqFV+A4T8 +Z91axKJwMrVuTrJkaLPDx9lNUskvvV6KgjZAtYRGpLQTN1AqXJZ09IoK9sNPE4rX +9fm4awECgYEAzMJkABL0UOoGJhdRf/R0aUOQMO7vYetX5SK9QXcEI04XYFieSaPm +71st+R/JlJ+LhrTrzGXvyU0tFAQaQZtwaGj/JhbptIpLlGrVf3mqSvxkNi/wzQnn +jBJrrf1ZkDiqtSy7AxGAefWblgK3R1ZU5+0a5jubDkmOltIlbULf0skCgYEAx76l ++5KhWOJPvrjNGB1a8oVXiFzoCpaVVZIhSdl0AtvkKollm5Ou+CKYpE3fKrejRXTD +zmr5bJFXT3VlmIa010cgXJ2btlFa1RiNzgretsOmMcHxLkpAu2/a0L4psHlCrWVK +fxbUW0BYEFVXBDe/4JhFw41YqohdPkFAyo5OUn8CgYBQZGYkzUxVVHzTicY66bym +85ryS217UY5x7WDHCjZ6shdlgYWsPgjWo0L6k+tuSfHbEr+dwcwSihWPzUiNx7yr +kcXTq51YgA/KluN6KEefJ1clG099AU2C5lyWtGjswgLsHULTopSBzdenXyuce53c +bXBpQq/PPTwZpSqCqoX8WQKBgGe+nsk+jGz1BoRBycyHmrAyD5e04ZR2R9PtFTsd +JYNCoIxzVoHqv8sDdRKJm6q9PKEbl4PDzg7UomuTxxPki1LxD17rQW/9a1cY7LYi +sTBuCAj5+YGYcWypGRaoXlDZeodC/+Fogx1uGw9Is+xt5EwL6tg5tt7D+uIV1Egg +h4+TAoGBAKYA/jn9v93bzPi+w1rlZrlPufRSr4k3mcHae165N/1PnjSguTFIF5DW ++1f5S+XioNyTcfx5gKI8f6wRn1j5zbB24GXgu8dXCzRHC2gzrwq2D9v1od4zP/o7 +xFxyiNGOMUJ7uW9d/nEL5Eg4CQKZEkZNmzHhuKNr8wDSr16DhXVK +-----END RSA PRIVATE KEY----- diff --git a/test/conf/stunnel.conf.template b/test/conf/stunnel.conf.template new file mode 100644 index 0000000000..24ed7593b0 --- /dev/null +++ b/test/conf/stunnel.conf.template @@ -0,0 +1,12 @@ +pid = __dirname/stunnel.pid +output = __dirname/stunnel.log +CAfile = __dirname/redis.js.org.cert +cert = __dirname/redis.js.org.cert +key = __dirname/redis.js.org.key +client = no +foreground = yes +libwrap = no +debug = 7 +[redis] +accept = 127.0.0.1:6380 +connect = 127.0.0.1:6379 diff --git a/test/helper.js b/test/helper.js index ae33c88990..942546e980 100644 --- a/test/helper.js +++ b/test/helper.js @@ -4,7 +4,9 @@ var assert = require("assert"); var path = require('path'); var config = require("./lib/config"); var RedisProcess = require("./lib/redis-process"); +var StunnelProcess = require("./lib/stunnel-process"); var rp; +var stunnel_process; function startRedis (conf, done) { RedisProcess.start(function (err, _rp) { @@ -13,6 +15,21 @@ function startRedis (conf, done) { }, path.resolve(__dirname, conf)); } +function startStunnel(done) { + StunnelProcess.start(function (err, _stunnel_process) { + stunnel_process = _stunnel_process; + return done(err); + }, path.resolve(__dirname, './conf')); +} + +function stopStunnel(done) { + if(stunnel_process) { + StunnelProcess.stop(stunnel_process, done); + } else { + done(); + } +} + // don't start redis every time we // include this helper file! if (!process.env.REDIS_TESTS_STARTED) { @@ -35,6 +52,8 @@ module.exports = { rp.stop(done); }, startRedis: startRedis, + stopStunnel: stopStunnel, + startStunnel: startStunnel, isNumber: function (expected, done) { return function (err, results) { assert.strictEqual(null, err, "expected " + expected + ", got error: " + err); diff --git a/test/lib/stunnel-process.js b/test/lib/stunnel-process.js new file mode 100644 index 0000000000..7318d3f0fa --- /dev/null +++ b/test/lib/stunnel-process.js @@ -0,0 +1,83 @@ +'use strict'; + +// helper to start and stop the stunnel process. +var spawn = require('child_process').spawn; +var EventEmitter = require('events').EventEmitter; +var fs = require('fs'); +var path = require('path'); +var util = require('util'); + +function once(cb) { + var called = false; + return function() { + if(called) return; + called = true; + cb.apply(this, arguments); + }; +} + +function StunnelProcess(conf_dir) { + EventEmitter.call(this); + + // set up an stunnel to redis; edit the conf file to include required absolute paths + var conf_file = path.resolve(conf_dir, 'stunnel.conf'); + var conf_text = fs.readFileSync(conf_file + '.template').toString().replace(/__dirname/g, conf_dir); + + fs.writeFileSync(conf_file, conf_text); + var stunnel = this.stunnel = spawn('stunnel', [conf_file]); + + // handle child process events, and failure to set up tunnel + var self = this; + this.timer = setTimeout(function() { + self.emit('error', new Error('Timeout waiting for stunnel to start')); + }, 8000); + + stunnel.on('error', function(err) { + self.clear(); + self.emit('error', err); + }); + + stunnel.on('exit', function(code) { + self.clear(); + if(code === 0) { + self.emit('stopped'); + } else { + self.emit('error', new Error('Stunnel exited unexpectedly; code = ' + code)); + } + }); + + // wait to stunnel to start + stunnel.stderr.on("data", function(data) { + if(data.toString().match(/Service.+redis.+bound/)) { + clearTimeout(this.timer); + self.emit('started'); + } + }); +} +util.inherits(StunnelProcess, EventEmitter); + +StunnelProcess.prototype.clear = function() { + this.stunnel = null; + clearTimeout(this.timer); +}; + +StunnelProcess.prototype.stop = function(done) { + if(this.stunnel) { + this.stunnel.kill(); + } +}; + +module.exports = { + start: function(done, conf_dir) { + done = once(done); + var stunnel = new StunnelProcess(conf_dir); + stunnel.once('error', done.bind(done)); + stunnel.once('started', done.bind(done, null, stunnel)); + }, + stop: function(stunnel, done) { + stunnel.removeAllListeners(); + stunnel.stop(); + stunnel.once('error', done.bind(done)); + stunnel.once('stopped', done.bind(done, null)); + } +}; diff --git a/test/tls.spec.js b/test/tls.spec.js new file mode 100644 index 0000000000..fc33c5a44f --- /dev/null +++ b/test/tls.spec.js @@ -0,0 +1,201 @@ +'use strict'; + +var assert = require("assert"); +var config = require("./lib/config"); +var fs = require('fs'); +var helper = require('./helper'); +var path = require('path'); +var redis = config.redis; + +var tls_options = { + servername: "redis.js.org", + rejectUnauthorized: false, + ca: [ String(fs.readFileSync(path.resolve(__dirname, "./conf/redis.js.org.cert"))) ] +}; + +var tls_port = 6380; + +describe("TLS connection tests", function () { + before(function (done) { + helper.stopStunnel(function () { + helper.startStunnel(done); + }); + }); + + after(function (done) { + helper.stopStunnel(done); + }); + + helper.allTests(function(parser, ip, args) { + + describe("using " + parser + " and " + ip, function () { + + var client; + + afterEach(function () { + if (client) { + client.end(); + } + }); + + describe("on lost connection", function () { + it("emit an error after max retry attempts and do not try to reconnect afterwards", function (done) { + var max_attempts = 4; + var options = { + parser: parser, + max_attempts: max_attempts, + port: tls_port, + tls: tls_options + }; + client = redis.createClient(options); + var calls = 0; + + client.once('ready', function() { + helper.killConnection(client); + }); + + client.on("reconnecting", function (params) { + calls++; + }); + + client.on('error', function(err) { + if (/Redis connection in broken state: maximum connection attempts.*?exceeded./.test(err.message)) { + setTimeout(function () { + assert.strictEqual(calls, max_attempts - 1); + done(); + }, 500); + } + }); + }); + + it("emit an error after max retry timeout and do not try to reconnect afterwards", function (done) { + var connect_timeout = 500; // in ms + client = redis.createClient({ + parser: parser, + connect_timeout: connect_timeout, + port: tls_port, + tls: tls_options + }); + var time = 0; + + client.once('ready', function() { + helper.killConnection(client); + }); + + client.on("reconnecting", function (params) { + time += params.delay; + }); + + client.on('error', function(err) { + if (/Redis connection in broken state: connection timeout.*?exceeded./.test(err.message)) { + setTimeout(function () { + assert(time === connect_timeout); + done(); + }, 500); + } + }); + }); + + it("end connection while retry is still ongoing", function (done) { + var connect_timeout = 1000; // in ms + client = redis.createClient({ + parser: parser, + connect_timeout: connect_timeout, + port: tls_port, + tls: tls_options + }); + + client.once('ready', function() { + helper.killConnection(client); + }); + + client.on("reconnecting", function (params) { + client.end(); + setTimeout(done, 100); + }); + }); + + it("can not connect with wrong host / port in the options object", function (done) { + var options = { + host: 'somewhere', + max_attempts: 1, + port: tls_port, + tls: tls_options + }; + client = redis.createClient(options); + var end = helper.callFuncAfter(done, 2); + + client.on('error', function (err) { + assert(/CONNECTION_BROKEN|ENOTFOUND|EAI_AGAIN/.test(err.code)); + end(); + }); + + }); + }); + + describe("when not connected", function () { + + it("connect with host and port provided in the options object", function (done) { + client = redis.createClient({ + host: 'localhost', + parser: parser, + connect_timeout: 1000, + port: tls_port, + tls: tls_options + }); + + client.once('ready', function() { + done(); + }); + }); + + it("connects correctly with args", function (done) { + var args_host = args[1]; + var args_options = args[2] || {}; + args_options.tls = tls_options; + client = redis.createClient(tls_port, args_host, args_options); + client.on("error", done); + + client.once("ready", function () { + client.removeListener("error", done); + client.get("recon 1", function (err, res) { + done(err); + }); + }); + }); + + if (ip === 'IPv4') { + it('allows connecting with the redis url and no auth and options as second parameter', function (done) { + var options = { + detect_buffers: false, + magic: Math.random(), + port: tls_port, + tls: tls_options + }; + client = redis.createClient('redis://' + config.HOST[ip] + ':' + tls_port, options); + // verify connection is using TCP, not UNIX socket + assert.strictEqual(client.connection_options.host, config.HOST[ip]); + assert.strictEqual(client.connection_options.port, tls_port); + // verify passed options are in use + assert.strictEqual(client.options.magic, options.magic); + client.on("ready", function () { + return done(); + }); + }); + + it('allows connecting with the redis url and no auth and options as third parameter', function (done) { + client = redis.createClient('redis://' + config.HOST[ip] + ':' + tls_port, null, { + detect_buffers: false, + tls: tls_options + }); + client.on("ready", function () { + return done(); + }); + }); + } + + }); + + }); + }); +});