From 59ea20ffa28fa407834571bb4850ea3b6cd0744f Mon Sep 17 00:00:00 2001 From: Mark Cavage Date: Thu, 26 Apr 2012 20:23:43 -0700 Subject: [PATCH] Client refactor, with pooled client (minor lint cleanup as well) --- examples/pooled_client.js | 60 +++ lib/{ => client}/client.js | 480 ++++++++++---------- lib/client/index.js | 64 +++ lib/client/pool.js | 263 +++++++++++ lib/controls/paged_results_control.js | 10 +- lib/errors/index.js | 13 + lib/index.js | 17 +- package.json | 11 +- test/client.test.js | 32 +- test/controls/paged_results_control_test.js | 10 +- 10 files changed, 670 insertions(+), 290 deletions(-) create mode 100644 examples/pooled_client.js rename lib/{ => client}/client.js (73%) create mode 100644 lib/client/index.js create mode 100644 lib/client/pool.js diff --git a/examples/pooled_client.js b/examples/pooled_client.js new file mode 100644 index 0000000..a0b04dc --- /dev/null +++ b/examples/pooled_client.js @@ -0,0 +1,60 @@ +var Logger = require('bunyan'); + +var ldap = require('../lib/index'); + + +/// +// Run the "inmemory.js" server in the same directory +/// + +function ifError(err) { + if (err) { + console.error(err.stack); + process.exit(1); + } +} + +var LOG = new Logger({ + name: 'ldapjs', + stream: process.stderr, + level: (process.env.LOG_LEVEL || 'info'), + serializers: Logger.stdSerializers +}); +var MAX_CONNS = process.env.LDAP_MAX_CONNS || 10; + +var client = ldap.createClient({ + url: 'ldap://localhost:1389', + maxConnections: MAX_CONNS, + log: LOG +}); + +client.bind('cn=root', 'secret', function (err) { + ifError(err); + + client.add('o=smartdc', {o: 'smartdc'}, function (err) { + ifError(err); + + var finished = 0; + for (var i = 0; i < MAX_CONNS; i++) { + client.search('o=smartdc', function (err, res) { + ifError(err); + res.on('end', function () { + if (++finished === (MAX_CONNS - 1)) { + console.error('Go kill the LDAP server and restart it') + setTimeout(function () { + console.log('readding suffix'); + client.add('o=smartdc', {o: 'smartdc'}, function (err) { + ifError(err); + client.unbind(function () { + console.log('All done'); + process.exit(0); + }); + }); + }, 15000); + } + }); + }); + } + }); + +}); diff --git a/lib/client.js b/lib/client/client.js similarity index 73% rename from lib/client.js rename to lib/client/client.js index 68ea56c..9830e16 100644 --- a/lib/client.js +++ b/lib/client/client.js @@ -6,15 +6,15 @@ var net = require('net'); var tls = require('tls'); var util = require('util'); -var Attribute = require('./attribute'); -var Change = require('./change'); -var Control = require('./controls/index').Control; -var Protocol = require('./protocol'); -var dn = require('./dn'); -var errors = require('./errors'); -var filters = require('./filters'); -var messages = require('./messages'); -var url = require('./url'); +var Attribute = require('../attribute'); +var Change = require('../change'); +var Control = require('../controls/index').Control; +var Protocol = require('../protocol'); +var dn = require('../dn'); +var errors = require('../errors'); +var filters = require('../filters'); +var messages = require('../messages'); +var url = require('../url'); @@ -38,29 +38,27 @@ var SearchReference = messages.SearchReference; var SearchResponse = messages.SearchResponse; var Parser = messages.Parser; - var Filter = filters.Filter; var PresenceFilter = filters.PresenceFilter; +var ConnectionError = errors.ConnectionError; + var CMP_EXPECT = [errors.LDAP_COMPARE_TRUE, errors.LDAP_COMPARE_FALSE]; var MAX_MSGID = Math.pow(2, 31) - 1; +// node 0.6 got rid of FDs, so make up a client id for logging +var CLIENT_ID = 0; + ///--- Internal Helpers -function xor() { - var b = false; - for (var i = 0; i < arguments.length; i++) { - if (arguments[i] && !b) { - b = true; - } else if (arguments[i] && b) { - return false; - } - } - return b; -} +function nextClientId() { + if (++CLIENT_ID === MAX_MSGID) + return 1; + return CLIENT_ID; +} function validateControls(controls) { if (Array.isArray(controls)) { @@ -78,15 +76,120 @@ function validateControls(controls) { } -function ConnectionError(message) { - errors.LDAPError.call(this, - 'ConnectionError', - 0x80, // LDAP_OTHER, - message, - null, - ConnectionError); +function setupSocket(socket, opts) { + var log = opts.log; + + socket.ldap = { + id: opts.url ? opts.url.href : opts.socketPath, + messageID: 0, + messages: {}, + getNextMessageID: function getNextMessageID() { + if (++socket.ldap.messageID >= MAX_MSGID) + socket.ldap.messageID = 1; + + return socket.ldap.messageID; + }, + parser: new Parser({ + log: log + }) + }; + + // This won't be set on TLS. So. Very. Annoying. + if (typeof (socket.setKeepAlive) !== 'function') { + socket.setKeepAlive = function setKeepAlive(enable, delay) { + return socket.socket ? socket.socket.setKeepAlive(enable, delay) : false; + }; + } + + // On close we have to walk the outstanding messages and go invoke their + // callback with an error + socket.on('close', function onClose(had_err) { + if (log.trace()) + log.trace('close event had_err=%s', had_err ? 'yes' : 'no'); + + opts.emit('close', had_err); + Object.keys(socket.ldap.messages).forEach(function (msgid) { + var err; + if (socket.unbindMessageID !== parseInt(msgid, 10)) { + err = new ConnectionError(socket.ldap.id + ' closed'); + } else { + err = new UnbindResponse({ + messageID: msgid + }); + err.status = 'unbind'; + } + + if (typeof (socket.ldap.messages[msgid]) === 'function') { + var callback = socket.ldap.messages[msgid]; + delete socket.ldap.messages[msgid]; + return callback(err); + } else if (socket.ldap.messages[msgid]) { + if (err instanceof Error) + socket.ldap.messages[msgid].emit('error', err); + delete socket.ldap.messages[msgid]; + } + + delete socket.ldap.parser; + delete socket.ldap; + return false; + }); + }); + + socket.on('data', function onData(data) { + if (log.trace()) + log.trace('data event: %s', util.inspect(data)); + + socket.ldap.parser.write(data); + }); + + socket.on('end', function onEnd() { + if (log.trace()) + log.trace('end event'); + + opts.emit('end'); + socket.end(); + }); + + socket.on('error', function onError(err) { + if (log.trace()) + log.trace({err: err}, 'error event: %s', new Error().stack); + + if (opts.listeners('error').length) + opts.emit('error', err); + + socket.end(); + }); + + socket.on('timeout', function onTimeout() { + if (log.trace()) + log.trace('timeout event'); + + opts.emit('socketTimeout'); + socket.end(); + }); + + // The "router" + socket.ldap.parser.on('message', function onMessage(message) { + message.connection = socket; + var callback = socket.ldap.messages[message.messageID]; + + if (!callback) { + log.error({message: message.json}, 'unsolicited message'); + return false; + } + + return callback(message); + }); + + socket.ldap.parser.on('error', function onParseError(err) { + log.debug({err: err}, 'parser error event'); + + if (opts.listeners('error').length) + opts.emit('error', err); + + socket.end(); + }); } -util.inherits(ConnectionError, errors.LDAPError); @@ -105,38 +208,24 @@ util.inherits(ConnectionError, errors.LDAPError); * @throws {TypeError} on bad input. */ function Client(options) { - if (!options || typeof (options) !== 'object') - throw new TypeError('options (object) required'); - if (options.url && typeof (options.url) !== 'string') - throw new TypeError('options.url (string) required'); - if (options.socketPath && typeof (options.socketPath) !== 'string') - throw new TypeError('options.socketPath must be a string'); - if (typeof (options.log) !== 'object') - throw new TypeError('options.log must be an object'); - - if (!xor(options.url, options.socketPath)) - throw new TypeError('options.url ^ options.socketPath (String) required'); + assert.ok(options); EventEmitter.call(this, options); - var parsedUrl; + var _url; if (options.url) - parsedUrl = url.parse(options.url); + _url = url.parse(options.url); - this.connection = null; - this.connectTimeout = options.connectTimeout || false; - this.connectOptions = { - port: parsedUrl ? parsedUrl.port : options.socketPath, - host: parsedUrl ? parsedUrl.hostname : undefined, - socketPath: options.socketPath || undefined - }; - this.log = options.log; - this.secure = parsedUrl ? parsedUrl.secure : false; - this.timeout = options.timeout || false; - this.url = parsedUrl || false; + this.connectTimeout = parseInt((options.connectTimeout || 0), 10); + this.host = _url ? _url.hostname : undefined; + this.log = options.log.child({clazz: 'Client'}, true); + this.port = _url ? _url.port : false; + this.secure = _url ? _url.secure : false; + this.socketPath = options.socketPath || false; + this.timeout = parseInt((options.timeout || 0), 10); + this.url = _url; - // We'll emit a connect event when this is done - this.connect(); + this.socket = this._connect(); } util.inherits(Client, EventEmitter); module.exports = Client; @@ -605,7 +694,7 @@ Client.prototype.unbind = function unbind(callback) { if (typeof (callback) !== 'function') throw new TypeError('callback must be a function'); - if (!this.connection) + if (!this.socket) return callback(); var req = new UnbindRequest(); @@ -613,155 +702,47 @@ Client.prototype.unbind = function unbind(callback) { }; -/** - * Connects this client, either at construct time, or after an unbind has - * been called. Under normal circumstances you don't need to call this method. - * - * @param {Function} (optional) callback invoked when `connect` is emitted. - */ -Client.prototype.connect = function connect(callback) { - var c = null; + +///--- Private API + +Client.prototype._connect = function _connect() { var log = this.log; - var opts = this.connectOptions; var proto = this.secure ? tls : net; var self = this; + var socket = null; var timer = false; - c = proto.connect(opts.port, opts.host, function () { + function onConnect() { if (timer) clearTimeout(timer); - assert.ok(c.ldap); + assert.ok(socket.ldap); - c.ldap.id += c.fd ? (':' + c.fd) : ''; + socket.ldap.id = nextClientId() + '__' + socket.ldap.id; + self.log = self.log.child({ldap_id: socket.ldap.id}, true); if (log.trace()) - log.trace('%s connect event', c.ldap.id); + log.trace('connect event'); - self.connection = c; - self.emit('connect', c); + self.socket = socket; + self.emit('connect', socket); + } - return (typeof (callback) === 'function' ? callback(null, c) : false); - }); + socket = proto.createConnection((this.port || this.socketPath), + (this.host ? this.host : onConnect), + (this.host ? onConnect : undefined)); + + setupSocket(socket, this); if (this.connectTimeout) { - timer = setTimeout(function () { - c.destroy(); + timer = setTimeout(function onConnectTimeout() { + socket.destroy(); - self.emit('connectTimeout', new ConnectionError('timeout')); + self.emit('connectTimeout'); }, this.connectTimeout); } - if (typeof (c.setKeepAlive) !== 'function') { - c.setKeepAlive = function setKeepAlive(enable, delay) { - return c.socket ? c.socket.setKeepAlive(enable, delay) : false; - }; - } - - c.ldap = { - id: self.url ? self.url.href : opts.socketPath, - messageID: 0, - messages: {}, - get nextMessageID() { - if (++c.ldap.messageID >= MAX_MSGID) - c.ldap.messageID = 1; - - return c.ldap.messageID; - }, - parser: new Parser({ - log: self.log - }) - }; - - - c.on('end', function () { - if (log.trace()) - log.trace('%s end event', c.ldap.id); - - c.end(); - }); - - // On close we have to walk the outstanding messages and go invoke their - // callback with an error - c.on('close', function (had_err) { - if (log.trace()) - log.trace('%s close event had_err=%s', c.ldap.id, had_err ? 'yes' : 'no'); - - Object.keys(c.ldap.messages).forEach(function (msgid) { - var err; - if (c.unbindMessageID !== parseInt(msgid, 10)) { - err = new ConnectionError(c.ldap.id + ' closed'); - } else { - err = new UnbindResponse({ - messageID: msgid - }); - err.status = 'unbind'; - } - - if (typeof (c.ldap.messages[msgid]) === 'function') { - var callback = c.ldap.messages[msgid]; - delete c.ldap.messages[msgid]; - return callback(err); - } else if (c.ldap.messages[msgid]) { - if (err instanceof Error) - c.ldap.messages[msgid].emit('error', err); - delete c.ldap.messages[msgid]; - } - - delete c.ldap.parser; - delete c.ldap; - return false; - }); - }); - - c.on('error', function (err) { - if (log.trace()) - log.trace({err: err}, '%s error event', c.ldap.id); - - if (self.listeners('error').length) - self.emit('error', err); - - c.end(); - }); - - c.on('timeout', function () { - if (log.trace()) - log.trace('%s timeout event=%s', c.ldap.id); - - self.emit('timeout'); - c.end(); - }); - - c.on('data', function (data) { - if (log.trace()) - log.trace('%s data event: %s', c.ldap.id, util.inspect(data)); - - c.ldap.parser.write(data); - }); - - // The "router" - c.ldap.parser.on('message', function (message) { - message.connection = c; - var callback = c.ldap.messages[message.messageID]; - - if (!callback) { - log.error({message: message.json}, '%s: unsolicited message', c.ldap.id); - return false; - } - - return callback(message); - }); - - c.ldap.parser.on('error', function (err) { - log.debug({err: err}, '%s parser error event', c.ldap.id, err); - - if (self.listeners('error').length) - self.emit('error', err); - - c.end(); - }); - - return c; + return socket; }; @@ -771,98 +752,95 @@ Client.prototype._send = function _send(message, expect, emitter, callback) { assert.ok(typeof (emitter) !== undefined); assert.ok(callback); - var conn = this.connection; + var conn = this.socket; + var log = this.log; var self = this; var timer = false; if (!conn) return callback(new ConnectionError('no socket')); - message.messageID = conn.ldap.nextMessageID; - conn.ldap.messages[message.messageID] = function messageCallback(res) { + function _done(event, obj) { + if (emitter) + return emitter.emit(event, obj); + + if (event === 'error') + return callback(obj); + + return callback(null, obj); + } // end function _done(event, obj) + + function messageCallback(msg) { if (timer) clearTimeout(timer); + if (log.debug()) + log.debug({msg: msg ? msg.json : null}, 'response received'); + if (expect === 'abandon') - return callback(null); + return _done('end', null); - if (self.log.debug()) - self.log.debug({res: res.json}, '%s: response received', conn.ldap.id); - - var err = null; - - if (res instanceof LDAPResult) { + if (msg instanceof SearchEntry || msg instanceof SearchReference) { + var event = msg.constructor.name; + event = event[0].toLowerCase() + event.slice(1); + return _done(event, msg); + } else { delete conn.ldap.messages[message.messageID]; - if (expect.indexOf(res.status) === -1) { - err = errors.getError(res); - if (emitter) - return emitter.emit('error', err); + if (msg instanceof LDAPResult) { + if (expect.indexOf(msg.status) === -1) + return _done('error', errors.getError(msg)); - return callback(err); + return _done('end', msg); + } else if (msg instanceof Error) { + return _done('error', msg); + } else { + return _done('error', new errors.ProtocolError(msg.type)); } - - if (emitter) - return emitter.emit('end', res); - - return callback(null, res); - } else if (res instanceof SearchEntry || res instanceof SearchReference) { - assert.ok(emitter); - var event = res.constructor.name; - event = event[0].toLowerCase() + event.slice(1); - return emitter.emit(event, res); - } else if (res instanceof Error) { - if (emitter) - return emitter.emit('error', res); - - return callback(res); } + } // end function messageCallback(msg) - delete conn.ldap.messages[message.messageID]; - err = new errors.ProtocolError(res.type); + function onRequestTimeout() { + self.emit('timeout', message); + if (conn.ldap.messages[message.messageID]) { + conn.ldap.messages[message.messageID](new LDAPResult({ + status: 80, // LDAP_OTHER + errorMessage: 'request timeout (client interrupt)' + })); + } + } // end function onRequestTimeout() - if (emitter) - return emitter.emit('error', err); + function writeCallback() { + if (expect === 'abandon') { + return callback(null); + } else if (expect === 'unbind') { + conn.unbindMessageID = message.id; + conn.end(); + } else if (emitter) { + return callback(null, emitter); + } + return false; + } // end writeCallback() - return callback(err); - }; + // Start actually doing something... + message.messageID = conn.ldap.getNextMessageID(); + conn.ldap.messages[message.messageID] = messageCallback; - // If there's a user specified timeout, pick that up - if (this.timeout) { - timer = setTimeout(function () { - self.emit('timeout', message); - if (conn.ldap.messages[message.messageID]) { - conn.ldap.messages[message.messageID](new LDAPResult({ - status: 80, // LDAP_OTHER - errorMessage: 'request timeout (client interrupt)' - })); - } - }, this.timeout); + if (self.timeout) { + log.debug('Setting timeout to %d', self.timeout); + timer = setTimeout(onRequestTimeout, self.timeout); } + if (log.debug()) + log.debug('sending request %j', message.json); + try { - // Finally send some data - if (this.log.debug()) - this.log.debug({msg: message.json}, '%s: sending request', conn.ldap.id); - - return conn.write(message.toBer(), function writeCallback() { - if (expect === 'abandon') { - return callback(null); - } else if (expect === 'unbind') { - conn.unbindMessageID = message.id; - conn.end(); - } else if (emitter) { - return callback(null, emitter); - } - return false; - }); - + return conn.write(message.toBer(), writeCallback); } catch (e) { if (timer) clearTimeout(timer); - conn.destroy(); - delete self.connection; + log.debug({err: e}, 'Error writing message to socket'); return callback(e); } }; diff --git a/lib/client/index.js b/lib/client/index.js new file mode 100644 index 0000000..66c640f --- /dev/null +++ b/lib/client/index.js @@ -0,0 +1,64 @@ +// Copyright 2012 Mark Cavage, Inc. All rights reserved. + +var assert = require('assert'); + +var Logger = require('bunyan'); +var Pool = require('generic-pool').Pool; + +var Client = require('./client'); +var ClientPool = require('./pool'); + + + +///--- Globals + +var DEF_LOG = new Logger({ + name: 'ldapjs', + component: 'client', + stream: process.stderr, + serializers: Logger.stdSerializers +}); + + + +///--- Functions + +function xor() { + var b = false; + for (var i = 0; i < arguments.length; i++) { + if (arguments[i] && !b) { + b = true; + } else if (arguments[i] && b) { + return false; + } + } + return b; +} + + + +///--- Exports + +module.exports = { + + createClient: function createClient(options) { + if (typeof (options) !== 'object') + throw new TypeError('options (object) required'); + if (options.url && typeof (options.url) !== 'string') + throw new TypeError('options.url (string) required'); + if (options.socketPath && typeof (options.socketPath) !== 'string') + throw new TypeError('options.socketPath must be a string'); + if (!xor(options.url, options.socketPath)) + throw new TypeError('options.url ^ options.socketPath (String) required'); + if (!options.log) + options.log = DEF_LOG; + if (typeof (options.log) !== 'object') + throw new TypeError('options.log must be an object'); + + if (options.maxConnections > 1) + return new ClientPool(options); + + return new Client(options); + } + +}; diff --git a/lib/client/pool.js b/lib/client/pool.js new file mode 100644 index 0000000..66140b7 --- /dev/null +++ b/lib/client/pool.js @@ -0,0 +1,263 @@ +// Copyright 2012 Mark Cavage, Inc. All rights reserved. + +var assert = require('assert'); +var EventEmitter = require('events').EventEmitter; +var util = require('util'); + +var pooling = require('pooling'); + +var ConnectionError = require('../errors').ConnectionError; +var BindResponse = require('../messages').BindResponse; + +var Client = require('./client'); + + + +///--- Globals + +var STD_OPS = [ + 'add', + 'del', + 'modify', + 'modifyDN' +]; + +var RETURN_VAL_OPS = [ + 'compare', + 'exop' +]; + + + +///--- Internal Functions + +function createPool(options) { + assert.ok(options); + + return pooling.createPool({ + checkInterval: options.checkInterval, + log: options.log, + name: 'ldapjs_' + (options.url || options.socketPath), + max: options.maxConnections, + maxIdleTime: options.maxIdleTime, + + create: function createConnection(callback) { + var client = new Client(options); + + client.once('error', function (err) { + return callback(err); + }); + + client.once('connect', function onConnect() { + client.removeAllListeners('error'); + + if (!options.bindDN || !options.bindCredentials) + return callback(null, client); + + function bindCallback(err, res) { + if (err) + return callback(err, null); + + return callback(null, client); + } + + return client.bind(options.bindDN, + options.bindCredentials, + options.bindControls || [], + bindCallback); + }); + }, + + check: function check(client, callback) { + // just do a root dse search + client.search('', '(objectclass=*)', function (err, res) { + if (err) + return callback(err); + + res.on('error', function (e) { + return callback(e); + }); + + return res.on('end', function () { + return callback(null); + }); + }); + }, + + destroy: function destroy(client) { + client.unbind(function () {}); + } + }); +} + + + +///--- API + +function ClientPool(options) { + assert.ok(options); + EventEmitter.call(this, options); + + this.log = options.log.child({clazz: 'ClientPool'}, true); + this.options = { + bindDN: options.bindDN, + bindCredentials: options.bindCredentials, + bindControls: options.bindControls || [], + checkInterval: options.checkInterval, + connectTimeout: (options.connectTimeout || 0), + maxIdleTime: options.maxIdleTime, + maxConnections: options.maxConnections, + log: options.log, + socketPath: options.socketPath, + timeout: (options.timeout || 0), + url: options.url + }; + this.pool = createPool(options); +} +util.inherits(ClientPool, EventEmitter); +module.exports = ClientPool; + + + +STD_OPS.forEach(function (op) { + ClientPool.prototype[op] = function clientProxy() { + var args = Array.prototype.slice.call(arguments); + var cb = args.pop(); + if (typeof (cb) !== 'function') + throw new TypeError('callback (Function) required'); + var self = this; + + return this.pool.acquire(function onAcquire(err, client) { + if (err) + return cb(err); + + args.push(function proxyCallback(err, res) { + self.pool.release(client); + return cb(err, res); + }); + + try { + return Client.prototype[op].apply(client, args); + } catch (e) { + self.pool.release(client); + return cb(e); + } + }); + }; +}); + + +RETURN_VAL_OPS.forEach(function (op) { + ClientPool.prototype[op] = function clientProxy() { + var args = Array.prototype.slice.call(arguments); + var cb = args.pop(); + if (typeof (cb) !== 'function') + throw new TypeError('callback (Function) required'); + var self = this; + + return this.pool.acquire(function onAcquire(poolErr, client) { + if (poolErr) + return cb(poolErr); + + args.push(function proxyCallback(err, val, res) { + self.pool.release(client); + return cb(err, val, res); + }); + + try { + return Client.prototype[op].apply(client, args); + } catch (e) { + self.pool.release(client); + return cb(e); + } + }); + }; +}); + + +ClientPool.prototype.search = function search(base, opts, controls, callback) { + if (typeof (controls) === 'function') { + callback = controls; + controls = []; + } + + var self = this; + + return this.pool.acquire(function onAcquire(err, client) { + if (err) + return callback(err); + + // This is largely in existence for search requests + client.timeout = self.timeout || client.timeout; + + + return client.search(base, opts, controls, function (err, res) { + function cleanup() { + self.pool.release(client); + } + + if (err) { + cleanup(); + return callback(err, res); + } + res.on('error', cleanup); + res.on('end', cleanup); + + return callback(null, res); + }); + }); +}; + + +ClientPool.prototype.abandon = function abandon(msgid, controls, callback) { + if (typeof (controls) === 'function') { + callback = controls; + controls = []; + } + + this.log.error({ + messageID: msgid + }, 'Abandon is not supported with connection pooling. Ignoring.'); + return callback(null); +}; + + +ClientPool.prototype.bind = function bind(dn, creds, controls, callback) { + if (typeof (controls) === 'function') { + callback = controls; + controls = []; + } + + var self = this; + + self.options.bindDN = null; + self.options.bindCredentials = null; + self.options.bindControls = null; + + return this.pool.shutdown(function () { + self.pool = createPool(self.options); + + return self.pool.acquire(function onAcquire(err, client) { + if (err) + return callback(err); + + return client.bind(dn, creds, controls, function (err, res) { + self.pool.release(client); + + if (err) + return callback(err, res); + + self.options.bindDN = dn; + self.options.bindCredentials = creds; + self.options.bindControls = controls; + + return callback(null, res); + }); + }); + }); +}; + + +ClientPool.prototype.unbind = function unbind(callback) { + return this.pool.shutdown(callback); +}; diff --git a/lib/controls/paged_results_control.js b/lib/controls/paged_results_control.js index 5dfa9c9..7cdbdf7 100644 --- a/lib/controls/paged_results_control.js +++ b/lib/controls/paged_results_control.js @@ -50,7 +50,10 @@ PagedResultsControl.prototype.parse = function parse(buffer) { this._value = {}; this._value.size = ber.readInt(); this._value.cookie = ber.readString(asn1.Ber.OctetString, true); - if(!this._value.cookie) this._value.cookie = new Buffer(0); //readString returns '' instead of a zero-length buffer + //readString returns '' instead of a zero-length buffer + if (!this._value.cookie) + this._value.cookie = new Buffer(0); + return true; } @@ -67,10 +70,11 @@ PagedResultsControl.prototype._toBer = function (ber) { var writer = new BerWriter(); writer.startSequence(); writer.writeInt(this.value.size); - if(this.value.cookie && this.value.cookie.length>0) + if (this.value.cookie && this.value.cookie.length > 0) { writer.writeBuffer(this.value.cookie, asn1.Ber.OctetString); - else + } else { writer.writeString(''); //writeBuffer rejects zero-length buffers + } writer.endSequence(); ber.writeBuffer(writer.buffer, 0x04); diff --git a/lib/errors/index.js b/lib/errors/index.js index 33315a0..c835741 100644 --- a/lib/errors/index.js +++ b/lib/errors/index.js @@ -139,3 +139,16 @@ module.exports.getMessage = function (code) { var errObj = ERRORS[code]; return (errObj && errObj.message ? errObj.message : ''); }; + + + +function ConnectionError(message) { + LDAPError.call(this, + 'ConnectionError', + 0x80, // LDAP_OTHER, + message, + null, + ConnectionError); +} +util.inherits(ConnectionError, LDAPError); +module.exports.ConnectionError = ConnectionError; diff --git a/lib/index.js b/lib/index.js index 8c1cabe..06314a7 100644 --- a/lib/index.js +++ b/lib/index.js @@ -2,7 +2,7 @@ var Logger = require('bunyan'); -var Client = require('./client'); +var client = require('./client'); var Attribute = require('./attribute'); var Change = require('./change'); var Protocol = require('./protocol'); @@ -42,20 +42,7 @@ if (!String.prototype.endsWith) { module.exports = { - Client: Client, - createClient: function (options) { - if (typeof (options) !== 'object') - throw new TypeError('options (object) required'); - - if (!options.log) { - options.log = new Logger({ - name: 'ldapjs', - component: 'client', - stream: process.stderr - }); - } - return new Client(options); - }, + createClient: client.createClient, Server: Server, createServer: function (options) { diff --git a/package.json b/package.json index c5ead46..d35a05a 100644 --- a/package.json +++ b/package.json @@ -25,13 +25,14 @@ }, "dependencies": { "asn1": "0.1.11", - "buffertools": "1.0.7", - "bunyan": "0.6.3", - "dtrace-provider": "0.0.6", - "nopt": "1.0.10" + "buffertools": "1.0.9", + "bunyan": "0.6.8", + "dtrace-provider": "0.0.7", + "nopt": "1.0.10", + "pooling": "0.1.0" }, "devDependencies": { - "tap": "0.2", + "tap": "0.2.4", "node-uuid": "1.3.3" }, "scripts": { diff --git a/test/client.test.js b/test/client.test.js index b2ac261..e0e214b 100644 --- a/test/client.test.js +++ b/test/client.test.js @@ -1,5 +1,7 @@ // Copyright 2011 Mark Cavage, Inc. All rights reserved. +var Logger = require('bunyan'); + var test = require('tap').test; var uuid = require('node-uuid'); @@ -129,7 +131,14 @@ test('setup', function (t) { server.listen(SOCKET, function () { client = ldap.createClient({ socketPath: SOCKET, - reconnect: false // turn this off for unit testing + maxConnections: process.env.LDAP_MAX_CONNS || 5, + log: new Logger({ + name: 'ldapjs_unit_test', + stream: process.stderr, + level: (process.env.LOG_LEVEL || 'info'), + serializers: Logger.stdSerializers, + idleTimeoutMillis: 10 + }) }); t.ok(client); t.end(); @@ -138,16 +147,6 @@ test('setup', function (t) { }); -test('simple bind success', function (t) { - client.bind(BIND_DN, BIND_PW, function (err, res) { - t.ifError(err); - t.ok(res); - t.equal(res.status, 0); - t.end(); - }); -}); - - test('simple bind failure', function (t) { client.bind(BIND_DN, uuid(), function (err, res) { t.ok(err); @@ -164,6 +163,16 @@ test('simple bind failure', function (t) { }); +test('simple bind success', function (t) { + client.bind(BIND_DN, BIND_PW, function (err, res) { + t.ifError(err); + t.ok(res); + t.equal(res.status, 0); + t.end(); + }); +}); + + test('add success', function (t) { var attrs = [ new Attribute({ @@ -579,6 +588,7 @@ test('search timeout (GH-51)', function (t) { }); }); + test('unbind (GH-30)', function (t) { client.unbind(function (err) { t.ifError(err); diff --git a/test/controls/paged_results_control_test.js b/test/controls/paged_results_control_test.js index 1ff97f1..e8c2f07 100644 --- a/test/controls/paged_results_control_test.js +++ b/test/controls/paged_results_control_test.js @@ -9,7 +9,7 @@ var getControl; var PagedResultsControl; function bufferEqual(t, a, b) { - t.equal(a.toString('hex'), b.toString('hex')) + t.equal(a.toString('hex'), b.toString('hex')); } @@ -38,14 +38,14 @@ test('new with args', function (t) { criticality: true, value: { size: 1000, - cookie: new Buffer([1,2,3]) + cookie: new Buffer([1, 2, 3]) } }); t.ok(c); t.equal(c.type, '1.2.840.113556.1.4.319'); t.ok(c.criticality); t.equal(c.value.size, 1000); - bufferEqual(t,c.value.cookie, new Buffer([1,2,3])); + bufferEqual(t, c.value.cookie, new Buffer([1, 2, 3])); var writer = new BerWriter(); @@ -57,7 +57,7 @@ test('new with args', function (t) { t.equal(psc.type, '1.2.840.113556.1.4.319'); t.ok(psc.criticality); t.equal(psc.value.size, 1000); - bufferEqual(t,psc.value.cookie, new Buffer([1,2,3])); + bufferEqual(t, psc.value.cookie, new Buffer([1, 2, 3])); t.end(); }); @@ -80,7 +80,7 @@ test('tober', function (t) { t.equal(c.type, '1.2.840.113556.1.4.319'); t.ok(c.criticality); t.equal(c.value.size, 20); - bufferEqual(t,c.value.cookie, new Buffer(0)); + bufferEqual(t, c.value.cookie, new Buffer(0)); t.end(); });