diff --git a/lib/server.js b/lib/server.js index 89da85e..7190917 100644 --- a/lib/server.js +++ b/lib/server.js @@ -716,12 +716,45 @@ Server.prototype._getRoute = function (_dn, backend) { this.routes[name] = {}; this.routes[name].backend = backend; this.routes[name].dn = _dn; + // Force regeneration of the route key cache on next request + this._routeKeyCache = null; } return this.routes[name]; }; +Server.prototype._sortedRouteKeys = function _sortedRouteKeys() { + // The filtered/sorted route keys are cached to prevent needlessly + // regenerating the list for every incoming request. + if (!this._routeKeyCache) { + var self = this; + var reversedRDNsToKeys = {}; + // Generate mapping of reversedRDNs(DN) -> routeKey + Object.keys(this.routes).forEach(function (key) { + var _dn = self.routes[key].dn; + // Ignore non-DN routes such as exop or unbind + if (_dn instanceof dn.DN) { + var reversed = _dn.clone(); + reversed.rdns.reverse(); + reversedRDNsToKeys[reversed.spaced(true).toString()] = key; + } + }); + var output = []; + // Reverse-sort on reversedRDS(DN) in order to output routeKey list. + // This will place more specific DNs in front of their parents: + // 1. dc=test, dc=domain, dc=sub + // 2. dc=test, dc=domain + // 3. dc=other, dc=foobar + Object.keys(reversedRDNsToKeys).sort().reverse().forEach(function (_dn) { + output.push(reversedRDNsToKeys[_dn]); + }); + this._routeKeyCache = output; + } + return this._routeKeyCache; +}; + + Server.prototype._getHandlerChain = function _getHandlerChain(req, res) { assert.ok(req); @@ -738,59 +771,70 @@ Server.prototype._getHandlerChain = function _getHandlerChain(req, res) { } var op = '0x' + req.protocolOp.toString(16); - var self = this; var routes = this.routes; - var keys = Object.keys(routes); - for (var i = 0; i < keys.length; i++) { - var r = keys[i]; - var route = routes[r]; + var route; - // Special cases are abandons, exops and unbinds, handle those first. - if (req.protocolOp === Protocol.LDAP_REQ_EXTENSION) { - if (r === req.requestName) { - return { - backend: routes.backend, - handlers: route[op] || [noExOpHandler] - }; - } - } else if (req.protocolOp === Protocol.LDAP_REQ_UNBIND) { + // Special cases are exops, unbinds and abandons. Handle those first. + if (req.protocolOp === Protocol.LDAP_REQ_EXTENSION) { + route = routes[req.requestName]; + if (route) { return { - backend: routes['unbind'] ? routes['unbind'].backend : self, - handlers: function getUnbindChain() { - if (routes['unbind'] && routes['unbind'][op]) - return routes['unbind'][op]; - - self.log.debug('%s unbind request %j', req.logId, req.json); - return [defaultNoOpHandler]; - } + backend: route.backend, + handlers: (route[op] ? route[op] : [noExOpHandler]) }; - } else if (req.protocolOp === Protocol.LDAP_REQ_ABANDON) { + } else { return { backend: self, - handlers: [defaultNoOpHandler] + handlers: [noExOpHandler] }; - } else if (route[op]) { - // Otherwise, match via DN rules - assert.ok(req.dn); - assert.ok(route.dn); + } + } else if (req.protocolOp === Protocol.LDAP_REQ_UNBIND) { + route = routes['unbind']; + return { + backend: route ? route.backend : self, + handlers: function getUnbindChain() { + if (route && route[op]) + return route[op]; - if (route.dn.equals(req.dn) || route.dn.parentOf(req.dn)) { + self.log.debug('%s unbind request %j', req.logId, req.json); + return [defaultNoOpHandler]; + } + }; + } else if (req.protocolOp === Protocol.LDAP_REQ_ABANDON) { + return { + backend: self, + handlers: [defaultNoOpHandler] + }; + } + + // Otherwise, match via DN rules + assert.ok(req.dn); + var keys = this._sortedRouteKeys(); + var fallbackHandler = [noSuffixHandler]; + 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[op]) { // We should be good to go. req.suffix = route.dn; return { backend: route.backend, - handlers: route[op] || [defaultHandler] + handlers: route[op] }; + } else { + // We found a valid suffix but not a valid operation. + // There might be a more generic suffix with a legitimate operation. + fallbackHandler = [defaultHandler]; } } } - - // We're here, so nothing matched. return { backend: self, - handlers: [(req.protocolOp !== Protocol.LDAP_REQ_EXTENSION ? - noSuffixHandler : noExOpHandler)] + handlers: fallbackHandler }; }; diff --git a/package.json b/package.json index 8fa54e2..88122c9 100644 --- a/package.json +++ b/package.json @@ -38,7 +38,8 @@ }, "devDependencies": { "tap": "0.4.1", - "node-uuid": "1.4.0" + "node-uuid": "1.4.0", + "vasync": "1.4.3" }, "scripts": { "test": "./node_modules/.bin/tap ./test" diff --git a/test/server.test.js b/test/server.test.js new file mode 100644 index 0000000..62263e3 --- /dev/null +++ b/test/server.test.js @@ -0,0 +1,158 @@ +// Copyright 2011 Mark Cavage, Inc. All rights reserved. + +var Logger = require('bunyan'); + +var test = require('tap').test; +var uuid = require('node-uuid'); +var vasync = require('vasync'); + + +///--- Globals + +var BIND_DN = 'cn=root'; +var BIND_PW = 'secret'; + +var SUFFIX = 'dc=test'; + +var ldap; +var Attribute; +var Change; +var client; +var server; +var sock; + +function getSock() { + if (process.platform === 'win32') { + return '\\\\.\\pipe\\' + uuid(); + } else { + return '/tmp/.' + uuid(); + } +} + +///--- Tests + +test('load library', function (t) { + ldap = require('../lib/index'); + t.ok(ldap.createServer); + t.end(); +}); + +test('basic create', function (t) { + server = ldap.createServer(); + t.ok(server); + t.end(); +}); + +test('listen on unix/named socket', { timeout: 1000 }, function (t) { + t.plan(1); + server = ldap.createServer(); + sock = getSock(); + server.listen(sock, function () { + t.ok(true); + server.close(); + }); +}); + +test('listen on ephemeral port', { timeout: 1000 }, function (t) { + t.plan(2); + server = ldap.createServer(); + server.listen(0, 'localhost', function () { + var addr = server.address(); + t.ok(addr.port > 0); + t.ok(addr.port < 65535); + server.close(); + }); +}); + + +test('route order', function (t) { + function generateHandler(response) { + var func = function handler(req, res, next) { + res.send({ + dn: response, + attributes: { } + }); + res.end(); + return next(); + }; + return func; + } + + server = ldap.createServer(); + sock = getSock(); + var dnShort = SUFFIX; + var dnMed = 'dc=sub, ' + SUFFIX; + var dnLong = 'dc=long, dc=sub, ' + SUFFIX; + + // Mount routes out of order + server.search(dnMed, generateHandler(dnMed)); + server.search(dnShort, generateHandler(dnShort)); + server.search(dnLong, generateHandler(dnLong)); + server.listen(sock, function () { + t.ok(true, 'server listen'); + client = ldap.createClient({ socketPath: sock }); + function runSearch(value, cb) { + client.search(value, '(objectclass=*)', function (err, res) { + t.ifError(err); + t.ok(res); + res.on('searchEntry', function (entry) { + t.equal(entry.dn.toString(), value); + }); + res.on('end', function () { + cb(); + }); + }); + } + + vasync.forEachParallel({ + 'func': runSearch, + 'inputs': [dnShort, dnMed, dnLong] + }, function (err, results) { + t.notOk(err); + client.unbind(); + server.close(); + t.end(); + }); + }); +}); + +test('route absent', function (t) { + server = ldap.createServer(); + sock = getSock(); + var DN_ROUTE = 'dc=base'; + var DN_MISSING = 'dc=absent'; + + server.bind(DN_ROUTE, function (req, res, next) { + res.end(); + return next(); + }); + + server.listen(sock, function () { + t.ok(true, 'server startup'); + vasync.parallel({ + 'funcs': [ + function presentBind(cb) { + var clt = ldap.createClient({ socketPath: sock }); + clt.bind(DN_ROUTE, '', function (err) { + t.notOk(err); + clt.unbind(); + cb(); + }); + }, + function absentBind(cb) { + var clt = ldap.createClient({ socketPath: sock }); + clt.bind(DN_MISSING, '', function (err) { + t.ok(err); + t.equal(err.code, ldap.LDAP_NO_SUCH_OBJECT); + clt.unbind(); + cb(); + }); + } + ] + }, function (err, result) { + t.notOk(err); + server.close(); + t.end(); + }); + }); +});