Merge pull request #195 from pfmooney/route-pr

Server request routing update
This commit is contained in:
Mark Cavage 2014-06-05 22:59:44 -07:00
commit d6780e30cb
3 changed files with 238 additions and 35 deletions

View File

@ -716,12 +716,45 @@ Server.prototype._getRoute = function (_dn, backend) {
this.routes[name] = {}; this.routes[name] = {};
this.routes[name].backend = backend; this.routes[name].backend = backend;
this.routes[name].dn = _dn; this.routes[name].dn = _dn;
// Force regeneration of the route key cache on next request
this._routeKeyCache = null;
} }
return this.routes[name]; 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) { Server.prototype._getHandlerChain = function _getHandlerChain(req, res) {
assert.ok(req); assert.ok(req);
@ -738,28 +771,31 @@ Server.prototype._getHandlerChain = function _getHandlerChain(req, res) {
} }
var op = '0x' + req.protocolOp.toString(16); var op = '0x' + req.protocolOp.toString(16);
var self = this; var self = this;
var routes = this.routes; var routes = this.routes;
var keys = Object.keys(routes); var route;
for (var i = 0; i < keys.length; i++) {
var r = keys[i];
var route = routes[r];
// Special cases are abandons, exops and unbinds, handle those first. // Special cases are exops, unbinds and abandons. Handle those first.
if (req.protocolOp === Protocol.LDAP_REQ_EXTENSION) { if (req.protocolOp === Protocol.LDAP_REQ_EXTENSION) {
if (r === req.requestName) { route = routes[req.requestName];
if (route) {
return { return {
backend: routes.backend, backend: route.backend,
handlers: route[op] || [noExOpHandler] handlers: (route[op] ? route[op] : [noExOpHandler])
};
} else {
return {
backend: self,
handlers: [noExOpHandler]
}; };
} }
} else if (req.protocolOp === Protocol.LDAP_REQ_UNBIND) { } else if (req.protocolOp === Protocol.LDAP_REQ_UNBIND) {
route = routes['unbind'];
return { return {
backend: routes['unbind'] ? routes['unbind'].backend : self, backend: route ? route.backend : self,
handlers: function getUnbindChain() { handlers: function getUnbindChain() {
if (routes['unbind'] && routes['unbind'][op]) if (route && route[op])
return routes['unbind'][op]; return route[op];
self.log.debug('%s unbind request %j', req.logId, req.json); self.log.debug('%s unbind request %j', req.logId, req.json);
return [defaultNoOpHandler]; return [defaultNoOpHandler];
@ -770,27 +806,35 @@ Server.prototype._getHandlerChain = function _getHandlerChain(req, res) {
backend: self, backend: self,
handlers: [defaultNoOpHandler] handlers: [defaultNoOpHandler]
}; };
} else if (route[op]) { }
// Otherwise, match via DN rules // Otherwise, match via DN rules
assert.ok(req.dn); 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); assert.ok(route.dn);
// Match a valid route or the route wildcard ('')
if (route.dn.equals(req.dn) || route.dn.parentOf(req.dn)) { if (route.dn.equals(req.dn) || route.dn.parentOf(req.dn) || suffix === '') {
if (route[op]) {
// We should be good to go. // We should be good to go.
req.suffix = route.dn; req.suffix = route.dn;
return { return {
backend: route.backend, 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 { return {
backend: self, backend: self,
handlers: [(req.protocolOp !== Protocol.LDAP_REQ_EXTENSION ? handlers: fallbackHandler
noSuffixHandler : noExOpHandler)]
}; };
}; };

View File

@ -38,7 +38,8 @@
}, },
"devDependencies": { "devDependencies": {
"tap": "0.4.1", "tap": "0.4.1",
"node-uuid": "1.4.0" "node-uuid": "1.4.0",
"vasync": "1.4.3"
}, },
"scripts": { "scripts": {
"test": "./node_modules/.bin/tap ./test" "test": "./node_modules/.bin/tap ./test"

158
test/server.test.js Normal file
View File

@ -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();
});
});
});