915 lines
23 KiB
JavaScript
915 lines
23 KiB
JavaScript
// Copyright 2011 Mark Cavage, Inc. All rights reserved.
|
|
|
|
var assert = require('assert');
|
|
var EventEmitter = require('events').EventEmitter;
|
|
var net = require('net');
|
|
var tls = require('tls');
|
|
var util = require('util');
|
|
|
|
var asn1 = require('asn1');
|
|
var VError = require('verror').VError;
|
|
|
|
var dn = require('./dn');
|
|
var dtrace = require('./dtrace');
|
|
var errors = require('./errors');
|
|
var Protocol = require('./protocol');
|
|
|
|
var Parser = require('./messages').Parser;
|
|
var AbandonResponse = require('./messages/abandon_response');
|
|
var AddResponse = require('./messages/add_response');
|
|
var BindResponse = require('./messages/bind_response');
|
|
var CompareResponse = require('./messages/compare_response');
|
|
var DeleteResponse = require('./messages/del_response');
|
|
var ExtendedResponse = require('./messages/ext_response');
|
|
var LDAPResult = require('./messages/result');
|
|
var ModifyResponse = require('./messages/modify_response');
|
|
var ModifyDNResponse = require('./messages/moddn_response');
|
|
var SearchRequest = require('./messages/search_request');
|
|
var SearchResponse = require('./messages/search_response');
|
|
var UnbindResponse = require('./messages/unbind_response');
|
|
|
|
|
|
|
|
///--- Globals
|
|
|
|
var Ber = asn1.Ber;
|
|
var BerReader = asn1.BerReader;
|
|
var DN = dn.DN;
|
|
|
|
var sprintf = util.format;
|
|
|
|
|
|
///--- Helpers
|
|
|
|
function mergeFunctionArgs(argv, start, end) {
|
|
assert.ok(argv);
|
|
|
|
if (!start)
|
|
start = 0;
|
|
if (!end)
|
|
end = argv.length;
|
|
|
|
var handlers = [];
|
|
|
|
for (var i = start; i < end; i++) {
|
|
if (argv[i] instanceof Array) {
|
|
var arr = argv[i];
|
|
for (var j = 0; j < arr.length; j++) {
|
|
if (!(arr[j] instanceof Function)) {
|
|
throw new TypeError('Invalid argument type: ' + typeof (arr[j]));
|
|
}
|
|
handlers.push(arr[j]);
|
|
}
|
|
} else if (argv[i] instanceof Function) {
|
|
handlers.push(argv[i]);
|
|
} else {
|
|
throw new TypeError('Invalid argument type: ' + typeof (argv[i]));
|
|
}
|
|
}
|
|
|
|
return handlers;
|
|
}
|
|
|
|
|
|
function getResponse(req) {
|
|
assert.ok(req);
|
|
|
|
var Response;
|
|
|
|
switch (req.protocolOp) {
|
|
case Protocol.LDAP_REQ_BIND:
|
|
Response = BindResponse;
|
|
break;
|
|
case Protocol.LDAP_REQ_ABANDON:
|
|
Response = AbandonResponse;
|
|
break;
|
|
case Protocol.LDAP_REQ_ADD:
|
|
Response = AddResponse;
|
|
break;
|
|
case Protocol.LDAP_REQ_COMPARE:
|
|
Response = CompareResponse;
|
|
break;
|
|
case Protocol.LDAP_REQ_DELETE:
|
|
Response = DeleteResponse;
|
|
break;
|
|
case Protocol.LDAP_REQ_EXTENSION:
|
|
Response = ExtendedResponse;
|
|
break;
|
|
case Protocol.LDAP_REQ_MODIFY:
|
|
Response = ModifyResponse;
|
|
break;
|
|
case Protocol.LDAP_REQ_MODRDN:
|
|
Response = ModifyDNResponse;
|
|
break;
|
|
case Protocol.LDAP_REQ_SEARCH:
|
|
Response = SearchResponse;
|
|
break;
|
|
case Protocol.LDAP_REQ_UNBIND:
|
|
Response = UnbindResponse;
|
|
break;
|
|
default:
|
|
return null;
|
|
}
|
|
assert.ok(Response);
|
|
|
|
var res = new Response({
|
|
messageID: req.messageID,
|
|
log: req.log,
|
|
attributes: ((req instanceof SearchRequest) ? req.attributes : undefined)
|
|
});
|
|
res.connection = req.connection;
|
|
res.logId = req.logId;
|
|
|
|
return res;
|
|
}
|
|
|
|
|
|
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);
|
|
assert.ok(next);
|
|
|
|
res.matchedDN = req.dn.toString();
|
|
res.errorMessage = 'Server method not implemented';
|
|
res.end(errors.LDAP_OTHER);
|
|
return next();
|
|
}
|
|
|
|
|
|
function defaultNoOpHandler(req, res, next) {
|
|
assert.ok(req);
|
|
assert.ok(res);
|
|
assert.ok(next);
|
|
|
|
res.end();
|
|
return next();
|
|
}
|
|
|
|
|
|
function noSuffixHandler(req, res, next) {
|
|
assert.ok(req);
|
|
assert.ok(res);
|
|
assert.ok(next);
|
|
|
|
res.errorMessage = 'No tree found for: ' + req.dn.toString();
|
|
res.end(errors.LDAP_NO_SUCH_OBJECT);
|
|
return next();
|
|
}
|
|
|
|
|
|
function noExOpHandler(req, res, next) {
|
|
assert.ok(req);
|
|
assert.ok(res);
|
|
assert.ok(next);
|
|
|
|
res.errorMessage = req.requestName + ' not supported';
|
|
res.end(errors.LDAP_PROTOCOL_ERROR);
|
|
return next();
|
|
}
|
|
|
|
|
|
function fireDTraceProbe(req, res) {
|
|
assert.ok(req);
|
|
|
|
req._dtraceId = res._dtraceId = dtrace._nextId();
|
|
var probeArgs = [
|
|
req._dtraceId,
|
|
req.connection.remoteAddress || 'localhost',
|
|
req.connection.ldap.bindDN.toString(),
|
|
req.dn.toString()
|
|
];
|
|
|
|
var op;
|
|
switch (req.protocolOp) {
|
|
case Protocol.LDAP_REQ_ABANDON:
|
|
op = 'abandon';
|
|
break;
|
|
case Protocol.LDAP_REQ_ADD:
|
|
op = 'add';
|
|
probeArgs.push(req.attributes.length);
|
|
break;
|
|
case Protocol.LDAP_REQ_BIND:
|
|
op = 'bind';
|
|
break;
|
|
case Protocol.LDAP_REQ_COMPARE:
|
|
op = 'compare';
|
|
probeArgs.push(req.attribute);
|
|
probeArgs.push(req.value);
|
|
break;
|
|
case Protocol.LDAP_REQ_DELETE:
|
|
op = 'delete';
|
|
break;
|
|
case Protocol.LDAP_REQ_EXTENSION:
|
|
op = 'exop';
|
|
probeArgs.push(req.name);
|
|
probeArgs.push(req.value);
|
|
break;
|
|
case Protocol.LDAP_REQ_MODIFY:
|
|
op = 'modify';
|
|
probeArgs.push(req.changes.length);
|
|
break;
|
|
case Protocol.LDAP_REQ_MODRDN:
|
|
op = 'modifydn';
|
|
probeArgs.push(req.newRdn.toString());
|
|
probeArgs.push((req.newSuperior ? req.newSuperior.toString() : ''));
|
|
break;
|
|
case Protocol.LDAP_REQ_SEARCH:
|
|
op = 'search';
|
|
probeArgs.push(req.scope);
|
|
probeArgs.push(req.filter.toString());
|
|
break;
|
|
case Protocol.LDAP_REQ_UNBIND:
|
|
op = 'unbind';
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
|
|
res._dtraceOp = op;
|
|
dtrace.fire('server-' + op + '-start', function () {
|
|
return probeArgs;
|
|
});
|
|
}
|
|
|
|
|
|
|
|
///--- API
|
|
|
|
/**
|
|
* Constructs a new server that you can call .listen() on, in the various
|
|
* forms node supports. You need to first assign some handlers to the various
|
|
* LDAP operations however.
|
|
*
|
|
* The options object currently only takes a certificate/private key, and a
|
|
* bunyan logger handle.
|
|
*
|
|
* This object exposes the following events:
|
|
* - 'error'
|
|
* - 'close'
|
|
*
|
|
* @param {Object} options (optional) parameterization object.
|
|
* @throws {TypeError} on bad input.
|
|
*/
|
|
function Server(options) {
|
|
if (options) {
|
|
if (typeof (options) !== 'object')
|
|
throw new TypeError('options (object) required');
|
|
if (typeof (options.log) !== 'object')
|
|
throw new TypeError('options.log must be an object');
|
|
|
|
if (options.certificate || options.key) {
|
|
if (!(options.certificate && options.key) ||
|
|
(typeof (options.certificate) !== 'string' &&
|
|
!Buffer.isBuffer(options.certificate)) ||
|
|
(typeof (options.key) !== 'string' &&
|
|
!Buffer.isBuffer(options.key))) {
|
|
throw new TypeError('options.certificate and options.key ' +
|
|
'(string or buffer) are both required for TLS');
|
|
}
|
|
}
|
|
} else {
|
|
options = {};
|
|
}
|
|
var self = this;
|
|
|
|
EventEmitter.call(this, options);
|
|
|
|
this._chain = [];
|
|
this.log = options.log;
|
|
this.strictDN = (options.strictDN !== undefined) ? options.strictDN : true;
|
|
|
|
var log = this.log;
|
|
|
|
function setupConnection(c) {
|
|
assert.ok(c);
|
|
|
|
if (c.type === 'unix') {
|
|
c.remoteAddress = self.server.path;
|
|
c.remotePort = c.fd;
|
|
} else if (c.socket) {
|
|
// TLS
|
|
c.remoteAddress = c.socket.remoteAddress;
|
|
c.remotePort = c.socket.remotePort;
|
|
}
|
|
|
|
|
|
var rdn = new dn.RDN({cn: 'anonymous'});
|
|
|
|
c.ldap = {
|
|
id: c.remoteAddress + ':' + c.remotePort,
|
|
config: options,
|
|
_bindDN: new DN([rdn])
|
|
};
|
|
c.addListener('timeout', function () {
|
|
log.trace('%s timed out', c.ldap.id);
|
|
c.destroy();
|
|
});
|
|
c.addListener('end', function () {
|
|
log.trace('%s shutdown', c.ldap.id);
|
|
});
|
|
c.addListener('error', function (err) {
|
|
log.warn('%s unexpected connection error', c.ldap.id, err);
|
|
self.emit('clientError', err);
|
|
c.destroy();
|
|
});
|
|
c.addListener('close', function (had_err) {
|
|
log.trace('%s close; had_err=%j', c.ldap.id, had_err);
|
|
c.end();
|
|
});
|
|
|
|
c.ldap.__defineGetter__('bindDN', function () {
|
|
return c.ldap._bindDN;
|
|
});
|
|
c.ldap.__defineSetter__('bindDN', function (val) {
|
|
if (!(val instanceof DN))
|
|
throw new TypeError('DN required');
|
|
|
|
c.ldap._bindDN = val;
|
|
return val;
|
|
});
|
|
return c;
|
|
}
|
|
|
|
function newConnection(c) {
|
|
setupConnection(c);
|
|
log.trace('new connection from %s', c.ldap.id);
|
|
|
|
dtrace.fire('server-connection', function () {
|
|
return [c.remoteAddress];
|
|
});
|
|
|
|
c.parser = new Parser({
|
|
log: options.log
|
|
});
|
|
c.parser.on('message', function (req) {
|
|
req.connection = c;
|
|
req.logId = c.ldap.id + '::' + req.messageID;
|
|
req.startTime = new Date().getTime();
|
|
|
|
if (log.debug())
|
|
log.debug('%s: message received: req=%j', c.ldap.id, req.json);
|
|
|
|
var res = getResponse(req);
|
|
if (!res) {
|
|
log.warn('Unimplemented server method: %s', req.type);
|
|
c.destroy();
|
|
return false;
|
|
}
|
|
|
|
// parse string DNs for routing/etc
|
|
decodeDN(req, this.strictDN);
|
|
|
|
res.connection = c;
|
|
res.logId = req.logId;
|
|
res.requestDN = req.dn;
|
|
|
|
var chain = self._getHandlerChain(req, res);
|
|
|
|
var i = 0;
|
|
return function (err) {
|
|
function sendError(err) {
|
|
res.status = err.code || errors.LDAP_OPERATIONS_ERROR;
|
|
res.matchedDN = req.suffix ? req.suffix.toString() : '';
|
|
res.errorMessage = err.message || '';
|
|
return res.end();
|
|
}
|
|
|
|
function after() {
|
|
if (!self._postChain || !self._postChain.length)
|
|
return;
|
|
|
|
function next() {} // stub out next for the post chain
|
|
|
|
self._postChain.forEach(function (c) {
|
|
c.call(self, req, res, next);
|
|
});
|
|
}
|
|
|
|
if (err) {
|
|
log.trace('%s sending error: %s', req.logId, err.stack || err);
|
|
self.emit('clientError', err);
|
|
sendError(err);
|
|
return after();
|
|
}
|
|
|
|
try {
|
|
var next = arguments.callee;
|
|
if (chain.handlers[i])
|
|
return chain.handlers[i++].call(chain.backend, req, res, next);
|
|
|
|
if (req.protocolOp === Protocol.LDAP_REQ_BIND && res.status === 0)
|
|
c.ldap.bindDN = req.dn;
|
|
|
|
return after();
|
|
} catch (e) {
|
|
if (!e.stack)
|
|
e.stack = e.toString();
|
|
log.error('%s uncaught exception: %s', req.logId, e.stack);
|
|
return sendError(new errors.OperationsError(e.message));
|
|
}
|
|
|
|
}();
|
|
});
|
|
|
|
c.parser.on('error', function (err, message) {
|
|
self.emit('error', new VError(err, 'Parser error for %s', c.ldap.id));
|
|
|
|
if (!message)
|
|
return c.destroy();
|
|
|
|
var res = getResponse(message);
|
|
if (!res)
|
|
return c.destroy();
|
|
|
|
res.status = 0x02; // protocol error
|
|
res.errorMessage = err.toString();
|
|
return c.end(res.toBer());
|
|
});
|
|
|
|
c.on('data', function (data) {
|
|
if (log.trace())
|
|
log.trace('data on %s: %s', c.ldap.id, util.inspect(data));
|
|
|
|
c.parser.write(data);
|
|
});
|
|
|
|
} // end newConnection
|
|
|
|
this.routes = {};
|
|
if ((options.cert || options.certificate) && options.key) {
|
|
options.cert = options.cert || options.certificate;
|
|
this.server = tls.createServer(options, newConnection);
|
|
} else {
|
|
this.server = net.createServer(newConnection);
|
|
}
|
|
this.server.log = options.log;
|
|
this.server.ldap = {
|
|
config: options
|
|
};
|
|
this.server.on('close', function () {
|
|
self.emit('close');
|
|
});
|
|
this.server.on('error', function (err) {
|
|
self.emit('error', err);
|
|
});
|
|
|
|
this.__defineGetter__('maxConnections', function () {
|
|
return self.server.maxConnections;
|
|
});
|
|
this.__defineSetter__('maxConnections', function (val) {
|
|
self.server.maxConnections = val;
|
|
});
|
|
this.__defineGetter__('connections', function () {
|
|
return self.server.connections;
|
|
});
|
|
this.__defineGetter__('name', function () {
|
|
return 'LDAPServer';
|
|
});
|
|
this.__defineGetter__('url', function () {
|
|
var str;
|
|
if (!this.server.address().family) {
|
|
str = 'ldapi://';
|
|
str += this.host.replace(new RegExp('/', 'g'), '%2f');
|
|
return str;
|
|
}
|
|
if (this.server instanceof tls.Server) {
|
|
str = 'ldaps://';
|
|
} else {
|
|
str = 'ldap://';
|
|
}
|
|
str += self.host || 'localhost';
|
|
str += ':';
|
|
str += self.port || 389;
|
|
return str;
|
|
});
|
|
}
|
|
util.inherits(Server, EventEmitter);
|
|
module.exports = Server;
|
|
|
|
|
|
/**
|
|
* Adds a handler (chain) for the LDAP add method.
|
|
*
|
|
* Note that this is of the form f(name, [function]) where the second...N
|
|
* arguments can all either be functions or arrays of functions.
|
|
*
|
|
* @param {String} name the DN to mount this handler chain at.
|
|
* @return {Server} this so you can chain calls.
|
|
* @throws {TypeError} on bad input
|
|
*/
|
|
Server.prototype.add = function (name) {
|
|
var args = Array.prototype.slice.call(arguments, 1);
|
|
return this._mount(Protocol.LDAP_REQ_ADD, name, args);
|
|
};
|
|
|
|
|
|
/**
|
|
* Adds a handler (chain) for the LDAP bind method.
|
|
*
|
|
* Note that this is of the form f(name, [function]) where the second...N
|
|
* arguments can all either be functions or arrays of functions.
|
|
*
|
|
* @param {String} name the DN to mount this handler chain at.
|
|
* @return {Server} this so you can chain calls.
|
|
* @throws {TypeError} on bad input
|
|
*/
|
|
Server.prototype.bind = function (name) {
|
|
var args = Array.prototype.slice.call(arguments, 1);
|
|
return this._mount(Protocol.LDAP_REQ_BIND, name, args);
|
|
};
|
|
|
|
|
|
/**
|
|
* Adds a handler (chain) for the LDAP compare method.
|
|
*
|
|
* Note that this is of the form f(name, [function]) where the second...N
|
|
* arguments can all either be functions or arrays of functions.
|
|
*
|
|
* @param {String} name the DN to mount this handler chain at.
|
|
* @return {Server} this so you can chain calls.
|
|
* @throws {TypeError} on bad input
|
|
*/
|
|
Server.prototype.compare = function (name) {
|
|
var args = Array.prototype.slice.call(arguments, 1);
|
|
return this._mount(Protocol.LDAP_REQ_COMPARE, name, args);
|
|
};
|
|
|
|
|
|
/**
|
|
* Adds a handler (chain) for the LDAP delete method.
|
|
*
|
|
* Note that this is of the form f(name, [function]) where the second...N
|
|
* arguments can all either be functions or arrays of functions.
|
|
*
|
|
* @param {String} name the DN to mount this handler chain at.
|
|
* @return {Server} this so you can chain calls.
|
|
* @throws {TypeError} on bad input
|
|
*/
|
|
Server.prototype.del = function (name) {
|
|
var args = Array.prototype.slice.call(arguments, 1);
|
|
return this._mount(Protocol.LDAP_REQ_DELETE, name, args);
|
|
};
|
|
|
|
|
|
/**
|
|
* Adds a handler (chain) for the LDAP exop method.
|
|
*
|
|
* Note that this is of the form f(name, [function]) where the second...N
|
|
* arguments can all either be functions or arrays of functions.
|
|
*
|
|
* @param {String} name OID to assign this handler chain to.
|
|
* @return {Server} this so you can chain calls.
|
|
* @throws {TypeError} on bad input.
|
|
*/
|
|
Server.prototype.exop = function (name) {
|
|
var args = Array.prototype.slice.call(arguments, 1);
|
|
return this._mount(Protocol.LDAP_REQ_EXTENSION, name, args, true);
|
|
};
|
|
|
|
|
|
/**
|
|
* Adds a handler (chain) for the LDAP modify method.
|
|
*
|
|
* Note that this is of the form f(name, [function]) where the second...N
|
|
* arguments can all either be functions or arrays of functions.
|
|
*
|
|
* @param {String} name the DN to mount this handler chain at.
|
|
* @return {Server} this so you can chain calls.
|
|
* @throws {TypeError} on bad input
|
|
*/
|
|
Server.prototype.modify = function (name) {
|
|
var args = Array.prototype.slice.call(arguments, 1);
|
|
return this._mount(Protocol.LDAP_REQ_MODIFY, name, args);
|
|
};
|
|
|
|
|
|
/**
|
|
* Adds a handler (chain) for the LDAP modifyDN method.
|
|
*
|
|
* Note that this is of the form f(name, [function]) where the second...N
|
|
* arguments can all either be functions or arrays of functions.
|
|
*
|
|
* @param {String} name the DN to mount this handler chain at.
|
|
* @return {Server} this so you can chain calls.
|
|
* @throws {TypeError} on bad input
|
|
*/
|
|
Server.prototype.modifyDN = function (name) {
|
|
var args = Array.prototype.slice.call(arguments, 1);
|
|
return this._mount(Protocol.LDAP_REQ_MODRDN, name, args);
|
|
};
|
|
|
|
|
|
/**
|
|
* Adds a handler (chain) for the LDAP search method.
|
|
*
|
|
* Note that this is of the form f(name, [function]) where the second...N
|
|
* arguments can all either be functions or arrays of functions.
|
|
*
|
|
* @param {String} name the DN to mount this handler chain at.
|
|
* @return {Server} this so you can chain calls.
|
|
* @throws {TypeError} on bad input
|
|
*/
|
|
Server.prototype.search = function (name) {
|
|
var args = Array.prototype.slice.call(arguments, 1);
|
|
return this._mount(Protocol.LDAP_REQ_SEARCH, name, args);
|
|
};
|
|
|
|
|
|
/**
|
|
* Adds a handler (chain) for the LDAP unbind method.
|
|
*
|
|
* This method is different than the others and takes no mount point, as unbind
|
|
* is a connection-wide operation, not constrianed to part of the DIT.
|
|
*
|
|
* @return {Server} this so you can chain calls.
|
|
* @throws {TypeError} on bad input
|
|
*/
|
|
Server.prototype.unbind = function () {
|
|
var args = Array.prototype.slice.call(arguments, 0);
|
|
return this._mount(Protocol.LDAP_REQ_UNBIND, 'unbind', args, true);
|
|
};
|
|
|
|
|
|
Server.prototype.use = function use() {
|
|
var args = Array.prototype.slice.call(arguments);
|
|
var chain = mergeFunctionArgs(args, 0, args.length);
|
|
var self = this;
|
|
chain.forEach(function (c) {
|
|
self._chain.push(c);
|
|
});
|
|
};
|
|
|
|
|
|
Server.prototype.after = function () {
|
|
if (!this._postChain)
|
|
this._postChain = [];
|
|
|
|
var self = this;
|
|
mergeFunctionArgs(arguments).forEach(function (h) {
|
|
self._postChain.push(h);
|
|
});
|
|
};
|
|
|
|
|
|
// All these just reexpose the requisite net.Server APIs
|
|
Server.prototype.listen = function (port, host, callback) {
|
|
if (typeof (port) !== 'number' && typeof (port) !== 'string')
|
|
throw new TypeError('port (number or path) required');
|
|
|
|
if (typeof (host) === 'function') {
|
|
callback = host;
|
|
host = '0.0.0.0';
|
|
}
|
|
if (typeof (port) === 'string' && /^[0-9]+$/.test(port)) {
|
|
// Disambiguate between string ports and file paths
|
|
port = parseInt(port, 10);
|
|
}
|
|
var self = this;
|
|
|
|
function _callback() {
|
|
if (typeof (port) === 'number') {
|
|
self.host = self.address().address;
|
|
self.port = self.address().port;
|
|
} else {
|
|
self.host = port;
|
|
self.port = self.server.fd;
|
|
}
|
|
|
|
if (typeof (callback) === 'function')
|
|
callback();
|
|
}
|
|
|
|
if (typeof (port) === 'number') {
|
|
return this.server.listen(port, host, _callback);
|
|
} else {
|
|
return this.server.listen(port, _callback);
|
|
}
|
|
};
|
|
Server.prototype.listenFD = function (fd) {
|
|
this.host = 'unix-domain-socket';
|
|
this.port = fd;
|
|
return this.server.listenFD(fd);
|
|
};
|
|
Server.prototype.close = function () {
|
|
return this.server.close();
|
|
};
|
|
Server.prototype.address = function () {
|
|
return this.server.address();
|
|
};
|
|
|
|
|
|
Server.prototype._getRoute = function (_dn, backend) {
|
|
assert.ok(dn);
|
|
|
|
if (!backend)
|
|
backend = this;
|
|
|
|
var name;
|
|
if (_dn instanceof dn.DN) {
|
|
name = _dn.toString();
|
|
} else {
|
|
name = _dn;
|
|
}
|
|
|
|
if (!this.routes[name]) {
|
|
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.format()] = 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);
|
|
|
|
fireDTraceProbe(req, res);
|
|
|
|
// check anonymous bind
|
|
if (req.protocolOp === Protocol.LDAP_REQ_BIND &&
|
|
req.dn.toString() === '' &&
|
|
req.credentials === '') {
|
|
return {
|
|
backend: self,
|
|
handlers: [defaultNoOpHandler]
|
|
};
|
|
}
|
|
|
|
var op = '0x' + req.protocolOp.toString(16);
|
|
var self = this;
|
|
var routes = this.routes;
|
|
var route;
|
|
|
|
// 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: route.backend,
|
|
handlers: (route[op] ? route[op] : [noExOpHandler])
|
|
};
|
|
} else {
|
|
return {
|
|
backend: self,
|
|
handlers: [noExOpHandler]
|
|
};
|
|
}
|
|
} else if (req.protocolOp === Protocol.LDAP_REQ_UNBIND) {
|
|
route = routes['unbind'];
|
|
if (route) {
|
|
return {
|
|
backend: route.backend,
|
|
handlers: route[op]
|
|
};
|
|
} else {
|
|
return {
|
|
backend: self,
|
|
handlers: [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];
|
|
// 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(testDN) || route.dn.parentOf(testDN) || suffix === '') {
|
|
if (route[op]) {
|
|
// We should be good to go.
|
|
req.suffix = route.dn;
|
|
return {
|
|
backend: route.backend,
|
|
handlers: route[op]
|
|
};
|
|
} else {
|
|
if (suffix === '') {
|
|
break;
|
|
} else {
|
|
// We found a valid suffix but not a valid operation.
|
|
// There might be a more generic suffix with a legitimate operation.
|
|
fallbackHandler = [defaultHandler];
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return {
|
|
backend: self,
|
|
handlers: fallbackHandler
|
|
};
|
|
};
|
|
|
|
|
|
Server.prototype._mount = function (op, name, argv, notDN) {
|
|
assert.ok(op);
|
|
assert.ok(name !== undefined);
|
|
assert.ok(argv);
|
|
|
|
if (typeof (name) !== 'string')
|
|
throw new TypeError('name (string) required');
|
|
if (!argv.length)
|
|
throw new Error('at least one handler required');
|
|
|
|
var backend = this;
|
|
var index = 0;
|
|
|
|
if (typeof (argv[0]) === 'object' && !Array.isArray(argv[0])) {
|
|
backend = argv[0];
|
|
index = 1;
|
|
}
|
|
var route = this._getRoute(notDN ? name : dn.parse(name), backend);
|
|
|
|
var chain = this._chain.slice();
|
|
argv.slice(index).forEach(function (a) {
|
|
chain.push(a);
|
|
});
|
|
route['0x' + op.toString(16)] = mergeFunctionArgs(chain);
|
|
|
|
return this;
|
|
};
|