node-ldapjs/lib/server.js

674 lines
18 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 sprintf = require('sprintf').sprintf;
var dn = require('./dn');
var errors = require('./errors');
var Protocol = require('./protocol');
var logStub = require('./log_stub');
var Parser = require('./messages').Parser;
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 ModifyResponse = require('./messages/modify_response');
var ModifyDNResponse = require('./messages/moddn_response');
var SearchResponse = require('./messages/search_response');
var UnbindResponse = require('./messages/unbind_response');
///--- Globals
var Ber = asn1.Ber;
var BerReader = asn1.BerReader;
///--- 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:
return; // Noop
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,
log4js: req.log4js
});
res.connection = req.connection;
res.logId = req.logId;
return res;
}
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 defaultUnbindHandler(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();
}
///--- 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
* log4js 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 (options.log4js && typeof(options.log4js) !== 'object')
throw new TypeError('options.log4s must be an object');
if (options.certificate || options.key) {
if (!(options.certificate && options.key) ||
typeof(options.certificate) !== 'string' ||
typeof(options.key) !== 'string') {
throw new TypeError('options.certificate and options.key (string) ' +
'are both required for TLS');
}
}
} else {
options = {};
}
var self = this;
if (!options.log4js)
options.log4js = logStub;
EventEmitter.call(this, options);
var log = this.log = options.log4js.getLogger('LDAPServer');
function setupConnection(c) {
assert.ok(c);
c.ldap = {
id: c.remoteAddress + ':' + c.remotePort,
config: options
};
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);
c.destroy();
});
c.addListener('close', function(had_err) {
log.trace('%s close; had_err=%j', c.ldap.id, had_err);
c.end();
});
return c;
}
function newConnection(c) {
if (c.type === 'unix') {
c.remoteAddress = self.server.path;
c.remotePort = c.fd;
}
setupConnection(c);
if (log.isTraceEnabled())
log.trace('new connection from %s', c.ldap.id);
c.parser = new Parser({
log4js: options.log4js
});
c.parser.on('message', function(req) {
req.connection = c;
req.logId = c.remoteAddress + '::' + req.messageID;
if (log.isDebugEnabled())
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;
}
var chain = self._getHandlerChain(req);
var i = 0;
return function(err) {
if (err) {
res.status = err.code || errors.LDAP_OPERATIONS_ERROR;
res.matchedDN = err.dn ? err.dn.toString() : req.dn.toString();
res.errorMessage = err.message || '';
return res.end();
}
var next = arguments.callee;
if (chain.handlers[i])
return chain.handlers[i++].call(chain.backend, req, res, next);
}();
});
c.parser.on('protocolError', function(err, messageID) {
log.warn('%s sent invalid protocol message', c.ldap.id, err);
c.destroy();
});
c.parser.on('error', function(err) {
log.error('Exception happened parsing for %s: %s',
c.ldap.id, err.stack);
c.destroy();
});
c.on('data', function(data) {
if (log.isTraceEnabled())
log.trace('data on %s: %s', c.ldap.id, util.inspect(data));
c.parser.write(data);
});
}; // end newConnection
this.routes = {};
if (options.certificate && options.key) {
this.server = tls.createServer(options, newConnection);
} else {
this.server = net.createServer(newConnection);
}
this.server.log4js = options.log4js;
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;
});
}
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) {
if (!name || typeof(name) !== 'string')
throw new TypeError('name (string) required');
if (arguments.length < 2)
throw new TypeError('name and at least one handler required');
var route = this._getRoute(dn.parse(name));
route['0x' + Protocol.LDAP_REQ_ADD.toString(16)] =
mergeFunctionArgs(Array.prototype.slice.call(arguments, 1));
return this;
};
/**
* 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) {
if (!name || typeof(name) !== 'string')
throw new TypeError('name (string) required');
if (arguments.length < 2)
throw new TypeError('name and at least one handler required');
var route = this._getRoute(dn.parse(name));
route['0x' + Protocol.LDAP_REQ_BIND.toString(16)] =
mergeFunctionArgs(Array.prototype.slice.call(arguments, 1));
return this;
};
/**
* 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) {
if (!name || typeof(name) !== 'string')
throw new TypeError('name (string) required');
if (arguments.length < 2)
throw new TypeError('name and at least one handler required');
var route = this._getRoute(dn.parse(name));
route['0x' + Protocol.LDAP_REQ_COMPARE.toString(16)] =
mergeFunctionArgs(Array.prototype.slice.call(arguments, 1));
return this;
};
/**
* 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) {
if (!name || typeof(name) !== 'string')
throw new TypeError('name (string) required');
if (arguments.length < 2)
throw new TypeError('name and at least one handler required');
var route = this._getRoute(dn.parse(name));
route['0x' + Protocol.LDAP_REQ_DELETE.toString(16)] =
mergeFunctionArgs(Array.prototype.slice.call(arguments, 1));
return this;
};
/**
* 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) {
if (!name || typeof(name) !== 'string')
throw new TypeError('name (string) required');
if (arguments.length < 2)
throw new TypeError('name and at least one handler required');
var route = this._getRoute(name, this.server);
route['0x' + Protocol.LDAP_REQ_EXTENSION.toString(16)] =
mergeFunctionArgs(Array.prototype.slice.call(arguments, 1));
return this;
};
/**
* 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) {
if (!name || typeof(name) !== 'string')
throw new TypeError('name (string) required');
if (arguments.length < 2)
throw new TypeError('name and at least one handler required');
var route = this._getRoute(dn.parse(name));
route['0x' + Protocol.LDAP_REQ_MODIFY.toString(16)] =
mergeFunctionArgs(Array.prototype.slice.call(arguments, 1));
return this;
};
/**
* 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) {
if (!name || typeof(name) !== 'string')
throw new TypeError('name (string) required');
if (arguments.length < 2)
throw new TypeError('name and at least one handler required');
var route = this._getRoute(dn.parse(name));
route['0x' + Protocol.LDAP_REQ_MODRDN.toString(16)] =
mergeFunctionArgs(Array.prototype.slice.call(arguments, 1));
return this;
};
/**
* 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) {
if (!name || typeof(name) !== 'string')
throw new TypeError('name (string) required');
if (arguments.length < 2)
throw new TypeError('name and at least one handler required');
var route = this._getRoute(dn.parse(name));
route['0x' + Protocol.LDAP_REQ_SEARCH.toString(16)] =
mergeFunctionArgs(Array.prototype.slice.call(arguments, 1));
return this;
};
/**
* 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() {
if (arguments.length < 1)
throw new TypeError('at least one handler required');
var route = this._getRoute('unbind');
route['0x' + Protocol.LDAP_REQ_UNBIND.toString(16)] =
mergeFunctionArgs(Array.prototype.slice.call(arguments, 0));
return this;
};
/**
* It's likely you'll write an entire backend for LDAP that does a series
* of things, like check schema, support entries, write an audit trail, etc.
* If such a plugin is "bundled", you can simply call `mount` with that plugin
* and assign it a point in the DIT, to save you manually building up the
* handler chains for all the ops.
*
* @param {String} name the point in the tree to mount at.
* @param {Object} backend an LDAP Backend (See the docs).
* @return {Server} this so you can chain.
* @throws {TypeError} on bad input.
*/
Server.prototype.mount = function(name, backend) {
if (!name || typeof(name) !== 'string')
throw new TypeError('name (string) required');
if (!backend || typeof(backend) !== 'object')
throw new TypeError('backend (object) required');
if (!backend.name || typeof(backend.name) !== 'string')
throw new TypeError('backend is not a valid LDAP Backend');
if (!backend.register || typeof(backend.register) !== 'function')
throw new TypeError('backend is not a valid LDAP Backend');
var _dn = dn.parse(name).toString();
name = _dn.toString();
var self = this;
// This is slightly ghetto, but easier than repeating all the code here.
var ops = ['add', 'bind', 'compare', 'del', 'modify', 'modifyDN', 'search'];
ops.forEach(function(o) {
self[o](name, backend.register(o));
});
// Overwrite the route table's backend with backend
var route = this._getRoute(_dn);
route.backend = backend;
return this;
};
// All these just reexpose the requisite net.Server APIs
Server.prototype.listen = function(port, host, callback) {
if (typeof(host) === 'function')
callback = host;
return this.server.listen(port, function() {
if (typeof(callback) === 'function')
return callback();
});
};
Server.prototype.listenFD = function(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;
}
return this.routes[name];
};
Server.prototype._getHandlerChain = function(req) {
assert.ok(req);
var op = '0x' + req.protocolOp.toString(16);
var self = this;
var routes = this.routes;
for (var r in routes) {
if (routes.hasOwnProperty(r)) {
var route = routes[r];
// Special cases are exops and unbinds, handle those first.
if (req.protocolOp === Protocol.LDAP_REQ_EXTENSION) {
if (r !== req.requestName)
continue;
return {
backend: routes.backend,
handlers: route[op] || [defaultExopHandler]
};
} else if (req.protocolOp === Protocol.LDAP_REQ_UNBIND) {
return {
backend: routes['unbind'].backend,
handlers: routes['unbind'][op] || [defaultUnbindHandler]
};
}
if (!route[op])
continue;
// Otherwise, match via DN rules
assert.ok(req.dn);
assert.ok(route.dn);
if (r !== req.dn.toString() && (!route.dn.parentOf(req.dn)))
continue;
// We should be good to go.
return {
backend: route.backend,
handlers: route[op] || [defaultHandler]
};
}
}
// We're here, so nothing matched.
return {
backend: self,
handlers: [(req.protocolOp !== Protocol.LDAP_REQ_EXTENSION ?
noSuffixHandler : noExOpHandler)]
};
};