Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -193,7 +193,8 @@ to maximum value, provided in milliseconds.
limits total time for client to reconnect. Value is provided in milliseconds and is counted once the disconnect occured.
* `max_attempts` defaults to `null`. By default client will try reconnecting until connected. Setting `max_attempts`
limits total amount of reconnects.
* `auth_pass` defaults to `null`. By default client will try connecting without auth. If set, client will run redis auth command on connect.
* `auth_pass` defaults to `null`. By default client will try connecting without auth. If set, client will run redis auth command on connect. If the `auth_pass` is set and there is no password in redis the driver will log the warning. If the password is enabled in redis at a later point (using `config set requirepass`) the client will seemlessly authenticate when some command fails and reissue the failed command, so there will be no failed command from the application perspective.
* `new_auth_pass` defaults to undefined. This option allows changing redis password without application restart. If authentication with `auth_pass` password fails, the driver will try authenticating with `new_auth_pass`.
* `family` defaults to `IPv4`. The client connects in IPv4 if not specified or if the DNS resolution returns an IPv4 address.
You can force an IPv6 if you set the family to 'IPv6'. See nodejs net or dns modules how to use the family type.

Expand Down
133 changes: 98 additions & 35 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -186,56 +186,87 @@ RedisClient.prototype.on_error = function (msg) {
var noPasswordIsSet = /no password is set/;
var loading = /LOADING/;

RedisClient.prototype.do_auth = function () {
var self = this;

debug("Sending auth to " + self.address + " id " + self.connection_id);

self.send_anyway = true;
self.send_command("auth", [this.auth_pass], function (err, res) {
function _do_auth(client, pass, cb) {
client.send_anyway = true;
client.send_command("auth", [pass], function (err, res) {
if (err) {
/* istanbul ignore if: this is almost impossible to test */
if (loading.test(err.message)) {
// if redis is still loading the db, it will not authenticate and everything else will fail
debug("Redis still loading, trying to authenticate later");
setTimeout(function () {
self.do_auth();
client.do_auth();
}, 2000); // TODO - magic number alert
return;
return cb();
} else if (noPasswordIsSet.test(err.message)) {
debug("Warning: Redis server does not require a password, but a password was supplied.");
err = null;
res = "OK";
} else if (self.auth_callback) {
self.auth_callback(err);
self.auth_callback = null;
client.auth_no_password = true;
} else {
self.emit("error", err);
return;
return cb(new Error("Auth error: " + err.message));
}
}

res = res.toString();
debug("Auth succeeded " + self.address + " id " + self.connection_id);
if (res !== "OK") {
return cb(new Error("Auth failed: " + res));
}

debug("Auth succeeded " + client.address + " id " + client.connection_id);

if (self.auth_callback) {
self.auth_callback(null, res);
self.auth_callback = null;
if (client.auth_callback) {
client.auth_callback(null, res);
client.auth_callback = null;
}

// now we are really connected
self.emit("connect");
self.initialize_retry_vars();
client.emit("connect");
client.initialize_retry_vars();

if (self.options.no_ready_check) {
self.on_ready();
if (client.options.no_ready_check) {
client.on_ready();
} else {
self.ready_check();
client.ready_check();
}

cb();
});
client.send_anyway = false;
}


RedisClient.prototype.do_auth = function () {
var self = this;

debug("Sending auth to " + self.address + " id " + self.connection_id);

_do_auth(this, this.auth_pass, function (err) {
function _error(err) {
err.command_used = 'AUTH';
if (self.auth_callback) {
self.auth_callback(err);
self.auth_callback = null;
} else {
self.emit("error", err);
}
}

if (err) {
var new_auth_pass = self.options.new_auth_pass;
if (new_auth_pass) {
_do_auth(self, new_auth_pass, function (err) {
if (err) _error(err);
});
} else {
_error(err);
}
}
});
self.send_anyway = false;
};


RedisClient.prototype.on_connect = function () {
debug("Stream connected " + this.address + " id " + this.connection_id);

Expand Down Expand Up @@ -396,13 +427,17 @@ RedisClient.prototype.ready_check = function () {
this.send_anyway = false;
};

function send_command_obj(client, command_obj) {
return client.send_command(command_obj.command, command_obj.args, command_obj.callback);
}

RedisClient.prototype.send_offline_queue = function () {
var command_obj, buffered_writes = 0;

while (this.offline_queue.length > 0) {
command_obj = this.offline_queue.shift();
debug("Sending offline command: " + command_obj.command);
buffered_writes += !this.send_command(command_obj.command, command_obj.args, command_obj.callback);
buffered_writes += !send_command_obj(this, command_obj);
}
this.offline_queue = new Queue();
// Even though items were shifted off, Queue backing store still uses memory until next add, so just get a new Queue
Expand Down Expand Up @@ -507,25 +542,50 @@ RedisClient.prototype.on_data = function (data) {
}
};

RedisClient.prototype.return_error = function (err) {
var command_obj = this.command_queue.shift(), queue_len = this.command_queue.length;
err.command_used = command_obj.command.toUpperCase();

if (this.pub_sub_mode === false && queue_len === 0) {
this.command_queue = new Queue();
this.emit("idle");
var AUTH_ERROR = /NOAUTH|authentication required|operation not permitted/i;
function _return_error(client, err, command_obj) {
var queue_len = client.command_queue.length;

if (client.pub_sub_mode === false && queue_len === 0) {
client.command_queue = new Queue();
client.emit("idle");
}
if (this.should_buffer && queue_len <= this.command_queue_low_water) {
this.emit("drain");
this.should_buffer = false;
if (client.should_buffer && queue_len <= client.command_queue_low_water) {
client.emit("drain");
client.should_buffer = false;
}
if (command_obj.callback) {
command_obj.callback(err);
} else {
this.emit('error', err);
client.emit('error', err);
}
}


RedisClient.prototype.return_error = function (err) {
var command_obj = this.command_queue.shift();
err.command_used = command_obj.command.toUpperCase();
var should_retry_auth = this.auth_no_password &&
command_obj.command !== 'auth' &&
AUTH_ERROR.test(err.message);
if (should_retry_auth) {
debug('Authentication error, possibly password enabled, try to re-authenticate');
var self = this;
this.send_command('auth', [this.auth_pass], function (e, res) {
if (e) {
_return_error(self, err, command_obj);
} else {
self.auth_no_password = false;
send_command_obj(self, command_obj);
}
});
} else {
_return_error(this, err, command_obj);
}
};


// hgetall converts its replies to an Object. If the reply is empty, null is returned.
function reply_to_object(reply) {
var obj = {}, j, jl, key, val;
Expand Down Expand Up @@ -650,6 +710,7 @@ RedisClient.prototype.return_reply = function (reply) {
}
};


// This Command constructor is ever so slightly faster than using an object literal, but more importantly, using
// a named constructor helps it show up meaningfully in the V8 CPU profiler and in heap snapshots.
function Command(command, args, sub_command, buffer_args, callback) {
Expand Down Expand Up @@ -688,6 +749,7 @@ RedisClient.prototype.send_command = function (command, args, callback) {
throw new Error("send_command: last argument must be a callback or undefined");
}
} else {
console.log('args', args);
throw new Error("send_command: second argument must be an array");
}

Expand All @@ -713,6 +775,7 @@ RedisClient.prototype.send_command = function (command, args, callback) {
if (callback) {
return callback && callback(err);
}
err.command_used = command.toUpperCase();
this.emit("error", err);
return;
}
Expand Down Expand Up @@ -915,7 +978,7 @@ RedisClient.prototype.auth = RedisClient.prototype.AUTH = function (pass, callba
this.auth_callback = callback;
debug("Saving auth as " + this.auth_pass);
if (this.connected) {
this.send_command("auth", pass, callback);
this.send_command("auth", [pass], callback);
}
};

Expand Down
126 changes: 126 additions & 0 deletions test/enable_auth.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
'use strict';

var assert = require('assert');
var config = require('./lib/config');
var helper = require('./helper');
var redis = config.redis;


describe('enabling/changing password in redis', function() {
var auth = 'porkchopsandwiches';
var new_auth = 'fishsandwiches';
var client;

afterEach(function (done) {
client.config('set', 'requirepass', '', done);
});

helper.allTests(function(parser, ip, args) {
it('should re-authenticate if password is enabled in redis', function (done) {
var args = config.configureClient(parser, ip, {
auth_pass: auth
});
client = redis.createClient.apply(redis.createClient, args);
client.on('ready', function () {
testSet(1, function (err) {
if (err) return done(err);
setRedisPass(auth, done, function() {
testSet(2, authOk(auth, done));
});
});
});
});

it('should fail re-authenticating if different password is enabled', function (done) {
var args = config.configureClient(parser, ip, {
auth_pass: auth
});
client = redis.createClient.apply(redis.createClient, args);
client.on('ready', function () {
testSet(1, function (err) {
if (err) return done(err);
setRedisPass(new_auth, done, function() {
testSet(2, function (err) {
if (err) {
client.auth(new_auth, done);
} else {
done(new Error('it should have failed'));
}
});
});
});
});
});


it('should re-authenticate if the password is changed and new_auth_pass option is present', function (done) {
var args = config.configureClient(parser, ip, {
auth_pass: auth,
new_auth_pass: new_auth
});
client = redis.createClient.apply(redis.createClient, args);
var readyCount = 0;
client.on('ready', function () {
readyCount++;
if (readyCount === 1) {
setRedisPass(auth, done, function() {
testSet(1, function (err) {
if (err) return done(err);
setRedisPass(new_auth, done, function() {
// no authentication is needed when password is changed and the client already authenticated
testSet(2, function (err) {
if (err) return done(err);
// only on re-connection it will happen
client.stream.destroy();
});
});
});
});
} else {
testSet(3, authOk(new_auth, done));
}
});
});
});


function setRedisPass(pass, errCb, cb) {
client.config('set', 'requirepass', pass, function (err, res) {
if (err) errCb(err);
else cb(res);
});
}


function testSet(i, cb) {
var key = 'test_key_' + i,
value = 'test_value_' + i;
client.set(key, value, function (err, res) {
if (err) return cb(err);
assert.equal(res, 'OK');
client.get(key, function (err, res) {
if (err) return cb(err);
assert.equal(res, value);
client.del(key, function (err, res) {
if (err) return cb(err);
assert.equal(res, 1);
cb();
});
});
});
}


function authOk(pass, done) {
return function (err) {
if (err) {
client.auth(pass, function(e, res) {
if (e) return done(e);
done(new Error('Did not authenticate'));
});
} else {
done();
}
};
}
});