Skip to content

Commit

Permalink
feat(sasl): Add support for SASL ANONYMOUS.
Browse files Browse the repository at this point in the history
By specifying an explicit Sasl mechanism via
policy.connect.saslMechanism, the user may opt-in to using/not-using a
particular Sasl security layer for AMQP.

Where:
 - **null** means: infer from the other policy settings and value
   supplied to connect (behaviour exactly as we have today, the default
   in policy.js)
 - **NONE** forces you not to attempt any SASL, raw AMQP only - if
   username / password are in the URL, they are ignored.
 - **PLAIN** forces you to try SASL plain - it's a client-side error not
   to have a username and password in the URL
 - **ANONYMOUS** forces you to try SASL anonymous - if username /
   password are in the URL, they are ignored.
 - **ANYTHING_ELSE** throws unsupported mechanism error

Fixes #250
  • Loading branch information
dnwe authored and mbroadst committed Oct 6, 2016
1 parent 98bd734 commit d067202
Show file tree
Hide file tree
Showing 4 changed files with 279 additions and 18 deletions.
33 changes: 29 additions & 4 deletions lib/amqp_client.js
Expand Up @@ -85,11 +85,14 @@ AMQPClient.ConnectionClosed = 'connection:closed';
*
* @method connect
* @param {string} url URI to connect to - right now only supports <code>amqp|amqps</code> as protocol.
* @param {Object} [policyOverrides] Policy overrides used for creating this connection
*
* @return {Promise}
*/
AMQPClient.prototype.connect = function(url) {
AMQPClient.prototype.connect = function(url, policyOverrides) {
var self = this;
policyOverrides = policyOverrides || {};
var connectPolicy = u.deepMerge(policyOverrides, self.policy.connect);
return new Promise(function(resolve, reject) {
if (self._connection) {
self._connection.close();
Expand All @@ -100,11 +103,33 @@ AMQPClient.prototype.connect = function(url) {
self._reconnect = self.connect.bind(self, url);
var address = self.policy.parseAddress(url);
self._defaultQueue = address.path.substr(1);
self.policy.connect.options.hostname = address.host;
var sasl = address.user ? new Sasl() : null;
connectPolicy.options.hostname = address.host;
var saslMechanism = connectPolicy.saslMechanism;
var sasl = null;
if (saslMechanism) {
if (!u.includes(Sasl.Mechanism, saslMechanism)) {
throw new errors.NotImplementedError(
saslMechanism + ' is not a supported saslMechanism policy');
}
if (saslMechanism === Sasl.Mechanism.NONE) {
if (address.user) {
console.warn(
'Sasl disabled by policy, but credentials provided in endpoint URI');
}
} else if (saslMechanism === Sasl.Mechanism.PLAIN && !address.user) {
throw new errors.AuthenticationError(
'Sasl PLAIN requested, but no credentials provided in endpoint URI');
} else {
sasl = new Sasl(saslMechanism);
}
} else if (address.user) {
// force SASL plain if no mechanism specified, but creds in URI
connectPolicy.saslMechanism = Sasl.Mechanism.PLAIN;
sasl = new Sasl(Sasl.Mechanism.PLAIN);
}
if (!!sasl && !!address.vhost) {
sasl._remoteHostname = address.vhost;
self.policy.connect.options.hostname = address.vhost;
connectPolicy.options.hostname = address.vhost;
}

self._connection = self._newConnection();
Expand Down
3 changes: 2 additions & 1 deletion lib/policies/policy.js
Expand Up @@ -60,7 +60,8 @@ function Policy(overrides) {
caFile: null,
rejectUnauthorized: false
}
}
},
saslMechanism: null // allow Sasl to be overriden by policy
},

session: {
Expand Down
53 changes: 40 additions & 13 deletions lib/sasl.js
Expand Up @@ -10,19 +10,34 @@ var debug = require('debug')('amqp10:sasl'),

Connection = require('./connection');

var saslMechanisms = {
PLAIN: 'PLAIN',
ANONYMOUS: 'ANONYMOUS',
NONE: 'NONE'
};

/**
* Currently, only supports SASL-PLAIN
* Currently, only supports SASL ANONYMOUS or PLAIN
*
* @constructor
*/
function Sasl() {
function Sasl(mechanism) {
if (mechanism && !u.includes(saslMechanisms, mechanism)) {
throw new errors.NotImplementedError(
'Only SASL PLAIN and ANONYMOUS are supported.');
}
this.mechanism = mechanism;
this.receivedHeader = false;
}

Sasl.Mechanism = saslMechanisms;

Sasl.prototype.negotiate = function(connection, credentials, done) {
u.assertArguments(credentials, ['user', 'pass']);
this.connection = connection;
this.credentials = credentials;
if (this.credentials.user && this.credentials.pass && !this.mechanism) {
this.mechanism = Sasl.Mechanism.PLAIN;
}
this.callback = done;
var self = this;
this._processFrameEH = function(frame) { self._processFrame(frame); };
Expand All @@ -46,18 +61,30 @@ Sasl.prototype.headerReceived = function(header) {

Sasl.prototype._processFrame = function(frame) {
if (frame instanceof frames.SaslMechanismsFrame) {
if (!u.includes(frame.saslServerMechanisms, 'PLAIN')) {
throw new errors.NotImplementedError('Only supports SASL-PLAIN at the moment.');
}

debug('Sending ' + this.credentials.user + ':' + this.credentials.pass);
var buf = new Builder();
buf.appendUInt8(0); // <null>
buf.appendString(this.credentials.user);
buf.appendUInt8(0); // <null>
buf.appendString(this.credentials.pass);
var mechanism = this.mechanism;
if (!u.includes(frame.saslServerMechanisms, mechanism)) {
throw new errors.AuthenticationError(
'SASL ' + mechanism + ' not supported by remote.');
}
if (mechanism === Sasl.Mechanism.PLAIN) {
debug('Sending ' + this.credentials.user + ':' + this.credentials.pass);
buf.appendUInt8(0); // <null>
buf.appendString(this.credentials.user);
buf.appendUInt8(0); // <null>
buf.appendString(this.credentials.pass);
} else if (mechanism === Sasl.Mechanism.ANONYMOUS) {
if (this.credentials.user && this.credentials.pass) {
console.warn(
'Sasl ANONYMOUS requested, but credentials provided in endpoint URI');
}
buf.appendUInt8(0); // <null>
} else {
throw new errors.NotImplementedError(
'Only SASL PLAIN and ANONYMOUS are supported.');
}
var initFrame = new frames.SaslInitFrame({
mechanism: 'PLAIN',
mechanism: mechanism,
initialResponse: buf.get()
});

Expand Down
208 changes: 208 additions & 0 deletions test/unit/policies/policy.test.js
@@ -0,0 +1,208 @@
'use strict';
var amqp = require('../../../lib'),
MockServer = require('../mocks').Server,
Builder = require('buffer-builder'),

constants = require('../../../lib/constants'),
frames = require('../../../lib/frames'),
errors = require('../../../lib/errors'),
Sasl = require('../../../lib/sasl'),
AMQPError = require('../../../lib/types/amqp_error'),
ErrorCondition = require('../../../lib/types/error_condition'),

expect = require('chai').expect,
test = require('../test-fixture');

function buildInitialResponseFor(user, pass) {
var buf = new Builder();
buf.appendUInt8(0); // <nul>
if (user) {
buf.appendString(user);
buf.appendUInt8(0); // <nul>
}
if (pass) {
buf.appendString(pass);
}
return buf.get();
}

describe('Default Policy', function() {
describe('#connect()', function() {
beforeEach(function() {
if (!!test.server) test.server = undefined;
if (!!test.client) test.client = undefined;
test.server = new MockServer();
return test.server.setup();
});

afterEach(function() {
if (!test.server) return;
return test.server.teardown()
.then(function() {
test.server = undefined;
});
});

it('should not add a SASL layer for anonymous auth by default', function() {
var policy = amqp.Policy.merge({
connect: {
options: {
containerId: 'test-client'
}
}
}, amqp.Policy.DefaultPolicy);

test.server.setExpectedFrameSequence([
constants.amqpVersion,
new frames.OpenFrame({containerId: 'test-client', hostname: '127.0.0.1'})
]);

test.server.setResponseSequence([
constants.amqpVersion,
new frames.OpenFrame(policy.connect.options),
new frames.BeginFrame({
remoteChannel: 1,
nextOutgoingId: 0,
incomingWindow: 2147483647,
outgoingWindow: 2147483647,
handleMax: 4294967295
}),
new frames.CloseFrame({
error: new AMQPError({
condition: ErrorCondition.ConnectionForced,
description: 'test'
})
})
]);

test.client = new amqp.Client(policy);
return test.client.connect(test.server.address())
.then(function() {
return test.client.disconnect();
});
});

it('should send SASL ANONYMOUS when requested', function() {
var policy = amqp.Policy.merge({
connect: {
options: {
containerId: 'test-client2'
},
saslMechanism: Sasl.Mechanism.ANONYMOUS
}
}, amqp.Policy.DefaultPolicy);
test.server.setExpectedFrameSequence([
constants.saslVersion,
new frames.SaslInitFrame({
mechanism: 'ANONYMOUS',
initialResponse: buildInitialResponseFor()
}),
new frames.OpenFrame(
{containerId: 'test-client2', hostname: '127.0.0.1'})
]);

test.server.setResponseSequence([
[
constants.saslVersion,
new frames.SaslMechanismsFrame(
{saslServerMechanisms: ['ANONYMOUS', 'PLAIN']})
],
new frames.SaslOutcomeFrame({code: constants.saslOutcomes.ok}),
constants.amqpVersion,
new frames.OpenFrame(policy.connect.options),
new frames.BeginFrame({
remoteChannel: 1,
nextOutgoingId: 0,
incomingWindow: 2147483647,
outgoingWindow: 2147483647,
handleMax: 4294967295
}),
new frames.CloseFrame({
error: new AMQPError({
condition: ErrorCondition.ConnectionForced,
description: 'test'
})
})
]);

test.client = new amqp.Client(policy);
return test.client.connect(test.server.address())
.then(function() {
return test.client.disconnect();
});
});

it('should disable SASL when requested (even if URI has creds)', function() {
var policy = amqp.Policy.merge({
connect: {
options: {
containerId: 'test-client3'
},
saslMechanism: Sasl.Mechanism.NONE
}
}, amqp.Policy.DefaultPolicy);
test.server.setExpectedFrameSequence([
constants.amqpVersion,
new frames.OpenFrame({containerId: 'test-client3', hostname: '127.0.0.1'})
]);

test.server.setResponseSequence([
constants.amqpVersion,
new frames.OpenFrame(policy.connect.options),
new frames.BeginFrame({
remoteChannel: 1,
nextOutgoingId: 0,
incomingWindow: 2147483647,
outgoingWindow: 2147483647,
handleMax: 4294967295
}),
new frames.CloseFrame({
error: new AMQPError({
condition: ErrorCondition.ConnectionForced,
description: 'test'
})
})
]);

test.client = new amqp.Client(policy);
var addr = 'amqp://user1:pass1@' + test.server.address().slice(7);
return test.client.connect(addr)
.then(function() {
return test.client.disconnect();
});
});

it('should reject connect if SASL PLAIN requested but no creds supplied',
function() {
var policy = amqp.Policy.merge({
connect: {
saslMechanism: Sasl.Mechanism.PLAIN
}
}, amqp.Policy.DefaultPolicy);

test.client = new amqp.Client(policy);
return test.client.connect(test.server.address())
.then(function() {
throw new Error();
}).catch(function(err) {
expect(err).to.be.an.instanceOf(errors.AuthenticationError);
});
});

it('should reject connect if SASL RANDOM requested', function() {
var policy = amqp.Policy.merge({
connect: {
saslMechanism: 'random'
}
}, amqp.Policy.DefaultPolicy);
test.client = new amqp.Client(policy);
return test.client.connect(test.server.address())
.then(function() {
throw new Error();
}).catch(function(err) {
expect(err).to.be.an.instanceOf(errors.NotImplementedError);
});
});
});

});

0 comments on commit d067202

Please sign in to comment.