Support invalid DNs in client/server

Some LDAP implementations (mainly AD and Outlook) accept and/or output
DNs that are not valid.  To support interaction with these invalid DNs a
strictDN flag (default: true) has been added to the client and server
constructors.  Setting this flag to false will allow use of
non-conforming DNs.

When disabling strictDN in the ldapjs client, strings which wouldn't
parse into a DN can then be passed to the ldap operation methods.  It
also means that some methods (such as search) may return results with
string-formatted DNs instead of DN objects.

When disabling strictDN in the ldapjs server, incoming requests that
contain invalid DNs will be routed to the default ('') handler for that
operation type.  It is your responsiblity to differentiate between
string-type and object-type DNs in those handlers.

Fix mcavage/node-ldapjs#222
Fix mcavage/node-ldapjs#146
Fix mcavage/node-ldapjs#113
Fix mcavage/node-ldapjs#104
This commit is contained in:
Patrick Mooney 2014-09-30 18:39:19 -05:00
parent 58f58883cd
commit 408e7c9f99
18 changed files with 211 additions and 109 deletions

View File

@ -80,6 +80,18 @@ function validateControls(controls) {
return controls;
}
function ensureDN(input, strict) {
if (dn.DN.isDN(input)) {
return dn;
} else if (strict) {
return dn.parse(input);
} else if (typeof (input) === 'string') {
return input;
} else {
throw new Error('invalid DN');
}
}
/**
* Queue to contain LDAP requests.
*
@ -318,6 +330,7 @@ function Client(options) {
failAfter: parseInt(rOpts.failAfter, 10) || Infinity
};
}
this.strictDN = (options.strictDN !== undefined) ? options.strictDN : true;
this.queue = new RequestQueue({
size: parseInt((options.queueSize || 0), 10),
@ -394,7 +407,7 @@ Client.prototype.abandon = function abandon(messageID, controls, callback) {
* @throws {TypeError} on invalid input.
*/
Client.prototype.add = function add(name, entry, controls, callback) {
assert.string(name, 'name');
assert.ok(name, 'name');
assert.object(entry, 'entry');
if (typeof (controls) === 'function') {
callback = controls;
@ -427,7 +440,7 @@ Client.prototype.add = function add(name, entry, controls, callback) {
}
var req = new AddRequest({
entry: dn.parse(name),
entry: ensureDN(name, this.strictDN),
attributes: entry,
controls: controls
});
@ -487,7 +500,7 @@ Client.prototype.compare = function compare(name,
value,
controls,
callback) {
assert.string(name, 'name');
assert.ok(name, 'name');
assert.string(attr, 'attr');
assert.string(value, 'value');
if (typeof (controls) === 'function') {
@ -499,7 +512,7 @@ Client.prototype.compare = function compare(name,
assert.func(callback, 'callback');
var req = new CompareRequest({
entry: dn.parse(name),
entry: ensureDN(name, this.strictDN),
attribute: attr,
value: value,
controls: controls
@ -523,7 +536,7 @@ Client.prototype.compare = function compare(name,
* @throws {TypeError} on invalid input.
*/
Client.prototype.del = function del(name, controls, callback) {
assert.string(name, 'name');
assert.ok(name, 'name');
if (typeof (controls) === 'function') {
callback = controls;
controls = [];
@ -533,7 +546,7 @@ Client.prototype.del = function del(name, controls, callback) {
assert.func(callback, 'callback');
var req = new DeleteRequest({
entry: dn.parse(name),
entry: ensureDN(name, this.strictDN),
controls: controls
});
@ -596,7 +609,7 @@ Client.prototype.exop = function exop(name, value, controls, callback) {
* @throws {TypeError} on invalid input.
*/
Client.prototype.modify = function modify(name, change, controls, callback) {
assert.string(name, 'name');
assert.ok(name, 'name');
assert.object(change, 'change');
var changes = [];
@ -651,7 +664,7 @@ Client.prototype.modify = function modify(name, change, controls, callback) {
assert.func(callback, 'callback');
var req = new ModifyRequest({
object: dn.parse(name),
object: ensureDN(name, this.strictDN),
changes: changes,
controls: controls
});
@ -678,20 +691,18 @@ Client.prototype.modifyDN = function modifyDN(name,
newName,
controls,
callback) {
if (typeof (name) !== 'string')
throw new TypeError('name (string) required');
if (typeof (newName) !== 'string')
throw new TypeError('newName (string) required');
assert.ok(name, 'name');
assert.string(newName, 'newName');
if (typeof (controls) === 'function') {
callback = controls;
controls = [];
} else {
controls = validateControls(controls);
}
if (typeof (callback) !== 'function')
throw new TypeError('callback (function) required');
assert.func(callback);
var DN = dn.parse(name);
var DN = ensureDN(name);
// TODO: is non-strict handling desired here?
var newDN = dn.parse(newName);
var req = new ModifyDNRequest({
@ -739,8 +750,7 @@ Client.prototype.search = function search(base,
controls,
callback,
_bypass) {
if (typeof (base) !== 'string' && !(base instanceof dn.DN))
throw new TypeError('base (string) required');
assert.ok(base, 'base');
if (Array.isArray(options) || (options instanceof Control)) {
controls = options;
options = {};
@ -762,15 +772,13 @@ Client.prototype.search = function search(base,
} else if (!filters.isFilter(options.filter)) {
throw new TypeError('options.filter (Filter) required');
}
if (typeof (controls) === 'function') {
callback = controls;
controls = [];
} else {
controls = validateControls(controls);
}
if (typeof (callback) !== 'function')
throw new TypeError('callback (function) required');
assert.func(callback, 'callback');
if (options.attributes) {
if (!Array.isArray(options.attributes)) {
@ -783,9 +791,10 @@ Client.prototype.search = function search(base,
}
var self = this;
var baseDN = ensureDN(base, this.strictDN);
function sendRequest(ctrls, emitter, cb) {
var req = new SearchRequest({
baseObject: typeof (base) === 'string' ? dn.parse(base) : base,
baseObject: baseDN,
scope: options.scope || 'base',
filter: options.filter,
derefAliases: options.derefAliases || Protocol.NEVER_DEREF_ALIASES,

View File

@ -218,6 +218,7 @@ DN.prototype.toString = function () {
return _dn.join(this.rdnSpaced ? ', ' : ',');
};
DN.prototype.spaced = function (spaces) {
this.rdnSpaced = (spaces === false) ? false : true;
return this;
@ -357,6 +358,20 @@ DN.prototype.unshift = function (rdn) {
};
DN.isDN = function (dn) {
if (!dn || typeof (dn) !== 'object') {
return false;
}
if (dn instanceof DN) {
return true;
}
if (Array.isArray(dn.rdns)) {
// Really simple duck-typing for now
return true;
}
return false;
};
///--- Exports

View File

@ -3,19 +3,9 @@
var assert = require('assert');
var util = require('util');
var asn1 = require('asn1');
var LDAPMessage = require('./message');
var LDAPResult = require('./result');
var dn = require('../dn');
var Attribute = require('../attribute');
var Protocol = require('../protocol');
///--- Globals
var Ber = asn1.Ber;
///--- API

View File

@ -3,19 +3,16 @@
var assert = require('assert');
var util = require('util');
var asn1 = require('asn1');
var LDAPMessage = require('./message');
var LDAPResult = require('./result');
var dn = require('../dn');
var Attribute = require('../attribute');
var Protocol = require('../protocol');
///--- Globals
var Ber = asn1.Ber;
var isDN = dn.DN.isDN;
///--- API
@ -24,8 +21,10 @@ function AddRequest(options) {
if (options) {
if (typeof (options) !== 'object')
throw new TypeError('options must be an object');
if (options.entry && !(options.entry instanceof dn.DN))
throw new TypeError('options.entry must be a DN');
if (options.entry &&
!(isDN(options.entry) || typeof (options.entry) === 'string')) {
throw new TypeError('options.entry must be a DN or string');
}
if (options.attributes) {
if (!Array.isArray(options.attributes))
throw new TypeError('options.attributes must be [Attribute]');
@ -55,7 +54,7 @@ module.exports = AddRequest;
AddRequest.prototype._parse = function (ber) {
assert.ok(ber);
this.entry = dn.parse(ber.readString());
this.entry = ber.readString();
ber.readSequence();

View File

@ -6,16 +6,11 @@ var util = require('util');
var asn1 = require('asn1');
var LDAPMessage = require('./message');
var LDAPResult = require('./result');
var dn = require('../dn');
var Protocol = require('../protocol');
///--- Globals
var Ber = asn1.Ber;
var LDAP_BIND_SIMPLE = 'simple';
var LDAP_BIND_SASL = 'sasl';
@ -49,7 +44,7 @@ BindRequest.prototype._parse = function (ber) {
assert.ok(ber);
this.version = ber.readInt();
this.name = dn.parse(ber.readString());
this.name = ber.readString();
var t = ber.peek();

View File

@ -4,21 +4,26 @@ var assert = require('assert');
var util = require('util');
var LDAPMessage = require('./message');
var LDAPResult = require('./result');
var dn = require('../dn');
var Protocol = require('../protocol');
///--- Globals
var isDN = dn.DN.isDN;
///--- API
function CompareRequest(options) {
if (options) {
if (typeof (options) !== 'object')
throw new TypeError('options must be an object');
if (options.entry && !(options.entry instanceof dn.DN))
throw new TypeError('options.entry must be a DN');
if (options.entry &&
!(isDN(options.entry) || typeof (options.entry) === 'string')) {
throw new TypeError('options.entry must be a DN or string');
}
if (options.attribute && typeof (options.attribute) !== 'string')
throw new TypeError('options.attribute must be a string');
if (options.value && typeof (options.value) !== 'string')
@ -36,9 +41,7 @@ function CompareRequest(options) {
var self = this;
this.__defineGetter__('type', function () { return 'CompareRequest'; });
this.__defineGetter__('_dn', function () {
return self.entry ? self.entry.toString() : '';
});
this.__defineGetter__('_dn', function () { return self.entry; });
}
util.inherits(CompareRequest, LDAPMessage);
module.exports = CompareRequest;
@ -47,7 +50,7 @@ module.exports = CompareRequest;
CompareRequest.prototype._parse = function (ber) {
assert.ok(ber);
this.entry = dn.parse(ber.readString());
this.entry = ber.readString();
ber.readSequence();
this.attribute = ber.readString().toLowerCase();

View File

@ -3,19 +3,15 @@
var assert = require('assert');
var util = require('util');
var asn1 = require('asn1');
var LDAPMessage = require('./message');
var LDAPResult = require('./result');
var dn = require('../dn');
var Attribute = require('../attribute');
var Protocol = require('../protocol');
///--- Globals
var Ber = asn1.Ber;
var isDN = dn.DN.isDN;
///--- API
@ -24,8 +20,10 @@ function DeleteRequest(options) {
if (options) {
if (typeof (options) !== 'object')
throw new TypeError('options must be an object');
if (options.entry && !(options.entry instanceof dn.DN))
throw new TypeError('options.entry must be a DN');
if (options.entry &&
!(isDN(options.entry) || typeof (options.entry) === 'string')) {
throw new TypeError('options.entry must be a DN or string');
}
} else {
options = {};
}
@ -46,7 +44,7 @@ module.exports = DeleteRequest;
DeleteRequest.prototype._parse = function (ber, length) {
assert.ok(ber);
this.entry = dn.parse(ber.buffer.slice(0, length).toString('utf8'));
this.entry = ber.buffer.slice(0, length).toString('utf8');
ber._offset += ber.length;
return true;

View File

@ -3,22 +3,11 @@
var assert = require('assert');
var util = require('util');
var asn1 = require('asn1');
var LDAPMessage = require('./message');
var LDAPResult = require('./result');
var dn = require('../dn');
var Protocol = require('../protocol');
///--- Globals
var Ber = asn1.Ber;
///--- API
function ExtendedRequest(options) {

View File

@ -3,19 +3,14 @@
var assert = require('assert');
var util = require('util');
var asn1 = require('asn1');
var LDAPMessage = require('./message');
var LDAPResult = require('./result');
var dn = require('../dn');
var Protocol = require('../protocol');
var dn = require('../dn');
///--- Globals
var Ber = asn1.Ber;
var isDN = dn.DN.isDN;
///--- API
@ -24,14 +19,16 @@ function ModifyDNRequest(options) {
if (options) {
if (typeof (options) !== 'object')
throw new TypeError('options must be an object');
if (options.entry && !(options.entry instanceof dn.DN))
throw new TypeError('options.entry must be a DN');
if (options.newRdn && !(options.newRdn instanceof dn.DN))
if (options.entry &&
!(isDN(options.entry) || typeof (options.entry) === 'string')) {
throw new TypeError('options.entry must be a DN or string');
}
if (options.newRdn && !isDN(options.newRdn))
throw new TypeError('options.newRdn must be a DN');
if (options.deleteOldRdn !== undefined &&
typeof (options.deleteOldRdn) !== 'boolean')
throw new TypeError('options.deleteOldRdn must be a boolean');
if (options.newSuperior && !(options.newSuperior instanceof dn.DN))
if (options.newSuperior && !isDN(options.newSuperior))
throw new TypeError('options.newSuperior must be a DN');
} else {
@ -57,7 +54,7 @@ module.exports = ModifyDNRequest;
ModifyDNRequest.prototype._parse = function (ber) {
assert.ok(ber);
this.entry = dn.parse(ber.readString());
this.entry = ber.readString();
this.newRdn = dn.parse(ber.readString());
this.deleteOldRdn = ber.readBoolean();
if (ber.peek() === 0x80)

View File

@ -4,8 +4,6 @@ var assert = require('assert');
var util = require('util');
var LDAPMessage = require('./message');
var LDAPResult = require('./result');
var dn = require('../dn');
var Attribute = require('../attribute');
var Change = require('../change');
@ -13,14 +11,21 @@ var Protocol = require('../protocol');
///--- API
var isDN = dn.DN.isDN;
///--- API
function ModifyRequest(options) {
if (options) {
if (typeof (options) !== 'object')
throw new TypeError('options must be an object');
if (options.object && !(options.object instanceof dn.DN))
throw new TypeError('options.object must be a DN');
if (options.object &&
!(isDN(options.object) || typeof (options.object) === 'string')) {
throw new TypeError('options.object must be a DN or string');
}
if (options.attributes) {
if (!Array.isArray(options.attributes))
throw new TypeError('options.attributes must be [Attribute]');
@ -50,7 +55,7 @@ module.exports = ModifyRequest;
ModifyRequest.prototype._parse = function (ber) {
assert.ok(ber);
this.object = dn.parse(ber.readString());
this.object = ber.readString();
ber.readSequence();
var end = ber.offset + ber.length;

View File

@ -0,0 +1,19 @@
// Copyright 2014 Joyent, Inc. All rights reserved.
function NoOpResponse(options) {
if (!options)
options = {};
if (typeof (options) !== 'object')
throw new TypeError('options must be an object');
options.protocolOp = 0;
LDAPMessage.call(this, options);
this.__defineGetter__('type', function () { return 'NoOpResponse'; });
}
util.inherits(NoOpResponse, LDAPMessage);
module.exports = NoOpResponse;
NoOpResponse.prototype.end = function () {};
NoOpResponse.prototype._json = function (j) { return j; };

View File

@ -7,7 +7,6 @@ var asn1 = require('asn1');
var LDAPMessage = require('./message');
var LDAPResult = require('./result');
var dn = require('../dn');
var filters = require('../filters');
var Protocol = require('../protocol');
@ -16,10 +15,10 @@ var Protocol = require('../protocol');
///--- Globals
var DN = dn.DN;
var Ber = asn1.Ber;
///--- API
function SearchRequest(options) {
@ -35,9 +34,7 @@ function SearchRequest(options) {
var self = this;
this.__defineGetter__('type', function () { return 'SearchRequest'; });
this.__defineGetter__('_dn', function () {
return self.baseObject;
});
this.__defineGetter__('_dn', function () { return self.baseObject; });
this.__defineGetter__('scope', function () {
switch (self._scope) {
case Protocol.SCOPE_BASE_OBJECT: return 'base';
@ -67,7 +64,7 @@ function SearchRequest(options) {
}
});
this.baseObject = options.baseObject || new dn.DN([ {} ]);
this.baseObject = options.baseObject || new DN([ {} ]);
this.scope = options.scope || 'base';
this.derefAliases = options.derefAliases || Protocol.NEVER_DEREF_ALIASES;
this.sizeLimit = options.sizeLimit || 0;
@ -93,7 +90,7 @@ SearchRequest.prototype.newResult = function () {
SearchRequest.prototype._parse = function (ber) {
assert.ok(ber);
this.baseObject = dn.parse(ber.readString());
this.baseObject = ber.readString();
this.scope = ber.readEnumeration();
this.derefAliases = ber.readEnumeration();
this.sizeLimit = ber.readInt();

View File

@ -3,11 +3,7 @@
var assert = require('assert');
var util = require('util');
var asn1 = require('asn1');
var LDAPMessage = require('./message');
var LDAPResult = require('./result');
var dn = require('../dn');
var Protocol = require('../protocol');
@ -15,8 +11,6 @@ var Protocol = require('../protocol');
///--- Globals
var Ber = asn1.Ber;
var DN = dn.DN;
var RDN = dn.RDN;

View File

@ -124,6 +124,45 @@ function getResponse(req) {
}
function decodeDN(req, strict) {
assert.ok(req);
var parse;
if (strict) {
parse = dn.parse;
} else {
parse = function (input) {
try {
return dn.parse(input);
} catch (e) {
return input;
}
};
}
switch (req.protocolOp) {
case Protocol.LDAP_REQ_BIND:
req.name = parse(req.name);
break;
case Protocol.LDAP_REQ_ADD:
case Protocol.LDAP_REQ_COMPARE:
case Protocol.LDAP_REQ_DELETE:
req.entry = parse(req.entry);
break;
case Protocol.LDAP_REQ_MODIFY:
req.object = parse(req.object);
break;
case Protocol.LDAP_REQ_MODRDN:
req.entry = parse(req.entry);
// TODO: handle newRdn/Superior
break;
case Protocol.LDAP_REQ_SEARCH:
req.baseObject = parse(req.baseObject);
break;
default:
break;
}
}
function defaultHandler(req, res, next) {
assert.ok(req);
assert.ok(res);
@ -276,6 +315,7 @@ function Server(options) {
this._chain = [];
this.log = options.log;
this.strictDN = (options.strictDN !== undefined) ? options.strictDN : true;
var log = this.log;
@ -355,6 +395,9 @@ function Server(options) {
return false;
}
// parse string DNs for routing/etc
decodeDN(req, this.strictDN);
res.connection = c;
res.logId = req.logId;
res.requestDN = req.dn;
@ -808,12 +851,15 @@ Server.prototype._getHandlerChain = function _getHandlerChain(req, res) {
assert.ok(req.dn);
var keys = this._sortedRouteKeys();
var fallbackHandler = [noSuffixHandler];
// invalid DNs in non-strict mode are routed to the default handler
var testDN = (typeof (req.dn) === 'string') ? '' : req.dn;
for (var i = 0; i < keys.length; i++) {
var suffix = keys[i];
route = routes[suffix];
assert.ok(route.dn);
// Match a valid route or the route wildcard ('')
if (route.dn.equals(req.dn) || route.dn.parentOf(req.dn) || suffix === '') {
if (route.dn.equals(testDN) || route.dn.parentOf(testDN) || suffix === '') {
if (route[op]) {
// We should be good to go.
req.suffix = route.dn;

View File

@ -143,3 +143,18 @@ test('rdn spacing', function (t) {
t.equals(dn2.spaced(false).toString(), 'cn=foo,dc=bar');
t.end();
});
test('isDN duck-testing', function (t) {
var valid = dn.parse('cn=foo');
var isDN = dn.DN.isDN;
t.notOk(isDN(null));
t.notOk(isDN('cn=foo'));
t.ok(isDN(valid));
var duck = {
rdns: [ {look: 'ma'}, {a: 'dn'} ],
toString: function () { return 'look=ma, a=dn'; }
};
t.ok(isDN(duck));
t.end();
});

View File

@ -53,8 +53,6 @@ test('parse', function (t) {
t.ok(req._parse(new BerReader(ber.buffer)));
t.equal(req.version, 3);
t.equal(req.dn.toString(), 'cn=root');
t.ok(req.name.constructor);
t.equal(req.name.constructor.name, 'DN');
t.equal(req.credentials, 'secret');
t.end();
});

View File

@ -36,7 +36,7 @@ test('new with args', function (t) {
value: 'testy'
});
t.ok(req);
t.equal(req.dn, 'cn=foo, o=test');
t.equal(req.dn.toString(), 'cn=foo, o=test');
t.equal(req.attribute, 'sn');
t.equal(req.value, 'testy');
t.end();

View File

@ -196,3 +196,36 @@ test('route unbind', function (t) {
});
});
});
test('non-strict route', function (t) {
server = ldap.createServer({
strictDN: false
});
sock = getSock();
var testDN = 'this ain\'t a DN';
// invalid DNs go to default handler
server.search('', function (req, res, next) {
t.ok(req.dn);
t.equal(typeof (req.dn), 'string');
t.equal(req.dn, testDN);
res.end();
next();
});
server.listen(sock, function () {
t.ok(true, 'server startup');
var clt = ldap.createClient({
socketPath: sock,
strictDN: false
});
clt.search(testDN, {scope: 'base'}, function (err, res) {
t.ifError(err);
res.on('end', function () {
clt.destroy();
server.close();
t.end();
});
});
});
});