diff --git a/docs/client.md b/docs/client.md index 3aee7555..a12fcb88 100644 --- a/docs/client.md +++ b/docs/client.md @@ -14,7 +14,7 @@ The code to create a new client looks like: var ldap = require('ldapjs'); var client = ldap.createClient({ - url: 'ldap://127.0.0.1:1389' + url: ['ldap://127.0.0.1:1389', 'ldap://127.0.0.2:1389'] }); You can use `ldap://` or `ldaps://`; the latter would connect over SSL (note @@ -24,7 +24,7 @@ client is: |Attribute |Description | |---------------|-----------------------------------------------------------| -|url |A valid LDAP URL (proto/host/port only) | +|url |A string or array of valid LDAP URL(s) (proto/host/port) | |socketPath |Socket path if using AF\_UNIX sockets | |log |A compatible logger instance (Default: no-op logger) | |timeout |Milliseconds client should let operations live for before timing out (Default: Infinity)| @@ -34,6 +34,13 @@ client is: |strictDN |Force strict DN parsing for client methods (Default is true)| |reconnect |Try to reconnect when the connection gets lost (Default is false)| +### url +This parameter takes a single connection string or an array of connection strings +as an input. In case an array is provided, the client tries to connect to the +servers in given order. To achieve random server strategy (e.g. to distribute +the load among the servers), please shuffle the array before passing it as an +argument. + ### Note On Logger A passed in logger is expected to conform to the [Bunyan](https://www.npmjs.com/package/bunyan) diff --git a/lib/client/client.js b/lib/client/client.js index b254d506..daf9314b 100644 --- a/lib/client/client.js +++ b/lib/client/client.js @@ -110,12 +110,13 @@ function Client (options) { EventEmitter.call(this, options) var self = this - var _url - if (options.url) { _url = url.parse(options.url) } - this.host = _url ? _url.hostname : undefined - this.port = _url ? _url.port : false - this.secure = _url ? _url.secure : false - this.url = _url + this.urls = options.url ? [].concat(options.url).map(url.parse) : [] + this._nextServer = 0 + // updated in connectSocket() after each connect + this.host = undefined + this.port = undefined + this.secure = undefined + this.url = undefined this.tlsOptions = options.tlsOptions this.socketPath = options.socketPath || false @@ -792,6 +793,9 @@ Client.prototype.connect = function connect () { // Establish basic socket connection function connectSocket (cb) { + var server = self.urls[self._nextServer] + self._nextServer = (self._nextServer + 1) % self.urls.length + cb = once(cb) function onResult (err, res) { @@ -820,16 +824,17 @@ Client.prototype.connect = function connect () { setupClient(cb) } - var port = (self.port || self.socketPath) - if (self.secure) { - socket = tls.connect(port, self.host, self.tlsOptions) + var port = (server && server.port) || self.socketPath + var host = server && server.hostname + if (server && server.secure) { + socket = tls.connect(port, host, self.tlsOptions) socket.once('secureConnect', onConnect) } else { - socket = net.connect(port, self.host) + socket = net.connect(port, host) socket.once('connect', onConnect) } socket.once('error', onResult) - initSocket() + initSocket(server) // Setup connection timeout handling, if desired if (self.connectTimeout) { @@ -844,9 +849,9 @@ Client.prototype.connect = function connect () { } // Initialize socket events and LDAP parser. - function initSocket () { + function initSocket (url) { tracker = messageTrackerFactory({ - id: self.url ? self.url.href : self.socketPath, + id: url ? url.href : self.socketPath, parser: new Parser({ log: log }) }) @@ -965,6 +970,13 @@ Client.prototype.connect = function connect () { self.emit('socketTimeout') socket.end() }) + + var server = self.urls[self._nextServer] + if (server) { + self.host = server.hostname + self.port = server.port + self.secure = server.secure + } } var retry @@ -975,12 +987,15 @@ Client.prototype.connect = function connect () { maxDelay: this.reconnect.maxDelay }) failAfter = this.reconnect.failAfter + if (this.urls.length > 1 && failAfter) { + failAfter *= this.urls.length + } } else { retry = backoff.exponential({ initialDelay: 1, maxDelay: 2 }) - failAfter = 1 + failAfter = this.urls.length || 1 } retry.failAfter(failAfter) diff --git a/lib/client/index.js b/lib/client/index.js index 0c23052d..fb9db3aa 100644 --- a/lib/client/index.js +++ b/lib/client/index.js @@ -7,7 +7,7 @@ module.exports = { Client: Client, createClient: function createClient (options) { if (isObject(options) === false) throw TypeError('options (object) required') - if (options.url && typeof options.url !== 'string') throw TypeError('options.url (string) required') + if (options.url && typeof options.url !== 'string' && !Array.isArray(options.url)) throw TypeError('options.url (string|array) required') if (options.socketPath && typeof options.socketPath !== 'string') throw TypeError('options.socketPath must be a string') if ((options.url && options.socketPath) || !(options.url || options.socketPath)) throw TypeError('options.url ^ options.socketPath (String) required') if (!options.log) options.log = logger diff --git a/test/client.test.js b/test/client.test.js index bd9b9a28..910adf08 100644 --- a/test/client.test.js +++ b/test/client.test.js @@ -340,10 +340,9 @@ tap.test('createClient', t => { t.throws(() => ldap.createClient(42), match) }) - t.test('url must be a string', async t => { - const match = /options\.url \(string\) required/ + t.test('url must be a string or array', async t => { + const match = /options\.url \(string\|array\) required/ t.throws(() => ldap.createClient({ url: {} }), match) - t.throws(() => ldap.createClient({ url: [] }), match) t.throws(() => ldap.createClient({ url: 42 }), match) }) @@ -379,6 +378,20 @@ tap.test('createClient', t => { } }) + t.test('url array is correctly assigned', async t => { + getPort().then(function (unusedPortNumber) { + const client = ldap.createClient({ + url: [ + `ldap://127.0.0.1:${unusedPortNumber}`, + `ldap://127.0.0.2:${unusedPortNumber}` + ], + connectTimeout: 1 + }) + + t.equal(client.urls.length, 2) + }) + }) + // TODO: this test is really flaky. It would be better if we could validate // the options _withouth_ having to connect to a server. // t.test('attaches a child function to logger', async t => {