diff --git a/lib/client/client.js b/lib/client/client.js index bcd8c85..221dcc2 100644 --- a/lib/client/client.js +++ b/lib/client/client.js @@ -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, diff --git a/lib/dn.js b/lib/dn.js index 55549c2..bf0e85e 100644 --- a/lib/dn.js +++ b/lib/dn.js @@ -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 diff --git a/lib/messages/abandon_request.js b/lib/messages/abandon_request.js index 22637bd..5902b7e 100644 --- a/lib/messages/abandon_request.js +++ b/lib/messages/abandon_request.js @@ -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 diff --git a/lib/messages/add_request.js b/lib/messages/add_request.js index 5c8a33a..eb93772 100644 --- a/lib/messages/add_request.js +++ b/lib/messages/add_request.js @@ -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(); diff --git a/lib/messages/bind_request.js b/lib/messages/bind_request.js index ab31434..a676aeb 100644 --- a/lib/messages/bind_request.js +++ b/lib/messages/bind_request.js @@ -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(); diff --git a/lib/messages/compare_request.js b/lib/messages/compare_request.js index 8c81253..318ca9b 100644 --- a/lib/messages/compare_request.js +++ b/lib/messages/compare_request.js @@ -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(); diff --git a/lib/messages/del_request.js b/lib/messages/del_request.js index 11cf48c..92abcdc 100644 --- a/lib/messages/del_request.js +++ b/lib/messages/del_request.js @@ -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; diff --git a/lib/messages/ext_request.js b/lib/messages/ext_request.js index 0056ac7..bfd945b 100644 --- a/lib/messages/ext_request.js +++ b/lib/messages/ext_request.js @@ -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) { diff --git a/lib/messages/moddn_request.js b/lib/messages/moddn_request.js index 8efdba2..9fb7961 100644 --- a/lib/messages/moddn_request.js +++ b/lib/messages/moddn_request.js @@ -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) diff --git a/lib/messages/modify_request.js b/lib/messages/modify_request.js index bdd0043..2f6af12 100644 --- a/lib/messages/modify_request.js +++ b/lib/messages/modify_request.js @@ -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; diff --git a/lib/messages/noop_response.js.sav b/lib/messages/noop_response.js.sav new file mode 100644 index 0000000..42fa9e5 --- /dev/null +++ b/lib/messages/noop_response.js.sav @@ -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; }; diff --git a/lib/messages/search_request.js b/lib/messages/search_request.js index a31e49a..d78d8f2 100644 --- a/lib/messages/search_request.js +++ b/lib/messages/search_request.js @@ -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(); diff --git a/lib/messages/unbind_request.js b/lib/messages/unbind_request.js index bafa060..f8b7cca 100644 --- a/lib/messages/unbind_request.js +++ b/lib/messages/unbind_request.js @@ -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; diff --git a/lib/server.js b/lib/server.js index 6bc0f17..a9a42c5 100644 --- a/lib/server.js +++ b/lib/server.js @@ -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; diff --git a/test/dn.test.js b/test/dn.test.js index 96bbb51..23192e7 100644 --- a/test/dn.test.js +++ b/test/dn.test.js @@ -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(); +}); diff --git a/test/messages/bind_request.test.js b/test/messages/bind_request.test.js index 423471e..8ea84ae 100644 --- a/test/messages/bind_request.test.js +++ b/test/messages/bind_request.test.js @@ -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(); }); diff --git a/test/messages/compare_request.test.js b/test/messages/compare_request.test.js index ec3c971..de892ac 100644 --- a/test/messages/compare_request.test.js +++ b/test/messages/compare_request.test.js @@ -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(); diff --git a/test/server.test.js b/test/server.test.js index fa12fcb..c4e5c19 100644 --- a/test/server.test.js +++ b/test/server.test.js @@ -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(); + }); + }); + }); +});