Comparing version 0.1.6 to 0.1.7
@@ -31,18 +31,17 @@ --- | ||
||numConnections||The size of the connection pool. Default is 1.|| | ||
||reconnect||Whether or not to automatically reconnect (and rebind) on socket errors. Takes amount of time in millliseconds. Default is 1000. 0/false will disable altogether.|| | ||
## Connection management | ||
If you'll recall, the LDAP protocol is connection-oriented, and completely | ||
asynchronous on a connection (meaning you can send as many requests as you want | ||
without waiting for responses). However, our friend `bind` is a little | ||
different in that you generally want to wait for binds to be completed since | ||
subsequent operations assume that level of privilege. | ||
As LDAP is a stateful protocol (as opposed to HTTP), having connections torn | ||
down from underneath you is difficult to deal with. As such, the ldapjs client | ||
will automatically reconnect when the underlying socket has errors. You can | ||
disable this behavior by passing `reconnect=false` in the options at construct | ||
time, or just setting the reconnect property to false at any time. | ||
The ldapjs client deals with this by maintaing a connection pool, and splaying | ||
requests across that connection pool, with the exception of `bind` and `unbind`, | ||
which it will apply to all connections in the pool. By default, a client will | ||
have one connection in the pool (since it's async already, you don't always need | ||
the complexity of a pool). And after that, the operations in the client are | ||
pretty much a mapping of the LDAP C API, but made higher-level, so they make | ||
sense in JS. | ||
On reconnect, the client will additionally automatically rebind (assuming you | ||
ever successfully called bind). Only after the rebind succeeds will other | ||
operations be allowed back through; in the meantime all callbacks will receive | ||
a `DisconnectedError`. If you never called `bind`, the client will allow | ||
operations when the socket is connected. | ||
@@ -211,9 +210,10 @@ ## Common patterns | ||
Responses from the `search` method are an `EventEmitter` where you will get a | ||
notification for each search entry that comes back from the server. You will | ||
additionally be able to listen for an `error` and `end` event. Note that the | ||
`error` event will only be for client/TCP errors, not LDAP error codes like the | ||
other APIs. You'll want to check the LDAP status code (likely for `0`) on the | ||
`end` event to assert success. LDAP search results can give you a lot of status | ||
codes, such as time or size exceeded, busy, inappropriate matching, etc., | ||
which is why this method doesn't try to wrap up the code matching. | ||
notification for each `searchEntry` that comes back from the server. You will | ||
additionally be able to listen for a `searchReference`, `error` and `end` event. | ||
Note that the `error` event will only be for client/TCP errors, not LDAP error | ||
codes like the other APIs. You'll want to check the LDAP status code | ||
(likely for `0`) on the `end` event to assert success. LDAP search results | ||
can give you a lot of status codes, such as time or size exceeded, busy, | ||
inappropriate matching, etc., which is why this method doesn't try to wrap up | ||
the code matching. | ||
@@ -233,2 +233,5 @@ Example: | ||
}); | ||
res.on('searchReference', function(referral) { | ||
console.log('referral: ' + referral.uris.join()); | ||
}); | ||
res.on('error', function(err) { | ||
@@ -235,0 +238,0 @@ console.error('error: ' + err.message); |
@@ -408,3 +408,3 @@ --- | ||
server.delete('o=example', function(req, res, next) { | ||
server.del('o=example', function(req, res, next) { | ||
console.log('DN: ' + req.dn.toString()); | ||
@@ -411,0 +411,0 @@ res.end(); |
@@ -36,2 +36,3 @@ // Copyright 2011 Mark Cavage, Inc. All rights reserved. | ||
var SearchEntry = messages.SearchEntry; | ||
var SearchReference = messages.SearchReference; | ||
var SearchResponse = messages.SearchResponse; | ||
@@ -77,10 +78,14 @@ var Parser = messages.Parser; | ||
function DisconnectedError(message) { | ||
Error.call(this, message); | ||
if (Error.captureStackTrace) | ||
Error.captureStackTrace(this, DisconnectedError); | ||
function ConnectionError(message) { | ||
errors.LDAPError.call(this, | ||
'ConnectionError', | ||
0x80, // LDAP_OTHER, | ||
message, | ||
null, | ||
ConnectionError); | ||
} | ||
util.inherits(DisconnectedError, Error); | ||
util.inherits(ConnectionError, errors.LDAPError); | ||
///--- API | ||
@@ -125,3 +130,4 @@ | ||
port: self.url ? self.url.port : options.socketPath, | ||
host: self.url ? self.url.hostname : undefined | ||
host: self.url ? self.url.hostname : undefined, | ||
socketPath: options.socketPath || undefined | ||
}; | ||
@@ -137,69 +143,5 @@ this.shutdown = false; | ||
// Build the connection pool | ||
function newConnection() { | ||
var c; | ||
if (self.secure) { | ||
c = tls.connect(self.connectOptions.port, self.connectOptions.host); | ||
} else { | ||
c = net.createConnection(self.connectOptions.port, | ||
self.connectOptions.host); | ||
} | ||
assert.ok(c); | ||
c.parser = new Parser({ | ||
log4js: self.log4js | ||
}); | ||
// Wrap the events | ||
c.ldap = { | ||
id: options.socketPath || self.url.hostname, | ||
connected: true, // lie, but node queues for us | ||
messageID: 0, | ||
messages: {} | ||
}; | ||
c.ldap.__defineGetter__('nextMessageID', function() { | ||
if (++c.ldap.messageID >= MAX_MSGID) | ||
c.ldap.messageID = 1; | ||
return c.ldap.messageID; | ||
}); | ||
c.on('connect', function() { | ||
c.ldap.connected = true; | ||
c.ldap.id += ':' + c.fd; | ||
self.emit('connect', c.ldap.id); | ||
}); | ||
c.on('end', function() { | ||
self.emit('end'); | ||
}); | ||
c.addListener('close', function(had_err) { | ||
self.emit('close', had_err); | ||
}); | ||
c.on('error', function(err) { | ||
self.emit('error', err); | ||
}); | ||
c.on('timeout', function() { | ||
self.emit('timeout'); | ||
}); | ||
c.on('data', function(data) { | ||
if (self.log.isTraceEnabled()) | ||
self.log.trace('data on %s: %s', c.ldap.id, util.inspect(data)); | ||
c.parser.write(data); | ||
}); | ||
// The "router" | ||
c.parser.on('message', function(message) { | ||
message.connection = c; | ||
var callback = c.ldap.messages[message.messageID]; | ||
if (!callback) { | ||
self.log.error('%s: received unsolicited message: %j', c.ldap.id, | ||
message.json); | ||
return; | ||
} | ||
return callback(message); | ||
}); | ||
return c; | ||
} | ||
self.connection = newConnection(); | ||
this.connection = this._newConnection(); | ||
this.reconnect = (typeof(options.reconnect) === 'number' ? | ||
options.reconnect : 1000); | ||
} | ||
@@ -217,5 +159,6 @@ util.inherits(Client, EventEmitter); | ||
* @param {Function} callback of the form f(err, res). | ||
* @param {Socket} conn don't use this. Internal only (reconnects). | ||
* @throws {TypeError} on invalid input. | ||
*/ | ||
Client.prototype.bind = function(name, credentials, controls, callback) { | ||
Client.prototype.bind = function(name, credentials, controls, callback, conn) { | ||
if (typeof(name) !== 'string') | ||
@@ -243,3 +186,10 @@ throw new TypeError('name (string) required'); | ||
return self._send(req, [errors.LDAP_SUCCESS], callback); | ||
return self._send(req, [errors.LDAP_SUCCESS], function(err, res) { | ||
if (!err) { // In case we need to reconnect later | ||
self._bindDN = name; | ||
self._credentials = credentials; | ||
} | ||
return callback(err, res); | ||
}, conn); | ||
}; | ||
@@ -590,2 +540,5 @@ | ||
if (!this.connection) | ||
return callback(new ConnectionError('no connection')); | ||
var res = new EventEmitter(); | ||
@@ -609,8 +562,10 @@ this._send(req, [errors.LDAP_SUCCESS], res); | ||
throw new TypeError('callback must be a function'); | ||
if (!callback) | ||
callback = function defUnbindCb() { self.log.trace('disconnected'); }; | ||
var self = this; | ||
this.reconnect = false; | ||
this._bindDN = null; | ||
this._credentials = null; | ||
if (!callback) | ||
callback = function defUnbindCb() { self.log.trace('disconnected'); }; | ||
var req = new UnbindRequest(); | ||
@@ -622,3 +577,3 @@ return self._send(req, 'unbind', callback); | ||
Client.prototype._send = function(message, expect, callback) { | ||
Client.prototype._send = function(message, expect, callback, connection) { | ||
assert.ok(message); | ||
@@ -628,6 +583,12 @@ assert.ok(expect); | ||
var conn = this.connection || connection; | ||
var self = this; | ||
var conn = self.connection; | ||
if (!conn) { | ||
if (typeof(callback) === 'function') | ||
return callback(new ConnectionError('no connection')); | ||
return callback.emit('error', new ConnectionError('no connection')); | ||
} | ||
// Now set up the callback in the messages table | ||
@@ -640,2 +601,3 @@ message.messageID = conn.ldap.nextMessageID; | ||
var err = null; | ||
if (res instanceof LDAPResult) { | ||
@@ -656,5 +618,14 @@ delete conn.ldap.messages[message.messageID]; | ||
callback.emit('end', res); | ||
} else if (res instanceof SearchEntry) { | ||
assert.ok(callback instanceof EventEmitter); | ||
callback.emit('searchEntry', res); | ||
} else if (res instanceof SearchReference) { | ||
assert.ok(callback instanceof EventEmitter); | ||
callback.emit('searchReference', res); | ||
} else if (res instanceof Error) { | ||
return callback(res); | ||
} else { | ||
@@ -678,5 +649,2 @@ delete conn.ldap.messages[message.messageID]; | ||
return conn.write(message.toBer(), (expect === 'unbind' ? function() { | ||
conn.on('end', function() { | ||
return callback(); | ||
}); | ||
conn.end(); | ||
@@ -686,1 +654,121 @@ } : null)); | ||
Client.prototype._newConnection = function() { | ||
var c; | ||
var connectOpts = this.connectOptions; | ||
var log = this.log; | ||
var self = this; | ||
if (this.secure) { | ||
c = tls.connect(connectOpts.port, connectOpts.host); | ||
} else { | ||
c = net.createConnection(connectOpts.port, connectOpts.host); | ||
} | ||
assert.ok(c); | ||
c.parser = new Parser({ | ||
log4js: self.log4js | ||
}); | ||
// Wrap the events | ||
c.ldap = { | ||
id: connectOpts.socketPath || self.url.hostname, | ||
messageID: 0, | ||
messages: {} | ||
}; | ||
c.ldap.__defineGetter__('nextMessageID', function() { | ||
if (++c.ldap.messageID >= MAX_MSGID) | ||
c.ldap.messageID = 1; | ||
return c.ldap.messageID; | ||
}); | ||
c.on('connect', function() { | ||
c.ldap.id += ':' + c.fd; | ||
self.emit('connect', c.ldap.id); | ||
if (log.isTraceEnabled()) | ||
log.trace('%s connect event', c.ldap.id); | ||
if (self._bindDN && self._credentials) { // reconnect case | ||
self.bind(self._bindDN, self._credentials, [], function(err) { | ||
if(err) { | ||
log.trace('%s error rebinding: %s', c.ldap.id, err.stack); | ||
return c.end(); | ||
} | ||
self.connection = c; | ||
}, c); | ||
} else { | ||
self.connection = c; | ||
} | ||
}); | ||
c.on('end', function() { | ||
self.emit('end'); | ||
if (log.isTraceEnabled()) | ||
log.trace('%s end event', c.ldap.id); | ||
}); | ||
c.addListener('close', function(had_err) { | ||
self.emit('close', had_err); | ||
if (log.isTraceEnabled()) | ||
log.trace('%s close event had_err=%s', c.ldap.id, had_err ? 'yes' : 'no'); | ||
Object.keys(c.ldap.messages).forEach(function(msgid) { | ||
if (typeof(c.ldap.messages[msgid]) === 'function') { | ||
var _cb = c.ldap.messages[msgid]; | ||
delete c.ldap.messages[msgid]; | ||
return _cb(new ConnectionError(c.ldap.id + ' closed')); | ||
} else if (c.ldap.messages[msgid]) { | ||
c.ldap.messages[msgid].emit('error', new ConnectionError(c.ldap.id + | ||
' closed')); | ||
} | ||
delete c.ldap.messages[msgid]; | ||
}); | ||
delete c.ldap; | ||
if (self.reconnect) { | ||
self.connection = null; | ||
setTimeout(function() { self._newConnection() }, self.reconnect); | ||
} | ||
}); | ||
c.on('error', function(err) { | ||
if (self.listeners('error').length) | ||
self.emit('error', err); | ||
if (log.isTraceEnabled()) | ||
log.trace('%s error event=%s', c.ldap.id, err ? err.stack : '?'); | ||
c.end(); | ||
}); | ||
c.on('timeout', function() { | ||
self.emit('timeout'); | ||
if (log.isTraceEnabled()) | ||
log.trace('%s timeout event=%s', c.ldap.id); | ||
c.end(); | ||
}); | ||
c.on('data', function(data) { | ||
if (log.isTraceEnabled()) | ||
log.trace('%s data event: %s', c.ldap.id, util.inspect(data)); | ||
c.parser.write(data); | ||
}); | ||
// The "router" | ||
c.parser.on('message', function(message) { | ||
message.connection = c; | ||
var callback = c.ldap.messages[message.messageID]; | ||
if (!callback) { | ||
log.error('%s: unsolicited message: %j', c.ldap.id, message.json); | ||
return; | ||
} | ||
return callback(message); | ||
}); | ||
return c; | ||
} |
@@ -83,2 +83,3 @@ // Copyright 2011 Mark Cavage, Inc. All rights reserved. | ||
module.exports = {}; | ||
module.exports.LDAPError = LDAPError; | ||
@@ -85,0 +86,0 @@ Object.keys(CODES).forEach(function(code) { |
@@ -23,2 +23,3 @@ // Copyright 2011 Mark Cavage, Inc. All rights reserved. | ||
var SearchEntry = require('./search_entry'); | ||
var SearchReference = require('./search_reference'); | ||
var SearchResponse = require('./search_response'); | ||
@@ -54,2 +55,3 @@ var UnbindRequest = require('./unbind_request'); | ||
SearchEntry: SearchEntry, | ||
SearchReference: SearchReference, | ||
SearchResponse: SearchResponse, | ||
@@ -56,0 +58,0 @@ UnbindRequest: UnbindRequest, |
@@ -25,2 +25,3 @@ // Copyright 2011 Mark Cavage, Inc. All rights reserved. | ||
var SearchEntry = require('./search_entry'); | ||
var SearchReference = require('./search_reference'); | ||
var SearchResponse = require('./search_response'); | ||
@@ -215,2 +216,6 @@ var UnbindRequest = require('./unbind_request'); | ||
case Protocol.LDAP_REP_SEARCH_REF: | ||
Message = SearchReference; | ||
break; | ||
case Protocol.LDAP_REP_SEARCH: | ||
@@ -217,0 +222,0 @@ Message = SearchResponse; |
@@ -8,4 +8,6 @@ // Copyright 2011 Mark Cavage, Inc. All rights reserved. | ||
var SearchEntry = require('./search_entry'); | ||
var SearchReference = require('./search_reference'); | ||
var parseDN = require('../dn').parse; | ||
var parseURL = require('../url').parse; | ||
var Protocol = require('../protocol'); | ||
@@ -49,3 +51,8 @@ | ||
if (!(entry instanceof SearchEntry)) { | ||
if (entry instanceof SearchEntry || entry instanceof SearchReference) { | ||
if (!entry.messageID) | ||
entry.messageID = this.messageID; | ||
if (entry.messageID !== this.messageID) | ||
throw new Error('SearchEntry messageID mismatch'); | ||
} else { | ||
if (!entry.dn) | ||
@@ -77,7 +84,2 @@ throw new Error('entry.dn required'); | ||
entry.fromObject(save); | ||
} else { | ||
if (!entry.messageID) | ||
entry.messageID = this.messageID; | ||
if (entry.messageID !== this.messageID) | ||
throw new Error('SearchEntry messageID mismatch'); | ||
} | ||
@@ -114,1 +116,22 @@ | ||
}; | ||
SearchResponse.prototype.createSearchReference = function(uris) { | ||
if (!uris) | ||
throw new TypeError('uris ([string]) required'); | ||
if (!Array.isArray(uris)) | ||
uris = [uris]; | ||
for (var i = 0; i < uris.length; i++) { | ||
if (typeof(uris[i]) == 'string') | ||
uris[i] = parseURL(uris[i]); | ||
} | ||
var self = this; | ||
return new SearchReference({ | ||
messageID: self.messageID, | ||
log4js: self.log4js, | ||
uris: uris | ||
}); | ||
}; |
@@ -6,3 +6,3 @@ { | ||
"description": "LDAP client and server APIs", | ||
"version": "0.1.6", | ||
"version": "0.1.7", | ||
"repository": { | ||
@@ -9,0 +9,0 @@ "type": "git", |
@@ -79,11 +79,17 @@ // Copyright 2011 Mark Cavage, Inc. All rights reserved. | ||
server.search(SUFFIX, function(req, res, next) { | ||
var e = res.createSearchEntry({ | ||
objectName: req.dn, | ||
attributes: { | ||
cn: ['unit', 'test'], | ||
sn: 'testy' | ||
} | ||
}); | ||
res.send(e); | ||
res.send(e); | ||
if (!req.dn.equals('cn=ref,' + SUFFIX)) { | ||
var e = res.createSearchEntry({ | ||
objectName: req.dn, | ||
attributes: { | ||
cn: ['unit', 'test'], | ||
sn: 'testy' | ||
} | ||
}); | ||
res.send(e); | ||
res.send(e); | ||
} else { | ||
res.send(res.createSearchReference('ldap://localhost')); | ||
} | ||
res.end(); | ||
@@ -100,6 +106,7 @@ return next(); | ||
client = ldap.createClient({ | ||
socketPath: SOCKET | ||
socketPath: SOCKET, | ||
reconnect: false // turn this off for unit testing | ||
}); | ||
t.ok(client); | ||
// client.log4js.setLevel('Debug'); | ||
client.log4js.setLevel('Trace'); | ||
t.end(); | ||
@@ -348,2 +355,33 @@ }); | ||
test('search referral', function(t) { | ||
client.search('cn=ref, ' + SUFFIX, '(objectclass=*)', function(err, res) { | ||
t.ifError(err); | ||
t.ok(res); | ||
var gotEntry = 0; | ||
var gotReferral = false; | ||
res.on('searchEntry', function(entry) { | ||
gotEntry++; | ||
}); | ||
res.on('searchReference', function(referral) { | ||
gotReferral = true; | ||
t.ok(referral); | ||
t.ok(referral instanceof ldap.SearchReference); | ||
t.ok(referral.uris); | ||
t.ok(referral.uris.length); | ||
}); | ||
res.on('error', function(err) { | ||
t.fail(err); | ||
}); | ||
res.on('end', function(res) { | ||
t.ok(res); | ||
t.ok(res instanceof ldap.SearchResponse); | ||
t.equal(res.status, 0); | ||
t.equal(gotEntry, 0); | ||
t.ok(gotReferral); | ||
t.end(); | ||
}); | ||
}); | ||
}); | ||
test('shutdown', function(t) { | ||
@@ -350,0 +388,0 @@ client.unbind(function() { |
Sorry, the diff of this file is not supported yet
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
622288
114
9388