8 |
9 | This document covers the ldapjs client API and assumes that you are familiar
10 | with LDAP. If you're not, read the [guide](guide.html) first.
11 |
12 |
13 |
14 | # Create a client
15 |
16 | The code to create a new client looks like:
17 |
18 | ```js
19 | const ldap = require('ldapjs');
20 |
21 | const client = ldap.createClient({
22 | url: ['ldap://127.0.0.1:1389', 'ldap://127.0.0.2:1389']
23 | });
24 |
25 | client.on('connectError', (err) => {
26 | // handle connection error
27 | })
28 | ```
29 |
30 | You can use `ldap://` or `ldaps://`; the latter would connect over SSL (note
31 | that this will not use the LDAP TLS extended operation, but literally an SSL
32 | connection to port 636, as in LDAP v2). The full set of options to create a
33 | client is:
34 |
35 | |Attribute |Description |
36 | |---------------|-----------------------------------------------------------|
37 | |url |A string or array of valid LDAP URL(s) (proto/host/port) |
38 | |socketPath |Socket path if using AF\_UNIX sockets |
39 | |log |A compatible logger instance (Default: no-op logger) |
40 | |timeout |Milliseconds client should let operations live for before timing out (Default: Infinity)|
41 | |connectTimeout |Milliseconds client should wait before timing out on TCP connections (Default: OS default)|
42 | |tlsOptions |Additional options passed to TLS connection layer when connecting via `ldaps://` (See: The TLS docs for node.js)|
43 | |idleTimeout |Milliseconds after last activity before client emits idle event|
44 | |reconnect |Try to reconnect when the connection gets lost (Default is false)|
45 |
46 | ### url
47 | This parameter takes a single connection string or an array of connection strings
48 | as an input. In case an array is provided, the client tries to connect to the
49 | servers in given order. To achieve random server strategy (e.g. to distribute
50 | the load among the servers), please shuffle the array before passing it as an
51 | argument.
52 |
53 | ### Note On Logger
54 |
55 | A passed in logger is expected to conform to the [Bunyan](https://www.npmjs.com/package/bunyan)
56 | API. Specifically, the logger is expected to have a `child()` method. If a logger
57 | is supplied that does not have such a method, then a shim version is added
58 | that merely returns the passed in logger.
59 |
60 | Known compatible loggers are:
61 |
62 | + [Bunyan](https://www.npmjs.com/package/bunyan)
63 | + [Pino](https://www.npmjs.com/package/pino)
64 |
65 |
66 | ### Note On Error Handling
67 |
68 | The client is an `EventEmitter`. If you don't register an error handler and
69 | e.g. a connection error occurs, Node.js will print a stack trace and exit the
70 | process ([reference](https://nodejs.org/api/events.html#error-events)).
71 |
72 | ## Connection management
73 |
74 | As LDAP is a stateful protocol (as opposed to HTTP), having connections torn
75 | down from underneath you can be difficult to deal with. Several mechanisms
76 | have been provided to mitigate this trouble.
77 |
78 | ### Reconnect
79 |
80 | You can provide a Boolean option indicating if a reconnect should be tried. For
81 | more sophisticated control, you can provide an Object with the properties
82 | `initialDelay` (default: `100`), `maxDelay` (default: `10000`) and
83 | `failAfter` (default: `Infinity`).
84 | After the reconnect you maybe need to [bind](#bind) again.
85 |
86 | ## Client events
87 |
88 | The client is an `EventEmitter` and can emit the following events:
89 |
90 | |Event |Description |
91 | |---------------|----------------------------------------------------------|
92 | |error |General error |
93 | |connectRefused |Server refused connection. Most likely bad authentication |
94 | |connectTimeout |Server timeout |
95 | |connectError |Socket connection error |
96 | |setupError |Setup error after successful connection |
97 | |socketTimeout |Socket timeout |
98 | |resultError |Search result error |
99 | |timeout |Search result timeout |
100 | |destroy |After client is disconnected |
101 | |end |Socket end event |
102 | |close |Socket closed |
103 | |connect |Client connected |
104 | |idle |Idle timeout reached |
105 |
106 | ## Common patterns
107 |
108 | The last two parameters in every API are `controls` and `callback`. `controls`
109 | can be either a single instance of a `Control` or an array of `Control` objects.
110 | You can, and probably will, omit this option.
111 |
112 | Almost every operation has the callback form of `function(err, res)` where err
113 | will be an instance of an `LDAPError` (you can use `instanceof` to switch).
114 | You probably won't need to check the `res` parameter, but it's there if you do.
115 |
116 | # bind
117 | `bind(dn, password, controls, callback)`
118 |
119 | Performs a bind operation against the LDAP server.
120 |
121 | The bind API only allows LDAP 'simple' binds (equivalent to HTTP Basic
122 | Authentication) for now. Note that all client APIs can optionally take an array
123 | of `Control` objects. You probably don't need them though...
124 |
125 | Example:
126 |
127 | ```js
128 | client.bind('cn=root', 'secret', (err) => {
129 | assert.ifError(err);
130 | });
131 | ```
132 |
133 | # add
134 | `add(dn, entry, controls, callback)`
135 |
136 | Performs an add operation against the LDAP server.
137 |
138 | Allows you to add an entry (which is just a plain JS object), and as always,
139 | controls are optional.
140 |
141 | Example:
142 |
143 | ```js
144 | const entry = {
145 | cn: 'foo',
146 | sn: 'bar',
147 | email: ['foo@bar.com', 'foo1@bar.com'],
148 | objectclass: 'fooPerson'
149 | };
150 | client.add('cn=foo, o=example', entry, (err) => {
151 | assert.ifError(err);
152 | });
153 | ```
154 |
155 | # compare
156 | `compare(dn, attribute, value, controls, callback)`
157 |
158 | Performs an LDAP compare operation with the given attribute and value against
159 | the entry referenced by dn.
160 |
161 | Example:
162 |
163 | ```js
164 | client.compare('cn=foo, o=example', 'sn', 'bar', (err, matched) => {
165 | assert.ifError(err);
166 |
167 | console.log('matched: ' + matched);
168 | });
169 | ```
170 |
171 | # del
172 | `del(dn, controls, callback)`
173 |
174 |
175 | Deletes an entry from the LDAP server.
176 |
177 | Example:
178 |
179 | ```js
180 | client.del('cn=foo, o=example', (err) => {
181 | assert.ifError(err);
182 | });
183 | ```
184 |
185 | # exop
186 | `exop(name, value, controls, callback)`
187 |
188 | Performs an LDAP extended operation against an LDAP server. `name` is typically
189 | going to be an OID (well, the RFC says it must be; however, ldapjs has no such
190 | restriction). `value` is completely arbitrary, and is whatever the exop says it
191 | should be.
192 |
193 | Example (performs an LDAP 'whois' extended op):
194 |
195 | ```js
196 | client.exop('1.3.6.1.4.1.4203.1.11.3', (err, value, res) => {
197 | assert.ifError(err);
198 |
199 | console.log('whois: ' + value);
200 | });
201 | ```
202 |
203 | # modify
204 | `modify(name, changes, controls, callback)`
205 |
206 | Performs an LDAP modify operation against the LDAP server. This API requires
207 | you to pass in a `Change` object, which is described below. Note that you can
208 | pass in a single `Change` or an array of `Change` objects.
209 |
210 | Example:
211 |
212 | ```js
213 | const change = new ldap.Change({
214 | operation: 'add',
215 | modification: {
216 | type: 'pets',
217 | values: ['cat', 'dog']
218 | }
219 | });
220 |
221 | client.modify('cn=foo, o=example', change, (err) => {
222 | assert.ifError(err);
223 | });
224 | ```
225 |
226 | ## Change
227 |
228 | A `Change` object maps to the LDAP protocol of a modify change, and requires you
229 | to set the `operation` and `modification`. The `operation` is a string, and
230 | must be one of:
231 |
232 | | Operation | Description |
233 | |-----------|-------------|
234 | | replace | Replaces the attribute referenced in `modification`. If the modification has no values, it is equivalent to a delete. |
235 | | add | Adds the attribute value(s) referenced in `modification`. The attribute may or may not already exist. |
236 | | delete | Deletes the attribute (and all values) referenced in `modification`. |
237 |
238 | `modification` is just a plain old JS object with the required type and values you want.
239 |
240 | | Operation | Description |
241 | |-----------|-------------|
242 | | type | String that defines the attribute type for the modification. |
243 | | values | Defines the values for modification. |
244 |
245 |
246 | # modifyDN
247 | `modifyDN(dn, newDN, controls, callback)`
248 |
249 | Performs an LDAP modifyDN (rename) operation against an entry in the LDAP
250 | server. A couple points with this client API:
251 |
252 | * There is no ability to set "keep old dn." It's always going to flag the old
253 | dn to be purged.
254 | * The client code will automatically figure out if the request is a "new
255 | superior" request ("new superior" means move to a different part of the tree,
256 | as opposed to just renaming the leaf).
257 |
258 | Example:
259 |
260 | ```js
261 | client.modifyDN('cn=foo, o=example', 'cn=bar', (err) => {
262 | assert.ifError(err);
263 | });
264 | ```
265 |
266 | # search
267 | `search(base, options, controls, callback)`
268 |
269 | Performs a search operation against the LDAP server.
270 |
271 | The search operation is more complex than the other operations, so this one
272 | takes an `options` object for all the parameters. However, ldapjs makes some
273 | defaults for you so that if you pass nothing in, it's pretty much equivalent
274 | to an HTTP GET operation (i.e., base search against the DN, filter set to
275 | always match).
276 |
277 | Like every other operation, `base` is a DN string.
278 |
279 | Options can be a string representing a valid LDAP filter or an object
280 | containing the following fields:
281 |
282 | |Attribute |Description |
283 | |-----------|---------------------------------------------------|
284 | |scope |One of `base`, `one`, or `sub`. Defaults to `base`.|
285 | |filter |A string version of an LDAP filter (see below), or a programatically constructed `Filter` object. Defaults to `(objectclass=*)`.|
286 | |attributes |attributes to select and return (if these are set, the server will return *only* these attributes). Defaults to the empty set, which means all attributes. You can provide a string if you want a single attribute or an array of string for one or many.|
287 | |attrsOnly |boolean on whether you want the server to only return the names of the attributes, and not their values. Borderline useless. Defaults to false.|
288 | |sizeLimit |the maximum number of entries to return. Defaults to 0 (unlimited).|
289 | |timeLimit |the maximum amount of time the server should take in responding, in seconds. Defaults to 10. Lots of servers will ignore this.|
290 | |paged |enable and/or configure automatic result paging|
291 |
292 | Responses inside callback of the `search` method are an `EventEmitter` where you will get a notification for
293 | each `searchEntry` that comes back from the server. You will additionally be able to listen for a `searchRequest`
294 | , `searchReference`, `error` and `end` event.
295 | `searchRequest` is emitted immediately after every `SearchRequest` is sent with a `SearchRequest` parameter. You can do operations
296 | like `client.abandon` with `searchRequest.messageId` to abandon this search request. Note that the `error` event will
297 | only be for client/TCP errors, not LDAP error codes like the other APIs. You'll want to check the LDAP status code
298 | (likely for `0`) on the `end` event to assert success. LDAP search results can give you a lot of status codes, such as
299 | time or size exceeded, busy, inappropriate matching, etc., which is why this method doesn't try to wrap up the code
300 | matching.
301 |
302 | Example:
303 |
304 | ```js
305 | const opts = {
306 | filter: '(&(l=Seattle)(email=*@foo.com))',
307 | scope: 'sub',
308 | attributes: ['dn', 'sn', 'cn']
309 | };
310 |
311 | client.search('o=example', opts, (err, res) => {
312 | assert.ifError(err);
313 |
314 | res.on('searchRequest', (searchRequest) => {
315 | console.log('searchRequest: ', searchRequest.messageId);
316 | });
317 | res.on('searchEntry', (entry) => {
318 | console.log('entry: ' + JSON.stringify(entry.pojo));
319 | });
320 | res.on('searchReference', (referral) => {
321 | console.log('referral: ' + referral.uris.join());
322 | });
323 | res.on('error', (err) => {
324 | console.error('error: ' + err.message);
325 | });
326 | res.on('end', (result) => {
327 | console.log('status: ' + result.status);
328 | });
329 | });
330 | ```
331 |
332 | ## Filter Strings
333 |
334 | The easiest way to write search filters is to write them compliant with RFC2254,
335 | which is "The string representation of LDAP search filters." Note that
336 | ldapjs doesn't support extensible matching, since it's one of those features
337 | that almost nobody actually uses in practice.
338 |
339 | Assuming you don't really want to read the RFC, search filters in LDAP are
340 | basically are a "tree" of attribute/value assertions, with the tree specified
341 | in prefix notation. For example, let's start simple, and build up a complicated
342 | filter. The most basic filter is equality, so let's assume you want to search
343 | for an attribute `email` with a value of `foo@bar.com`. The syntax would be:
344 |
345 | ```
346 | (email=foo@bar.com)
347 | ```
348 |
349 | ldapjs requires all filters to be surrounded by '()' blocks. Ok, that was easy.
350 | Let's now assume that you want to find all records where the email is actually
351 | just anything in the "@bar.com" domain and the location attribute is set to
352 | Seattle:
353 |
354 | ```
355 | (&(email=*@bar.com)(l=Seattle))
356 | ```
357 |
358 | Now our filter is actually three LDAP filters. We have an `and` filter (single
359 | amp `&`), an `equality` filter `(the l=Seattle)`, and a `substring` filter.
360 | Substrings are wildcard filters. They use `*` as the wildcard. You can put more
361 | than one wildcard for a given string. For example you could do `(email=*@*bar.com)`
362 | to match any email of @bar.com or its subdomains like `"example@foo.bar.com"`.
363 |
364 | Now, let's say we also want to set our filter to include a
365 | specification that either the employeeType *not* be a manager nor a secretary:
366 |
367 | ```
368 | (&(email=*@bar.com)(l=Seattle)(!(|(employeeType=manager)(employeeType=secretary))))
369 | ```
370 |
371 | The `not` character is represented as a `!`, the `or` as a single pipe `|`.
372 | It gets a little bit complicated, but it's actually quite powerful, and lets you
373 | find almost anything you're looking for.
374 |
375 | ## Paging
376 | Many LDAP server enforce size limits upon the returned result set (commonly
377 | 1000). In order to retrieve results beyond this limit, a `PagedResultControl`
378 | is passed between the client and server to iterate through the entire dataset.
379 | While callers could choose to do this manually via the `controls` parameter to
380 | `search()`, ldapjs has internal mechanisms to easily automate the process. The
381 | most simple way to use the paging automation is to set the `paged` option to
382 | true when performing a search:
383 |
384 | ```js
385 | const opts = {
386 | filter: '(objectclass=commonobject)',
387 | scope: 'sub',
388 | paged: true,
389 | sizeLimit: 200
390 | };
391 | client.search('o=largedir', opts, (err, res) => {
392 | assert.ifError(err);
393 | res.on('searchEntry', (entry) => {
394 | // do per-entry processing
395 | });
396 | res.on('page', (result) => {
397 | console.log('page end');
398 | });
399 | res.on('error', (resErr) => {
400 | assert.ifError(resErr);
401 | });
402 | res.on('end', (result) => {
403 | console.log('done ');
404 | });
405 | });
406 | ```
407 |
408 | This will enable paging with a default page size of 199 (`sizeLimit` - 1) and
409 | will output all of the resulting objects via the `searchEntry` event. At the
410 | end of each result during the operation, a `page` event will be emitted as
411 | well (which includes the intermediate `searchResult` object).
412 |
413 | For those wanting more precise control over the process, an object with several
414 | parameters can be provided for the `paged` option. The `pageSize` parameter
415 | sets the size of result pages requested from the server. If no value is
416 | specified, it will fall back to the default (100 or `sizeLimit` - 1, to obey
417 | the RFC). The `pagePause` parameter allows back-pressure to be exerted on the
418 | paged search operation by pausing at the end of each page. When enabled, a
419 | callback function is passed as an additional parameter to `page` events. The
420 | client will wait to request the next page until that callback is executed.
421 |
422 | Here is an example where both of those parameters are used:
423 |
424 | ```js
425 | const queue = new MyWorkQueue(someSlowWorkFunction);
426 | const opts = {
427 | filter: '(objectclass=commonobject)',
428 | scope: 'sub',
429 | paged: {
430 | pageSize: 250,
431 | pagePause: true
432 | },
433 | };
434 | client.search('o=largerdir', opts, (err, res) => {
435 | assert.ifError(err);
436 | res.on('searchEntry', (entry) => {
437 | // Submit incoming objects to queue
438 | queue.push(entry);
439 | });
440 | res.on('page', (result, cb) => {
441 | // Allow the queue to flush before fetching next page
442 | queue.cbWhenFlushed(cb);
443 | });
444 | res.on('error', (resErr) => {
445 | assert.ifError(resErr);
446 | });
447 | res.on('end', (result) => {
448 | console.log('done');
449 | });
450 | });
451 | ```
452 |
453 | # starttls
454 | `starttls(options, controls, callback)`
455 |
456 | Attempt to secure existing LDAP connection via STARTTLS.
457 |
458 | Example:
459 |
460 | ```js
461 | const opts = {
462 | ca: [fs.readFileSync('mycacert.pem')]
463 | };
464 |
465 | client.starttls(opts, (err, res) => {
466 | assert.ifError(err);
467 |
468 | // Client communication now TLS protected
469 | });
470 | ```
471 |
472 |
473 | # unbind
474 | `unbind(callback)`
475 |
476 | Performs an unbind operation against the LDAP server.
477 |
478 | Note that unbind operation is not an opposite operation
479 | for bind. Unbinding results in disconnecting the client
480 | regardless of whether a bind operation was performed.
481 |
482 | The `callback` argument is optional as unbind does
483 | not have a response.
484 |
485 | Example:
486 |
487 | ```js
488 | client.unbind((err) => {
489 | assert.ifError(err);
490 | });
491 | ```
492 |
--------------------------------------------------------------------------------
/docs/dn.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: DN API | ldapjs
3 | ---
4 |
5 | # ldapjs DN API
6 |
7 |
8 |
9 | This document covers the ldapjs DN API and assumes that you are familiar
10 | with LDAP. If you're not, read the [guide](guide.html) first.
11 |
12 |
13 |
14 | DNs are LDAP distinguished names, and are composed of a set of RDNs (relative
15 | distinguished names). [RFC2253](http://www.ietf.org/rfc/rfc2253.txt) has the
16 | complete specification, but basically an RDN is an attribute value assertion
17 | with `=` as the seperator, like: `cn=foo` where 'cn' is 'commonName' and 'foo'
18 | is the value. You can have compound RDNs by using the `+` character:
19 | `cn=foo+sn=bar`. As stated above, DNs are a set of RDNs, typically separated
20 | with the `,` character, like: `cn=foo, ou=people, o=example`. This uniquely
21 | identifies an entry in the tree, and is read "bottom up".
22 |
23 | # parseDN(dnString)
24 |
25 | The `parseDN` API converts a string representation of a DN into an ldapjs DN
26 | object; in most cases this will be handled for you under the covers of the
27 | ldapjs framework, but if you need it, it's there.
28 |
29 | ```js
30 | const parseDN = require('ldapjs').parseDN;
31 |
32 | const dn = parseDN('cn=foo+sn=bar, ou=people, o=example');
33 | console.log(dn.toString());
34 | ```
35 |
36 | # DN
37 |
38 | The DN object is largely what you'll be interacting with, since all the server
39 | APIs are setup to give you a DN object.
40 |
41 | ## childOf(dn)
42 |
43 | Returns a boolean indicating whether 'this' is a child of the passed in dn. The
44 | `dn` argument can be either a string or a DN.
45 |
46 | ```js
47 | server.add('o=example', (req, res, next) => {
48 | if (req.dn.childOf('ou=people, o=example')) {
49 | ...
50 | } else {
51 | ...
52 | }
53 | });
54 | ```
55 |
56 | ## parentOf(dn)
57 |
58 | The inverse of `childOf`; returns a boolean on whether or not `this` is a parent
59 | of the passed in dn. Like `childOf`, can take either a string or a DN.
60 |
61 | ```js
62 | server.add('o=example', (req, res, next) => {
63 | const dn = parseDN('ou=people, o=example');
64 | if (dn.parentOf(req.dn)) {
65 | ...
66 | } else {
67 | ...
68 | }
69 | });
70 | ```
71 |
72 | ## equals(dn)
73 |
74 | Returns a boolean indicating whether `this` is equivalent to the passed in `dn`
75 | argument. `dn` can be a string or a DN.
76 |
77 | ```js
78 | server.add('o=example', (req, res, next) => {
79 | if (req.dn.equals('cn=foo, ou=people, o=example')) {
80 | ...
81 | } else {
82 | ...
83 | }
84 | });
85 | ```
86 |
87 | ## parent()
88 |
89 | Returns a DN object that is the direct parent of `this`. If there is no parent
90 | this can return `null` (e.g. `parseDN('o=example').parent()` will return null).
91 |
92 |
93 | ## format(options)
94 |
95 | Convert a DN object to string according to specified formatting options. These
96 | options are divided into two types. Preservation Options use data recorded
97 | during parsing to preserve details of the original DN. Modification options
98 | alter string formatting defaults. Preservation options _always_ take
99 | precedence over Modification Options.
100 |
101 | Preservation Options:
102 |
103 | - `keepOrder`: Order of multi-value RDNs.
104 | - `keepQuote`: RDN values which were quoted will remain so.
105 | - `keepSpace`: Leading/trailing spaces will be output.
106 | - `keepCase`: Parsed attribute name will be output instead of lowercased version.
107 |
108 | Modification Options:
109 |
110 | - `upperName`: RDN names will be uppercased instead of lowercased.
111 | - `skipSpace`: Disable trailing space after RDN separators
112 |
113 | ## setFormat(options)
114 |
115 | Sets the default `options` for string formatting when `toString` is called.
116 | It accepts the same parameters as `format`.
117 |
118 |
119 | ## toString()
120 |
121 | Returns the string representation of `this`.
122 |
123 | ```js
124 | server.add('o=example', (req, res, next) => {
125 | console.log(req.dn.toString());
126 | });
127 | ```
128 |
--------------------------------------------------------------------------------
/docs/errors.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: Errors API | ldapjs
3 | ---
4 |
5 | # ldapjs Errors API
6 |
7 |
8 |
9 | This document covers the ldapjs errors API and assumes that you are familiar
10 | with LDAP. If you're not, read the [guide](guide.html) first.
11 |
12 |
13 |
14 | All errors in the ldapjs framework extend from an abstract error type called
15 | `LDAPError`. In addition to the properties listed below, all errors will have
16 | a `stack` property correctly set.
17 |
18 | In general, you'll be using the errors in ldapjs like:
19 |
20 | ```js
21 | const ldap = require('ldapjs');
22 |
23 | const db = {};
24 |
25 | server.add('o=example', (req, res, next) => {
26 | const parent = req.dn.parent();
27 | if (parent) {
28 | if (!db[parent.toString()])
29 | return next(new ldap.NoSuchObjectError(parent.toString()));
30 | }
31 | if (db[req.dn.toString()])
32 | return next(new ldap.EntryAlreadyExistsError(req.dn.toString()));
33 |
34 | ...
35 | });
36 | ```
37 |
38 | I.e., if you just pass them into the `next()` handler, ldapjs will automatically
39 | return the appropriate LDAP error message, and stop the handler chain.
40 |
41 | All errors will have the following properties:
42 |
43 | ## code
44 |
45 | Returns the LDAP status code associated with this error.
46 |
47 | ## name
48 |
49 | The name of this error.
50 |
51 | ## message
52 |
53 | The message that will be returned to the client.
54 |
55 | # Complete list of LDAPError subclasses
56 |
57 | * OperationsError
58 | * ProtocolError
59 | * TimeLimitExceededError
60 | * SizeLimitExceededError
61 | * CompareFalseError
62 | * CompareTrueError
63 | * AuthMethodNotSupportedError
64 | * StrongAuthRequiredError
65 | * ReferralError
66 | * AdminLimitExceededError
67 | * UnavailableCriticalExtensionError
68 | * ConfidentialityRequiredError
69 | * SaslBindInProgressError
70 | * NoSuchAttributeError
71 | * UndefinedAttributeTypeError
72 | * InappropriateMatchingError
73 | * ConstraintViolationError
74 | * AttributeOrValueExistsError
75 | * InvalidAttriubteSyntaxError
76 | * NoSuchObjectError
77 | * AliasProblemError
78 | * InvalidDnSyntaxError
79 | * AliasDerefProblemError
80 | * InappropriateAuthenticationError
81 | * InvalidCredentialsError
82 | * InsufficientAccessRightsError
83 | * BusyError
84 | * UnavailableError
85 | * UnwillingToPerformError
86 | * LoopDetectError
87 | * NamingViolationError
88 | * ObjectclassViolationError
89 | * NotAllowedOnNonLeafError
90 | * NotAllowedOnRdnError
91 | * EntryAlreadyExistsError
92 | * ObjectclassModsProhibitedError
93 | * AffectsMultipleDsasError
94 | * OtherError
95 |
--------------------------------------------------------------------------------
/docs/examples.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: Examples | ldapjs
3 | ---
4 |
5 | # ldapjs Examples
6 |
7 |
8 |
9 | This page contains a (hopefully) growing list of sample code to get you started
10 | with ldapjs.
11 |
12 |
8 |
9 | This document covers the ldapjs filters API and assumes that you are familiar
10 | with LDAP. If you're not, read the [guide](guide.html) first.
11 |
12 |
13 |
14 | LDAP search filters are really the backbone of LDAP search operations, and
15 | ldapjs tries to get you in "easy" with them if your dataset is small, and also
16 | lets you introspect them if you want to write a "query planner". For reference,
17 | make sure to read over [RFC2254](http://www.ietf.org/rfc/rfc2254.txt), as this
18 | explains the LDAPv3 text filter representation.
19 |
20 | ldapjs gives you a distinct object type mapping to each filter that is
21 | context-sensitive. However, _all_ filters have a `matches()` method on them, if
22 | that's all you need. Most filters will have an `attribute` property on them,
23 | since "simple" filters all operate on an attribute/value assertion. The
24 | "complex" filters are really aggregations of other filters (i.e. 'and'), and so
25 | these don't provide that property.
26 |
27 | All Filters in the ldapjs framework extend from `Filter`, which wil have the
28 | property `type` available; this will return a string name for the filter, and
29 | will be one of:
30 |
31 | # parseFilter(filterString)
32 |
33 | Parses an [RFC2254](http://www.ietf.org/rfc/rfc2254.txt) filter string into an
34 | ldapjs object(s). If the filter is "complex", it will be a "tree" of objects.
35 | For example:
36 |
37 | ```js
38 | const parseFilter = require('ldapjs').parseFilter;
39 |
40 | const f = parseFilter('(objectclass=*)');
41 | ```
42 |
43 | Is a "simple" filter, and would just return a `PresenceFilter` object. However,
44 |
45 | ```js
46 | const f = parseFilter('(&(employeeType=manager)(l=Seattle))');
47 | ```
48 |
49 | Would return an `AndFilter`, which would have a `filters` array of two
50 | `EqualityFilter` objects.
51 |
52 | `parseFilter` will throw if an invalid string is passed in (that is, a
53 | syntactically invalid string).
54 |
55 | # EqualityFilter
56 |
57 | The equality filter is used to check exact matching of attribute/value
58 | assertions. This object will have an `attribute` and `value` property, and the
59 | `name` property will be `equal`.
60 |
61 | The string syntax for an equality filter is `(attr=value)`.
62 |
63 | The `matches()` method will return true IFF the passed in object has a
64 | key matching `attribute` and a value matching `value`.
65 |
66 | ```js
67 | const f = new EqualityFilter({
68 | attribute: 'cn',
69 | value: 'foo'
70 | });
71 |
72 | f.matches({cn: 'foo'}); => true
73 | f.matches({cn: 'bar'}); => false
74 | ```
75 |
76 | Equality matching uses "strict" type JavaScript comparison, and by default
77 | everything in ldapjs (and LDAP) is a UTF-8 string. If you want comparison
78 | of numbers, or something else, you'll need to use a middleware interceptor
79 | that transforms values of objects.
80 |
81 | # PresenceFilter
82 |
83 | The presence filter is used to check if an object has an attribute at all, with
84 | any value. This object will have an `attribute` property, and the `name`
85 | property will be `present`.
86 |
87 | The string syntax for a presence filter is `(attr=*)`.
88 |
89 | The `matches()` method will return true IFF the passed in object has a
90 | key matching `attribute`.
91 |
92 | ```js
93 | const f = new PresenceFilter({
94 | attribute: 'cn'
95 | });
96 |
97 | f.matches({cn: 'foo'}); => true
98 | f.matches({sn: 'foo'}); => false
99 | ```
100 |
101 | # SubstringFilter
102 |
103 | The substring filter is used to do wildcard matching of a string value. This
104 | object will have an `attribute` property and then it will have an `initial`
105 | property, which is the prefix match, an `any` which will be an array of strings
106 | that are to be found _somewhere_ in the target string, and a `final` property,
107 | which will be the suffix match of the string. `any` and `final` are both
108 | optional. The `name` property will be `substring`.
109 |
110 | The string syntax for a presence filter is `(attr=foo*bar*cat*dog)`, which would
111 | map to:
112 |
113 | ```js
114 | {
115 | initial: 'foo',
116 | any: ['bar', 'cat'],
117 | final: 'dog'
118 | }
119 | ```
120 |
121 | The `matches()` method will return true IFF the passed in object has a
122 | key matching `attribute` and the "regex" matches the value
123 |
124 | ```js
125 | const f = new SubstringFilter({
126 | attribute: 'cn',
127 | initial: 'foo',
128 | any: ['bar'],
129 | final: 'baz'
130 | });
131 |
132 | f.matches({cn: 'foobigbardogbaz'}); => true
133 | f.matches({sn: 'fobigbardogbaz'}); => false
134 | ```
135 |
136 | # GreaterThanEqualsFilter
137 |
138 | The ge filter is used to do comparisons and ordering based on the value type. As
139 | mentioned elsewhere, by default everything in LDAP and ldapjs is a string, so
140 | this filter's `matches()` would be using lexicographical ordering of strings.
141 | If you wanted `>=` semantics over numeric values, you would need to add some
142 | middleware to convert values before comparison (and the value of the filter).
143 | Note that the ldapjs schema middleware will do this.
144 |
145 | The GreaterThanEqualsFilter will have an `attribute` property, a `value`
146 | property and the `name` property will be `ge`.
147 |
148 | The string syntax for a ge filter is:
149 |
150 | ```
151 | (cn>=foo)
152 | ```
153 |
154 | The `matches()` method will return true IFF the passed in object has a
155 | key matching `attribute` and the value is `>=` this filter's `value`.
156 |
157 | ```js
158 | const f = new GreaterThanEqualsFilter({
159 | attribute: 'cn',
160 | value: 'foo',
161 | });
162 |
163 | f.matches({cn: 'foobar'}); => true
164 | f.matches({cn: 'abc'}); => false
165 | ```
166 |
167 | # LessThanEqualsFilter
168 |
169 | The le filter is used to do comparisons and ordering based on the value type. As
170 | mentioned elsewhere, by default everything in LDAP and ldapjs is a string, so
171 | this filter's `matches()` would be using lexicographical ordering of strings.
172 | If you wanted `<=` semantics over numeric values, you would need to add some
173 | middleware to convert values before comparison (and the value of the filter).
174 | Note that the ldapjs schema middleware will do this.
175 |
176 | The string syntax for a le filter is:
177 |
178 | ```
179 | (cn<=foo)
180 | ```
181 |
182 | The LessThanEqualsFilter will have an `attribute` property, a `value`
183 | property and the `name` property will be `le`.
184 |
185 | The `matches()` method will return true IFF the passed in object has a
186 | key matching `attribute` and the value is `<=` this filter's `value`.
187 |
188 | ```js
189 | const f = new LessThanEqualsFilter({
190 | attribute: 'cn',
191 | value: 'foo',
192 | });
193 |
194 | f.matches({cn: 'abc'}); => true
195 | f.matches({cn: 'foobar'}); => false
196 | ```
197 |
198 | # AndFilter
199 |
200 | The and filter is a complex filter that simply contains "child" filters. The
201 | object will have a `filters` property which is an array of `Filter` objects. The
202 | `name` property will be `and`.
203 |
204 | The string syntax for an and filter is (assuming below we're and'ing two
205 | equality filters):
206 |
207 | ```
208 | (&(cn=foo)(sn=bar))
209 | ```
210 |
211 | The `matches()` method will return true IFF the passed in object matches all
212 | the filters in the `filters` array.
213 |
214 | ```js
215 | const f = new AndFilter({
216 | filters: [
217 | new EqualityFilter({
218 | attribute: 'cn',
219 | value: 'foo'
220 | }),
221 | new EqualityFilter({
222 | attribute: 'sn',
223 | value: 'bar'
224 | })
225 | ]
226 | });
227 |
228 | f.matches({cn: 'foo', sn: 'bar'}); => true
229 | f.matches({cn: 'foo', sn: 'baz'}); => false
230 | ```
231 |
232 | # OrFilter
233 |
234 | The or filter is a complex filter that simply contains "child" filters. The
235 | object will have a `filters` property which is an array of `Filter` objects. The
236 | `name` property will be `or`.
237 |
238 | The string syntax for an or filter is (assuming below we're or'ing two
239 | equality filters):
240 |
241 | ```
242 | (|(cn=foo)(sn=bar))
243 | ```
244 |
245 | The `matches()` method will return true IFF the passed in object matches *any*
246 | of the filters in the `filters` array.
247 |
248 | ```js
249 | const f = new OrFilter({
250 | filters: [
251 | new EqualityFilter({
252 | attribute: 'cn',
253 | value: 'foo'
254 | }),
255 | new EqualityFilter({
256 | attribute: 'sn',
257 | value: 'bar'
258 | })
259 | ]
260 | });
261 |
262 | f.matches({cn: 'foo', sn: 'baz'}); => true
263 | f.matches({cn: 'bar', sn: 'baz'}); => false
264 | ```
265 |
266 | # NotFilter
267 |
268 | The not filter is a complex filter that contains a single "child" filter. The
269 | object will have a `filter` property which is an instance of a `Filter` object.
270 | The `name` property will be `not`.
271 |
272 | The string syntax for a not filter is (assuming below we're not'ing an
273 | equality filter):
274 |
275 | ```
276 | (!(cn=foo))
277 | ```
278 |
279 | The `matches()` method will return true IFF the passed in object does not match
280 | the filter in the `filter` property.
281 |
282 | ```js
283 | const f = new NotFilter({
284 | filter: new EqualityFilter({
285 | attribute: 'cn',
286 | value: 'foo'
287 | })
288 | });
289 |
290 | f.matches({cn: 'bar'}); => true
291 | f.matches({cn: 'foo'}); => false
292 | ```
293 |
294 | # ApproximateFilter
295 |
296 | The approximate filter is used to check "approximate" matching of
297 | attribute/value assertions. This object will have an `attribute` and
298 | `value` property, and the `name` property will be `approx`.
299 |
300 | As a side point, this is a useless filter. It's really only here if you have
301 | some whacky client that's sending this. It just does an exact match (which
302 | is what ActiveDirectory does too).
303 |
304 | The string syntax for an equality filter is `(attr~=value)`.
305 |
306 | The `matches()` method will return true IFF the passed in object has a
307 | key matching `attribute` and a value exactly matching `value`.
308 |
309 | ```js
310 | const f = new ApproximateFilter({
311 | attribute: 'cn',
312 | value: 'foo'
313 | });
314 |
315 | f.matches({cn: 'foo'}); => true
316 | f.matches({cn: 'bar'}); => false
317 | ```
318 |
--------------------------------------------------------------------------------
/docs/index.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: ldapjs
3 | ---
4 |
5 |
12 |
13 | ldapjs is a pure JavaScript, from-scratch framework for implementing
14 | [LDAP](http://tools.ietf.org/html/rfc4510) clients and servers in
15 | [Node.js](http://nodejs.org). It is intended for developers used to interacting
16 | with HTTP services in node and [restify](http://restify.com).
17 |
18 |
19 |
20 | ```js
21 | const ldap = require('ldapjs');
22 |
23 | const server = ldap.createServer();
24 |
25 | server.search('o=example', (req, res, next) => {
26 | const obj = {
27 | dn: req.dn.toString(),
28 | attributes: {
29 | objectclass: ['organization', 'top'],
30 | o: 'example'
31 | }
32 | };
33 |
34 | if (req.filter.matches(obj.attributes))
35 | res.send(obj);
36 |
37 | res.end();
38 | });
39 |
40 | server.listen(1389, () => {
41 | console.log('LDAP server listening at %s', server.url);
42 | });
43 | ```
44 |
45 | Try hitting that with:
46 |
47 | ```shell
48 | $ ldapsearch -H ldap://localhost:1389 -x -b o=example objectclass=*
49 | ```
50 |
51 | # Features
52 |
53 | ldapjs implements most of the common operations in the LDAP v3 RFC(s), for
54 | both client and server. It is 100% wire-compatible with the LDAP protocol
55 | itself, and is interoperable with [OpenLDAP](http://openldap.org) and any other
56 | LDAPv3-compliant implementation. ldapjs gives you a powerful routing and
57 | "intercepting filter" pattern for implementing server(s). It is intended
58 | that you can build LDAP over anything you want, not just traditional databases.
59 |
60 | # Getting started
61 |
62 | ```shell
63 | $ npm install ldapjs
64 | ```
65 |
66 | If you're new to LDAP, check out the [guide](guide.html). Otherwise, the
67 | API documentation is:
68 |
69 |
70 | |Section |Content |
71 | |---------------------------|-------------------------------------------|
72 | |[Server API](server.html) |Reference for implementing LDAP servers. |
73 | |[Client API](client.html) |Reference for implementing LDAP clients. |
74 | |[DN API](dn.html) |API reference for the DN class. |
75 | |[Filter API](filters.html) |API reference for LDAP search filters. |
76 | |[Error API](errors.html) |Listing of all ldapjs Error objects. |
77 | |[Examples](examples.html) |Collection of sample/getting started code. |
78 |
79 | # More information
80 |
81 | - License:[MIT](http://opensource.org/licenses/mit-license.php)
82 | - Code: [ldapjs/node-ldapjs](https://github.com/ldapjs/node-ldapjs)
83 |
84 | # What's not in the box?
85 |
86 | Since most developers and system(s) adminstrators struggle with some of the
87 | esoteric features of LDAP, not all features in LDAP are implemented here.
88 | Specifically:
89 |
90 | * LDIF
91 | * Aliases
92 | * Attributes by OID
93 | * Extensible matching
94 |
95 | There are a few others, but those are the "big" ones.
96 |
--------------------------------------------------------------------------------
/dt.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ldapjs/node-ldapjs/8ffd0bc9c149088a10ec4c1ec6a18450f76ad05d/dt.png
--------------------------------------------------------------------------------
/examples/cluster-threading-net-server.js:
--------------------------------------------------------------------------------
1 | const cluster = require('cluster')
2 | const ldap = require('ldapjs')
3 | const net = require('net')
4 | const os = require('os')
5 |
6 | const threads = []
7 | threads.getNext = function () {
8 | return (Math.floor(Math.random() * this.length))
9 | }
10 |
11 | const serverOptions = {
12 | port: 1389
13 | }
14 |
15 | if (cluster.isMaster) {
16 | const server = net.createServer(serverOptions, (socket) => {
17 | socket.pause()
18 | console.log('ldapjs client requesting connection')
19 | const routeTo = threads.getNext()
20 | threads[routeTo].send({ type: 'connection' }, socket)
21 | })
22 |
23 | for (let i = 0; i < os.cpus().length; i++) {
24 | const thread = cluster.fork({
25 | id: i
26 | })
27 | thread.id = i
28 | thread.on('message', function () {
29 |
30 | })
31 | threads.push(thread)
32 | }
33 |
34 | server.listen(serverOptions.port, function () {
35 | console.log('ldapjs listening at ldap://127.0.0.1:' + serverOptions.port)
36 | })
37 | } else {
38 | const server = ldap.createServer(serverOptions)
39 |
40 | const threadId = process.env.id
41 |
42 | process.on('message', (msg, socket) => {
43 | switch (msg.type) {
44 | case 'connection':
45 | server.newConnection(socket)
46 | socket.resume()
47 | console.log('ldapjs client connection accepted on ' + threadId.toString())
48 | }
49 | })
50 |
51 | server.search('dc=example', function (req, res) {
52 | console.log('ldapjs search initiated on ' + threadId.toString())
53 | const obj = {
54 | dn: req.dn.toString(),
55 | attributes: {
56 | objectclass: ['organization', 'top'],
57 | o: 'example'
58 | }
59 | }
60 |
61 | if (req.filter.matches(obj.attributes)) { res.send(obj) }
62 |
63 | res.end()
64 | })
65 | }
66 |
--------------------------------------------------------------------------------
/examples/cluster-threading.js:
--------------------------------------------------------------------------------
1 | const cluster = require('cluster')
2 | const ldap = require('ldapjs')
3 | const os = require('os')
4 |
5 | const threads = []
6 | threads.getNext = function () {
7 | return (Math.floor(Math.random() * this.length))
8 | }
9 |
10 | const serverOptions = {
11 | connectionRouter: (socket) => {
12 | socket.pause()
13 | console.log('ldapjs client requesting connection')
14 | const routeTo = threads.getNext()
15 | threads[routeTo].send({ type: 'connection' }, socket)
16 | }
17 | }
18 |
19 | const server = ldap.createServer(serverOptions)
20 |
21 | if (cluster.isMaster) {
22 | for (let i = 0; i < os.cpus().length; i++) {
23 | const thread = cluster.fork({
24 | id: i
25 | })
26 | thread.id = i
27 | thread.on('message', function () {
28 |
29 | })
30 | threads.push(thread)
31 | }
32 |
33 | server.listen(1389, function () {
34 | console.log('ldapjs listening at ' + server.url)
35 | })
36 | } else {
37 | const threadId = process.env.id
38 | serverOptions.connectionRouter = () => {
39 | console.log('should not be hit')
40 | }
41 |
42 | process.on('message', (msg, socket) => {
43 | switch (msg.type) {
44 | case 'connection':
45 | server.newConnection(socket)
46 | socket.resume()
47 | console.log('ldapjs client connection accepted on ' + threadId.toString())
48 | }
49 | })
50 |
51 | server.search('dc=example', function (req, res) {
52 | console.log('ldapjs search initiated on ' + threadId.toString())
53 | const obj = {
54 | dn: req.dn.toString(),
55 | attributes: {
56 | objectclass: ['organization', 'top'],
57 | o: 'example'
58 | }
59 | }
60 |
61 | if (req.filter.matches(obj.attributes)) { res.send(obj) }
62 |
63 | res.end()
64 | })
65 | }
66 |
--------------------------------------------------------------------------------
/examples/inmemory.js:
--------------------------------------------------------------------------------
1 | const ldap = require('../lib/index')
2 |
3 | /// --- Shared handlers
4 |
5 | function authorize (req, res, next) {
6 | /* Any user may search after bind, only cn=root has full power */
7 | const isSearch = (req instanceof ldap.SearchRequest)
8 | if (!req.connection.ldap.bindDN.equals('cn=root') && !isSearch) { return next(new ldap.InsufficientAccessRightsError()) }
9 |
10 | return next()
11 | }
12 |
13 | /// --- Globals
14 |
15 | const SUFFIX = 'o=smartdc'
16 | const db = {}
17 | const server = ldap.createServer()
18 |
19 | server.bind('cn=root', function (req, res, next) {
20 | if (req.dn.toString() !== 'cn=root' || req.credentials !== 'secret') { return next(new ldap.InvalidCredentialsError()) }
21 |
22 | res.end()
23 | return next()
24 | })
25 |
26 | server.add(SUFFIX, authorize, function (req, res, next) {
27 | const dn = req.dn.toString()
28 |
29 | if (db[dn]) { return next(new ldap.EntryAlreadyExistsError(dn)) }
30 |
31 | db[dn] = req.toObject().attributes
32 | res.end()
33 | return next()
34 | })
35 |
36 | server.bind(SUFFIX, function (req, res, next) {
37 | const dn = req.dn.toString()
38 | if (!db[dn]) { return next(new ldap.NoSuchObjectError(dn)) }
39 |
40 | if (!db[dn].userpassword) { return next(new ldap.NoSuchAttributeError('userPassword')) }
41 |
42 | if (db[dn].userpassword.indexOf(req.credentials) === -1) { return next(new ldap.InvalidCredentialsError()) }
43 |
44 | res.end()
45 | return next()
46 | })
47 |
48 | server.compare(SUFFIX, authorize, function (req, res, next) {
49 | const dn = req.dn.toString()
50 | if (!db[dn]) { return next(new ldap.NoSuchObjectError(dn)) }
51 |
52 | if (!db[dn][req.attribute]) { return next(new ldap.NoSuchAttributeError(req.attribute)) }
53 |
54 | let matches = false
55 | const vals = db[dn][req.attribute]
56 | for (let i = 0; i < vals.length; i++) {
57 | if (vals[i] === req.value) {
58 | matches = true
59 | break
60 | }
61 | }
62 |
63 | res.end(matches)
64 | return next()
65 | })
66 |
67 | server.del(SUFFIX, authorize, function (req, res, next) {
68 | const dn = req.dn.toString()
69 | if (!db[dn]) { return next(new ldap.NoSuchObjectError(dn)) }
70 |
71 | delete db[dn]
72 |
73 | res.end()
74 | return next()
75 | })
76 |
77 | server.modify(SUFFIX, authorize, function (req, res, next) {
78 | const dn = req.dn.toString()
79 | if (!req.changes.length) { return next(new ldap.ProtocolError('changes required')) }
80 | if (!db[dn]) { return next(new ldap.NoSuchObjectError(dn)) }
81 |
82 | const entry = db[dn]
83 |
84 | let mod
85 | for (let i = 0; i < req.changes.length; i++) {
86 | mod = req.changes[i].modification
87 | switch (req.changes[i].operation) {
88 | case 'replace':
89 | if (!entry[mod.type]) { return next(new ldap.NoSuchAttributeError(mod.type)) }
90 |
91 | if (!mod.vals || !mod.vals.length) {
92 | delete entry[mod.type]
93 | } else {
94 | entry[mod.type] = mod.vals
95 | }
96 |
97 | break
98 |
99 | case 'add':
100 | if (!entry[mod.type]) {
101 | entry[mod.type] = mod.vals
102 | } else {
103 | mod.vals.forEach(function (v) {
104 | if (entry[mod.type].indexOf(v) === -1) { entry[mod.type].push(v) }
105 | })
106 | }
107 |
108 | break
109 |
110 | case 'delete':
111 | if (!entry[mod.type]) { return next(new ldap.NoSuchAttributeError(mod.type)) }
112 |
113 | delete entry[mod.type]
114 |
115 | break
116 | }
117 | }
118 |
119 | res.end()
120 | return next()
121 | })
122 |
123 | server.search(SUFFIX, authorize, function (req, res, next) {
124 | const dn = req.dn.toString()
125 | if (!db[dn]) { return next(new ldap.NoSuchObjectError(dn)) }
126 |
127 | let scopeCheck
128 |
129 | switch (req.scope) {
130 | case 'base':
131 | if (req.filter.matches(db[dn])) {
132 | res.send({
133 | dn,
134 | attributes: db[dn]
135 | })
136 | }
137 |
138 | res.end()
139 | return next()
140 |
141 | case 'one':
142 | scopeCheck = function (k) {
143 | if (req.dn.equals(k)) { return true }
144 |
145 | const parent = ldap.parseDN(k).parent()
146 | return (parent ? parent.equals(req.dn) : false)
147 | }
148 | break
149 |
150 | case 'sub':
151 | scopeCheck = function (k) {
152 | return (req.dn.equals(k) || req.dn.parentOf(k))
153 | }
154 |
155 | break
156 | }
157 |
158 | Object.keys(db).forEach(function (key) {
159 | if (!scopeCheck(key)) { return }
160 |
161 | if (req.filter.matches(db[key])) {
162 | res.send({
163 | dn: key,
164 | attributes: db[key]
165 | })
166 | }
167 | })
168 |
169 | res.end()
170 | return next()
171 | })
172 |
173 | /// --- Fire it up
174 |
175 | server.listen(1389, function () {
176 | console.log('LDAP server up at: %s', server.url)
177 | })
178 |
--------------------------------------------------------------------------------
/lib/client/constants.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 |
3 | module.exports = {
4 | // https://tools.ietf.org/html/rfc4511#section-4.1.1
5 | // Message identifiers are an integer between (0, maxint).
6 | MAX_MSGID: Math.pow(2, 31) - 1
7 | }
8 |
--------------------------------------------------------------------------------
/lib/client/index.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 |
3 | const logger = require('../logger')
4 | const Client = require('./client')
5 |
6 | module.exports = {
7 | Client,
8 | createClient: function createClient (options) {
9 | if (isObject(options) === false) throw TypeError('options (object) required')
10 | if (options.url && typeof options.url !== 'string' && !Array.isArray(options.url)) throw TypeError('options.url (string|array) required')
11 | if (options.socketPath && typeof options.socketPath !== 'string') throw TypeError('options.socketPath must be a string')
12 | if ((options.url && options.socketPath) || !(options.url || options.socketPath)) throw TypeError('options.url ^ options.socketPath (String) required')
13 | if (!options.log) options.log = logger
14 | if (isObject(options.log) !== true) throw TypeError('options.log must be an object')
15 | if (!options.log.child) options.log.child = function () { return options.log }
16 |
17 | return new Client(options)
18 | }
19 | }
20 |
21 | function isObject (input) {
22 | return Object.prototype.toString.apply(input) === '[object Object]'
23 | }
24 |
--------------------------------------------------------------------------------
/lib/client/message-tracker/ge-window.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 |
3 | const { MAX_MSGID } = require('../constants')
4 |
5 | /**
6 | * Compare a reference id with another id to determine "greater than or equal"
7 | * between the two values according to a sliding window.
8 | *
9 | * @param {integer} ref
10 | * @param {integer} comp
11 | *
12 | * @returns {boolean} `true` if the `comp` value is >= to the `ref` value
13 | * within the computed window, otherwise `false`.
14 | */
15 | module.exports = function geWindow (ref, comp) {
16 | let max = ref + Math.floor(MAX_MSGID / 2)
17 | const min = ref
18 | if (max >= MAX_MSGID) {
19 | // Handle roll-over
20 | max = max - MAX_MSGID - 1
21 | return ((comp <= max) || (comp >= min))
22 | } else {
23 | return ((comp <= max) && (comp >= min))
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/lib/client/message-tracker/id-generator.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 |
3 | const { MAX_MSGID } = require('../constants')
4 |
5 | /**
6 | * Returns a function that generates message identifiers. According to RFC 4511
7 | * the identifers should be `(0, MAX_MSGID)`. The returned function handles
8 | * this and wraps around when the maximum has been reached.
9 | *
10 | * @param {integer} [start=0] Starting number in the identifier sequence.
11 | *
12 | * @returns {function} This function accepts no parameters and returns an
13 | * increasing sequence identifier each invocation until it reaches the maximum
14 | * identifier. At this point the sequence starts over.
15 | */
16 | module.exports = function idGeneratorFactory (start = 0) {
17 | let currentID = start
18 | return function nextID () {
19 | const id = currentID + 1
20 | currentID = (id >= MAX_MSGID) ? 1 : id
21 | return currentID
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/lib/client/message-tracker/index.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 |
3 | const idGeneratorFactory = require('./id-generator')
4 | const purgeAbandoned = require('./purge-abandoned')
5 |
6 | /**
7 | * Returns a message tracker object that keeps track of which message
8 | * identifiers correspond to which message handlers. Also handles keeping track
9 | * of abandoned messages.
10 | *
11 | * @param {object} options
12 | * @param {string} options.id An identifier for the tracker.
13 | * @param {object} options.parser An object that will be used to parse messages.
14 | *
15 | * @returns {MessageTracker}
16 | */
17 | module.exports = function messageTrackerFactory (options) {
18 | if (Object.prototype.toString.call(options) !== '[object Object]') {
19 | throw Error('options object is required')
20 | }
21 | if (!options.id || typeof options.id !== 'string') {
22 | throw Error('options.id string is required')
23 | }
24 | if (!options.parser || Object.prototype.toString.call(options.parser) !== '[object Object]') {
25 | throw Error('options.parser object is required')
26 | }
27 |
28 | let currentID = 0
29 | const nextID = idGeneratorFactory()
30 | const messages = new Map()
31 | const abandoned = new Map()
32 |
33 | /**
34 | * @typedef {object} MessageTracker
35 | * @property {string} id The identifier of the tracker as supplied via the options.
36 | * @property {object} parser The parser object given by the the options.
37 | */
38 | const tracker = {
39 | id: options.id,
40 | parser: options.parser
41 | }
42 |
43 | /**
44 | * Count of messages awaiting response.
45 | *
46 | * @alias pending
47 | * @memberof! MessageTracker#
48 | */
49 | Object.defineProperty(tracker, 'pending', {
50 | get () {
51 | return messages.size
52 | }
53 | })
54 |
55 | /**
56 | * Move a specific message to the abanded track.
57 | *
58 | * @param {integer} msgID The identifier for the message to move.
59 | *
60 | * @memberof MessageTracker
61 | * @method abandon
62 | */
63 | tracker.abandon = function abandonMessage (msgID) {
64 | if (messages.has(msgID) === false) return false
65 | const toAbandon = messages.get(msgID)
66 | abandoned.set(msgID, {
67 | age: currentID,
68 | message: toAbandon.message,
69 | cb: toAbandon.callback
70 | })
71 | return messages.delete(msgID)
72 | }
73 |
74 | /**
75 | * @typedef {object} Tracked
76 | * @property {object} message The tracked message. Usually the outgoing
77 | * request object.
78 | * @property {Function} callback The handler to use when receiving a
79 | * response to the tracked message.
80 | */
81 |
82 | /**
83 | * Retrieves the message handler for a message. Removes abandoned messages
84 | * that have been given time to be resolved.
85 | *
86 | * @param {integer} msgID The identifier for the message to get the handler for.
87 | *
88 | * @memberof MessageTracker
89 | * @method fetch
90 | */
91 | tracker.fetch = function fetchMessage (msgID) {
92 | const tracked = messages.get(msgID)
93 | if (tracked) {
94 | purgeAbandoned(msgID, abandoned)
95 | return tracked
96 | }
97 |
98 | // We sent an abandon request but the server either wasn't able to process
99 | // it or has not received it yet. Therefore, we received a response for the
100 | // abandoned message. So we must return the abandoned message's callback
101 | // to be processed normally.
102 | const abandonedMsg = abandoned.get(msgID)
103 | if (abandonedMsg) {
104 | return { message: abandonedMsg, callback: abandonedMsg.cb }
105 | }
106 |
107 | return null
108 | }
109 |
110 | /**
111 | * Removes all message tracks, cleans up the abandoned track, and invokes
112 | * a callback for each message purged.
113 | *
114 | * @param {function} cb A function with the signature `(msgID, handler)`.
115 | *
116 | * @memberof MessageTracker
117 | * @method purge
118 | */
119 | tracker.purge = function purgeMessages (cb) {
120 | messages.forEach((val, key) => {
121 | purgeAbandoned(key, abandoned)
122 | tracker.remove(key)
123 | cb(key, val.callback)
124 | })
125 | }
126 |
127 | /**
128 | * Removes a message from all tracking.
129 | *
130 | * @param {integer} msgID The identifier for the message to remove from tracking.
131 | *
132 | * @memberof MessageTracker
133 | * @method remove
134 | */
135 | tracker.remove = function removeMessage (msgID) {
136 | if (messages.delete(msgID) === false) {
137 | abandoned.delete(msgID)
138 | }
139 | }
140 |
141 | /**
142 | * Add a message handler to be tracked.
143 | *
144 | * @param {object} message The message object to be tracked. This object will
145 | * have a new property added to it: `messageId`.
146 | * @param {function} callback The handler for the message.
147 | *
148 | * @memberof MessageTracker
149 | * @method track
150 | */
151 | tracker.track = function trackMessage (message, callback) {
152 | currentID = nextID()
153 | // This side effect is not ideal but the client doesn't attach the tracker
154 | // to itself until after the `.connect` method has fired. If this can be
155 | // refactored later, then we can possibly get rid of this side effect.
156 | message.messageId = currentID
157 | messages.set(currentID, { callback, message })
158 | }
159 |
160 | return tracker
161 | }
162 |
--------------------------------------------------------------------------------
/lib/client/message-tracker/purge-abandoned.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 |
3 | const { AbandonedError } = require('../../errors')
4 | const geWindow = require('./ge-window')
5 |
6 | /**
7 | * Given a `msgID` and a set of `abandoned` messages, remove any abandoned
8 | * messages that existed _prior_ to the specified `msgID`. For example, let's
9 | * assume the server has sent 3 messages:
10 | *
11 | * 1. A search message.
12 | * 2. An abandon message for the search message.
13 | * 3. A new search message.
14 | *
15 | * When the response for message #1 comes in, if it does, it will be processed
16 | * normally due to the specification. Message #2 will not receive a response, or
17 | * if the server does send one since the spec sort of allows it, we won't do
18 | * anything with it because we just discard that listener. Now the response
19 | * for message #3 comes in. At this point, we will issue a purge of responses
20 | * by passing in `msgID = 3`. This result is that we will remove the tracking
21 | * for message #1.
22 | *
23 | * @param {integer} msgID An upper bound for the messages to be purged.
24 | * @param {Map} abandoned A set of abandoned messages. Each message is an object
25 | * `{ age: , cb: }` where `age` was the current message id when the
26 | * abandon message was sent.
27 | */
28 | module.exports = function purgeAbandoned (msgID, abandoned) {
29 | abandoned.forEach((val, key) => {
30 | if (geWindow(val.age, msgID) === false) return
31 | val.cb(new AbandonedError('client request abandoned'))
32 | abandoned.delete(key)
33 | })
34 | }
35 |
--------------------------------------------------------------------------------
/lib/client/request-queue/enqueue.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 |
3 | /**
4 | * Adds requests to the queue. If a timeout has been added to the queue then
5 | * this will freeze the queue with the newly added item, flush it, and then
6 | * unfreeze it when the queue has been cleared.
7 | *
8 | * @param {object} message An LDAP message object.
9 | * @param {object} expect An expectation object.
10 | * @param {object} emitter An event emitter or `null`.
11 | * @param {function} cb A callback to invoke when the request is finished.
12 | *
13 | * @returns {boolean} `true` if the requested was queued. `false` if the queue
14 | * is not accepting any requests.
15 | */
16 | module.exports = function enqueue (message, expect, emitter, cb) {
17 | if (this._queue.size >= this.size || this._frozen) {
18 | return false
19 | }
20 |
21 | this._queue.add({ message, expect, emitter, cb })
22 |
23 | if (this.timeout === 0) return true
24 | if (this._timer === null) return true
25 |
26 | // A queue can have a specified time allotted for it to be cleared. If that
27 | // time has been reached, reject new entries until the queue has been cleared.
28 | this._timer = setTimeout(queueTimeout.bind(this), this.timeout)
29 |
30 | return true
31 |
32 | function queueTimeout () {
33 | this.freeze()
34 | this.purge()
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/lib/client/request-queue/flush.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 |
3 | /**
4 | * Invokes all requests in the queue by passing them to the supplied callback
5 | * function and then clears all items from the queue.
6 | *
7 | * @param {function} cb A function used to handle the requests.
8 | */
9 | module.exports = function flush (cb) {
10 | if (this._timer) {
11 | clearTimeout(this._timer)
12 | this._timer = null
13 | }
14 |
15 | // We must get a local copy of the queue and clear it before iterating it.
16 | // The client will invoke this flush function _many_ times. If we try to
17 | // iterate it without a local copy and clearing first then we will overflow
18 | // the stack.
19 | const requests = Array.from(this._queue.values())
20 | this._queue.clear()
21 | for (const req of requests) {
22 | cb(req.message, req.expect, req.emitter, req.cb)
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/lib/client/request-queue/index.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 |
3 | const enqueue = require('./enqueue')
4 | const flush = require('./flush')
5 | const purge = require('./purge')
6 |
7 | /**
8 | * Builds a request queue object and returns it.
9 | *
10 | * @param {object} [options]
11 | * @param {integer} [options.size] Maximum size of the request queue. Must be
12 | * a number greater than `0` if supplied. Default: `Infinity`.
13 | * @param {integer} [options.timeout] Time in milliseconds a queue has to
14 | * complete the requests it contains.
15 | *
16 | * @returns {object} A queue instance.
17 | */
18 | module.exports = function requestQueueFactory (options) {
19 | const opts = Object.assign({}, options)
20 | const q = {
21 | size: (opts.size > 0) ? opts.size : Infinity,
22 | timeout: (opts.timeout > 0) ? opts.timeout : 0,
23 | _queue: new Set(),
24 | _timer: null,
25 | _frozen: false
26 | }
27 |
28 | q.enqueue = enqueue.bind(q)
29 | q.flush = flush.bind(q)
30 | q.purge = purge.bind(q)
31 | q.freeze = function freeze () {
32 | this._frozen = true
33 | }
34 | q.thaw = function thaw () {
35 | this._frozen = false
36 | }
37 |
38 | return q
39 | }
40 |
--------------------------------------------------------------------------------
/lib/client/request-queue/purge.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 |
3 | const { TimeoutError } = require('../../errors')
4 |
5 | /**
6 | * Flushes the queue by rejecting all pending requests with a timeout error.
7 | */
8 | module.exports = function purge () {
9 | this.flush(function flushCB (a, b, c, cb) {
10 | cb(new TimeoutError('request queue timeout'))
11 | })
12 | }
13 |
--------------------------------------------------------------------------------
/lib/client/search_pager.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 |
3 | const EventEmitter = require('events').EventEmitter
4 | const util = require('util')
5 | const assert = require('assert-plus')
6 | const { PagedResultsControl } = require('@ldapjs/controls')
7 | const CorkedEmitter = require('../corked_emitter.js')
8 |
9 | /// --- API
10 |
11 | /**
12 | * Handler object for paged search operations.
13 | *
14 | * Provided to consumers in place of the normal search EventEmitter it adds the
15 | * following new events:
16 | * 1. page - Emitted whenever the end of a result page is encountered.
17 | * If this is the last page, 'end' will also be emitted.
18 | * The event passes two arguments:
19 | * 1. The result object (similar to 'end')
20 | * 2. A callback function optionally used to continue the search
21 | * operation if the pagePause option was specified during
22 | * initialization.
23 | * 2. pageError - Emitted if the server does not support paged search results
24 | * If there are no listeners for this event, the 'error' event
25 | * will be emitted (and 'end' will not be). By listening to
26 | * 'pageError', a successful search that lacks paging will be
27 | * able to emit 'end'.
28 | */
29 | function SearchPager (opts) {
30 | assert.object(opts)
31 | assert.func(opts.callback)
32 | assert.number(opts.pageSize)
33 | assert.func(opts.sendRequest)
34 |
35 | CorkedEmitter.call(this, {})
36 |
37 | this.callback = opts.callback
38 | this.controls = opts.controls
39 | this.pageSize = opts.pageSize
40 | this.pagePause = opts.pagePause
41 | this.sendRequest = opts.sendRequest
42 |
43 | this.controls.forEach(function (control) {
44 | if (control.type === PagedResultsControl.OID) {
45 | // The point of using SearchPager is not having to do this.
46 | // Toss an error if the pagedResultsControl is present
47 | throw new Error('redundant pagedResultControl')
48 | }
49 | })
50 |
51 | this.finished = false
52 | this.started = false
53 |
54 | const emitter = new EventEmitter()
55 | emitter.on('searchRequest', this.emit.bind(this, 'searchRequest'))
56 | emitter.on('searchEntry', this.emit.bind(this, 'searchEntry'))
57 | emitter.on('end', this._onEnd.bind(this))
58 | emitter.on('error', this._onError.bind(this))
59 | this.childEmitter = emitter
60 | }
61 | util.inherits(SearchPager, CorkedEmitter)
62 | module.exports = SearchPager
63 |
64 | /**
65 | * Start the paged search.
66 | */
67 | SearchPager.prototype.begin = function begin () {
68 | // Starting first page
69 | this._nextPage(null)
70 | }
71 |
72 | SearchPager.prototype._onEnd = function _onEnd (res) {
73 | const self = this
74 | let cookie = null
75 | res.controls.forEach(function (control) {
76 | if (control.type === PagedResultsControl.OID) {
77 | cookie = control.value.cookie
78 | }
79 | })
80 | // Pass a noop callback by default for page events
81 | const nullCb = function () { }
82 |
83 | if (cookie === null) {
84 | // paged search not supported
85 | this.finished = true
86 | this.emit('page', res, nullCb)
87 | const err = new Error('missing paged control')
88 | err.name = 'PagedError'
89 | if (this.listeners('pageError').length > 0) {
90 | this.emit('pageError', err)
91 | // If the consumer as subscribed to pageError, SearchPager is absolved
92 | // from delivering the fault via the 'error' event. Emitting an 'end'
93 | // event after 'error' breaks the contract that the standard client
94 | // provides, so it's only a possibility if 'pageError' is used instead.
95 | this.emit('end', res)
96 | } else {
97 | this.emit('error', err)
98 | // No end event possible per explanation above.
99 | }
100 | return
101 | }
102 |
103 | if (cookie.length === 0) {
104 | // end of paged results
105 | this.finished = true
106 | this.emit('page', nullCb)
107 | this.emit('end', res)
108 | } else {
109 | if (this.pagePause) {
110 | // Wait to fetch next page until callback is invoked
111 | // Halt page fetching if called with error
112 | this.emit('page', res, function (err) {
113 | if (!err) {
114 | self._nextPage(cookie)
115 | } else {
116 | // the paged search has been canceled so emit an end
117 | self.emit('end', res)
118 | }
119 | })
120 | } else {
121 | this.emit('page', res, nullCb)
122 | this._nextPage(cookie)
123 | }
124 | }
125 | }
126 |
127 | SearchPager.prototype._onError = function _onError (err) {
128 | this.finished = true
129 | this.emit('error', err)
130 | }
131 |
132 | /**
133 | * Initiate a search for the next page using the returned cookie value.
134 | */
135 | SearchPager.prototype._nextPage = function _nextPage (cookie) {
136 | const controls = this.controls.slice(0)
137 | controls.push(new PagedResultsControl({
138 | value: {
139 | size: this.pageSize,
140 | cookie
141 | }
142 | }))
143 |
144 | this.sendRequest(controls, this.childEmitter, this._sendCallback.bind(this))
145 | }
146 |
147 | /**
148 | * Callback provided to the client API for successful transmission.
149 | */
150 | SearchPager.prototype._sendCallback = function _sendCallback (err) {
151 | if (err) {
152 | this.finished = true
153 | if (!this.started) {
154 | // EmitSend error during the first page, bail via callback
155 | this.callback(err, null)
156 | } else {
157 | this.emit('error', err)
158 | }
159 | } else {
160 | // search successfully send
161 | if (!this.started) {
162 | this.started = true
163 | // send self as emitter as the client would
164 | this.callback(null, this)
165 | }
166 | }
167 | }
168 |
--------------------------------------------------------------------------------
/lib/controls/index.js:
--------------------------------------------------------------------------------
1 | // Copyright 2011 Mark Cavage, Inc. All rights reserved.
2 |
3 | const controls = require('@ldapjs/controls')
4 | module.exports = controls
5 |
--------------------------------------------------------------------------------
/lib/corked_emitter.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 |
3 | const EventEmitter = require('events').EventEmitter
4 |
5 | /**
6 | * A CorkedEmitter is a variant of an EventEmitter where events emitted
7 | * wait for the appearance of the first listener of any kind. That is,
8 | * a CorkedEmitter will store all .emit()s it receives, to be replayed
9 | * later when an .on() is applied.
10 | * It is meant for situations where the consumers of the emitter are
11 | * unable to register listeners right away, and cannot afford to miss
12 | * any events emitted from the start.
13 | * Note that, whenever the first emitter (for any event) appears,
14 | * the emitter becomes uncorked and works as usual for ALL events, and
15 | * will not cache anything anymore. This is necessary to avoid
16 | * re-ordering emits - either everything is being buffered, or nothing.
17 | */
18 | function CorkedEmitter () {
19 | const self = this
20 | EventEmitter.call(self)
21 | /**
22 | * An array of arguments objects (array-likes) to emit on open.
23 | */
24 | self._outstandingEmits = []
25 | /**
26 | * Whether the normal flow of emits is restored yet.
27 | */
28 | self._opened = false
29 | // When the first listener appears, we enqueue an opening.
30 | // It is not done immediately, so that other listeners can be
31 | // registered in the same critical section.
32 | self.once('newListener', function () {
33 | setImmediate(function releaseStoredEvents () {
34 | self._opened = true
35 | self._outstandingEmits.forEach(function (args) {
36 | self.emit.apply(self, args)
37 | })
38 | })
39 | })
40 | }
41 | CorkedEmitter.prototype = Object.create(EventEmitter.prototype)
42 | CorkedEmitter.prototype.emit = function emit (eventName) {
43 | if (this._opened || eventName === 'newListener') {
44 | EventEmitter.prototype.emit.apply(this, arguments)
45 | } else {
46 | this._outstandingEmits.push(arguments)
47 | }
48 | }
49 |
50 | module.exports = CorkedEmitter
51 |
--------------------------------------------------------------------------------
/lib/errors/codes.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 |
3 | module.exports = {
4 | LDAP_SUCCESS: 0,
5 | LDAP_OPERATIONS_ERROR: 1,
6 | LDAP_PROTOCOL_ERROR: 2,
7 | LDAP_TIME_LIMIT_EXCEEDED: 3,
8 | LDAP_SIZE_LIMIT_EXCEEDED: 4,
9 | LDAP_COMPARE_FALSE: 5,
10 | LDAP_COMPARE_TRUE: 6,
11 | LDAP_AUTH_METHOD_NOT_SUPPORTED: 7,
12 | LDAP_STRONG_AUTH_REQUIRED: 8,
13 | LDAP_REFERRAL: 10,
14 | LDAP_ADMIN_LIMIT_EXCEEDED: 11,
15 | LDAP_UNAVAILABLE_CRITICAL_EXTENSION: 12,
16 | LDAP_CONFIDENTIALITY_REQUIRED: 13,
17 | LDAP_SASL_BIND_IN_PROGRESS: 14,
18 | LDAP_NO_SUCH_ATTRIBUTE: 16,
19 | LDAP_UNDEFINED_ATTRIBUTE_TYPE: 17,
20 | LDAP_INAPPROPRIATE_MATCHING: 18,
21 | LDAP_CONSTRAINT_VIOLATION: 19,
22 | LDAP_ATTRIBUTE_OR_VALUE_EXISTS: 20,
23 | LDAP_INVALID_ATTRIBUTE_SYNTAX: 21,
24 | LDAP_NO_SUCH_OBJECT: 32,
25 | LDAP_ALIAS_PROBLEM: 33,
26 | LDAP_INVALID_DN_SYNTAX: 34,
27 | LDAP_ALIAS_DEREF_PROBLEM: 36,
28 | LDAP_INAPPROPRIATE_AUTHENTICATION: 48,
29 | LDAP_INVALID_CREDENTIALS: 49,
30 | LDAP_INSUFFICIENT_ACCESS_RIGHTS: 50,
31 | LDAP_BUSY: 51,
32 | LDAP_UNAVAILABLE: 52,
33 | LDAP_UNWILLING_TO_PERFORM: 53,
34 | LDAP_LOOP_DETECT: 54,
35 | LDAP_SORT_CONTROL_MISSING: 60,
36 | LDAP_INDEX_RANGE_ERROR: 61,
37 | LDAP_NAMING_VIOLATION: 64,
38 | LDAP_OBJECTCLASS_VIOLATION: 65,
39 | LDAP_NOT_ALLOWED_ON_NON_LEAF: 66,
40 | LDAP_NOT_ALLOWED_ON_RDN: 67,
41 | LDAP_ENTRY_ALREADY_EXISTS: 68,
42 | LDAP_OBJECTCLASS_MODS_PROHIBITED: 69,
43 | LDAP_AFFECTS_MULTIPLE_DSAS: 71,
44 | LDAP_CONTROL_ERROR: 76,
45 | LDAP_OTHER: 80,
46 | LDAP_PROXIED_AUTHORIZATION_DENIED: 123
47 | }
48 |
--------------------------------------------------------------------------------
/lib/errors/index.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 |
3 | const util = require('util')
4 | const assert = require('assert-plus')
5 |
6 | const LDAPResult = require('../messages').LDAPResult
7 |
8 | /// --- Globals
9 |
10 | const CODES = require('./codes')
11 | const ERRORS = []
12 |
13 | /// --- Error Base class
14 |
15 | function LDAPError (message, dn, caller) {
16 | if (Error.captureStackTrace) { Error.captureStackTrace(this, caller || LDAPError) }
17 |
18 | this.lde_message = message
19 | this.lde_dn = dn
20 | }
21 | util.inherits(LDAPError, Error)
22 | Object.defineProperties(LDAPError.prototype, {
23 | name: {
24 | get: function getName () { return 'LDAPError' },
25 | configurable: false
26 | },
27 | code: {
28 | get: function getCode () { return CODES.LDAP_OTHER },
29 | configurable: false
30 | },
31 | message: {
32 | get: function getMessage () {
33 | return this.lde_message || this.name
34 | },
35 | set: function setMessage (message) {
36 | this.lde_message = message
37 | },
38 | configurable: false
39 | },
40 | dn: {
41 | get: function getDN () {
42 | return (this.lde_dn ? this.lde_dn.toString() : '')
43 | },
44 | configurable: false
45 | }
46 | })
47 |
48 | /// --- Exported API
49 |
50 | module.exports = {}
51 | module.exports.LDAPError = LDAPError
52 |
53 | // Some whacky games here to make sure all the codes are exported
54 | Object.keys(CODES).forEach(function (code) {
55 | module.exports[code] = CODES[code]
56 | if (code === 'LDAP_SUCCESS') { return }
57 |
58 | let err = ''
59 | let msg = ''
60 | const pieces = code.split('_').slice(1)
61 | for (let i = 0; i < pieces.length; i++) {
62 | const lc = pieces[i].toLowerCase()
63 | const key = lc.charAt(0).toUpperCase() + lc.slice(1)
64 | err += key
65 | msg += key + ((i + 1) < pieces.length ? ' ' : '')
66 | }
67 |
68 | if (!/\w+Error$/.test(err)) { err += 'Error' }
69 |
70 | // At this point LDAP_OPERATIONS_ERROR is now OperationsError in $err
71 | // and 'Operations Error' in $msg
72 | module.exports[err] = function (message, dn, caller) {
73 | LDAPError.call(this, message, dn, caller || module.exports[err])
74 | }
75 | module.exports[err].constructor = module.exports[err]
76 | util.inherits(module.exports[err], LDAPError)
77 | Object.defineProperties(module.exports[err].prototype, {
78 | name: {
79 | get: function getName () { return err },
80 | configurable: false
81 | },
82 | code: {
83 | get: function getCode () { return CODES[code] },
84 | configurable: false
85 | }
86 | })
87 |
88 | ERRORS[CODES[code]] = {
89 | err,
90 | message: msg
91 | }
92 | })
93 |
94 | module.exports.getError = function (res) {
95 | assert.ok(res instanceof LDAPResult, 'res (LDAPResult) required')
96 |
97 | const errObj = ERRORS[res.status]
98 | const E = module.exports[errObj.err]
99 | return new E(res.errorMessage || errObj.message,
100 | res.matchedDN || null,
101 | module.exports.getError)
102 | }
103 |
104 | module.exports.getMessage = function (code) {
105 | assert.number(code, 'code (number) required')
106 |
107 | const errObj = ERRORS[code]
108 | return (errObj && errObj.message ? errObj.message : '')
109 | }
110 |
111 | /// --- Custom application errors
112 |
113 | function ConnectionError (message) {
114 | LDAPError.call(this, message, null, ConnectionError)
115 | }
116 | util.inherits(ConnectionError, LDAPError)
117 | module.exports.ConnectionError = ConnectionError
118 | Object.defineProperties(ConnectionError.prototype, {
119 | name: {
120 | get: function () { return 'ConnectionError' },
121 | configurable: false
122 | }
123 | })
124 |
125 | function AbandonedError (message) {
126 | LDAPError.call(this, message, null, AbandonedError)
127 | }
128 | util.inherits(AbandonedError, LDAPError)
129 | module.exports.AbandonedError = AbandonedError
130 | Object.defineProperties(AbandonedError.prototype, {
131 | name: {
132 | get: function () { return 'AbandonedError' },
133 | configurable: false
134 | }
135 | })
136 |
137 | function TimeoutError (message) {
138 | LDAPError.call(this, message, null, TimeoutError)
139 | }
140 | util.inherits(TimeoutError, LDAPError)
141 | module.exports.TimeoutError = TimeoutError
142 | Object.defineProperties(TimeoutError.prototype, {
143 | name: {
144 | get: function () { return 'TimeoutError' },
145 | configurable: false
146 | }
147 | })
148 |
--------------------------------------------------------------------------------
/lib/index.js:
--------------------------------------------------------------------------------
1 | // Copyright 2011 Mark Cavage, Inc. All rights reserved.
2 |
3 | const logger = require('./logger')
4 |
5 | const client = require('./client')
6 | const Attribute = require('@ldapjs/attribute')
7 | const Change = require('@ldapjs/change')
8 | const Protocol = require('@ldapjs/protocol')
9 | const Server = require('./server')
10 |
11 | const controls = require('./controls')
12 | const persistentSearch = require('./persistent_search')
13 | const dn = require('@ldapjs/dn')
14 | const errors = require('./errors')
15 | const filters = require('@ldapjs/filter')
16 | const messages = require('./messages')
17 | const url = require('./url')
18 |
19 | const hasOwnProperty = (target, val) => Object.prototype.hasOwnProperty.call(target, val)
20 |
21 | /// --- API
22 |
23 | module.exports = {
24 | Client: client.Client,
25 | createClient: client.createClient,
26 |
27 | Server,
28 | createServer: function (options) {
29 | if (options === undefined) { options = {} }
30 |
31 | if (typeof (options) !== 'object') { throw new TypeError('options (object) required') }
32 |
33 | if (!options.log) {
34 | options.log = logger
35 | }
36 |
37 | return new Server(options)
38 | },
39 |
40 | Attribute,
41 | Change,
42 |
43 | dn,
44 | DN: dn.DN,
45 | RDN: dn.RDN,
46 | parseDN: dn.DN.fromString,
47 |
48 | persistentSearch,
49 | PersistentSearchCache: persistentSearch.PersistentSearchCache,
50 |
51 | filters,
52 | parseFilter: filters.parseString,
53 |
54 | url,
55 | parseURL: url.parse
56 | }
57 |
58 | /// --- Export all the childrenz
59 |
60 | let k
61 |
62 | for (k in Protocol) {
63 | if (hasOwnProperty(Protocol, k)) { module.exports[k] = Protocol[k] }
64 | }
65 |
66 | for (k in messages) {
67 | if (hasOwnProperty(messages, k)) { module.exports[k] = messages[k] }
68 | }
69 |
70 | for (k in controls) {
71 | if (hasOwnProperty(controls, k)) { module.exports[k] = controls[k] }
72 | }
73 |
74 | for (k in filters) {
75 | if (hasOwnProperty(filters, k)) {
76 | if (k !== 'parse' && k !== 'parseString') { module.exports[k] = filters[k] }
77 | }
78 | }
79 |
80 | for (k in errors) {
81 | if (hasOwnProperty(errors, k)) {
82 | module.exports[k] = errors[k]
83 | }
84 | }
85 |
--------------------------------------------------------------------------------
/lib/logger.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 |
3 | const logger = require('abstract-logging')
4 | logger.child = function () { return logger }
5 |
6 | module.exports = logger
7 |
--------------------------------------------------------------------------------
/lib/messages/index.js:
--------------------------------------------------------------------------------
1 | // Copyright 2011 Mark Cavage, Inc. All rights reserved.
2 |
3 | const messages = require('@ldapjs/messages')
4 |
5 | const Parser = require('./parser')
6 |
7 | const SearchResponse = require('./search_response')
8 |
9 | /// --- API
10 |
11 | module.exports = {
12 |
13 | LDAPMessage: messages.LdapMessage,
14 | LDAPResult: messages.LdapResult,
15 | Parser,
16 |
17 | AbandonRequest: messages.AbandonRequest,
18 | AbandonResponse: messages.AbandonResponse,
19 | AddRequest: messages.AddRequest,
20 | AddResponse: messages.AddResponse,
21 | BindRequest: messages.BindRequest,
22 | BindResponse: messages.BindResponse,
23 | CompareRequest: messages.CompareRequest,
24 | CompareResponse: messages.CompareResponse,
25 | DeleteRequest: messages.DeleteRequest,
26 | DeleteResponse: messages.DeleteResponse,
27 | ExtendedRequest: messages.ExtensionRequest,
28 | ExtendedResponse: messages.ExtensionResponse,
29 | ModifyRequest: messages.ModifyRequest,
30 | ModifyResponse: messages.ModifyResponse,
31 | ModifyDNRequest: messages.ModifyDnRequest,
32 | ModifyDNResponse: messages.ModifyDnResponse,
33 | SearchRequest: messages.SearchRequest,
34 | SearchEntry: messages.SearchResultEntry,
35 | SearchReference: messages.SearchResultReference,
36 | SearchResponse,
37 | UnbindRequest: messages.UnbindRequest
38 |
39 | }
40 |
--------------------------------------------------------------------------------
/lib/messages/parser.js:
--------------------------------------------------------------------------------
1 | // Copyright 2011 Mark Cavage, Inc. All rights reserved.
2 |
3 | const EventEmitter = require('events').EventEmitter
4 | const util = require('util')
5 |
6 | const assert = require('assert-plus')
7 | const asn1 = require('@ldapjs/asn1')
8 | const logger = require('../logger')
9 |
10 | const messages = require('@ldapjs/messages')
11 | const AbandonRequest = messages.AbandonRequest
12 | const AddRequest = messages.AddRequest
13 | const AddResponse = messages.AddResponse
14 | const BindRequest = messages.BindRequest
15 | const BindResponse = messages.BindResponse
16 | const CompareRequest = messages.CompareRequest
17 | const CompareResponse = messages.CompareResponse
18 | const DeleteRequest = messages.DeleteRequest
19 | const DeleteResponse = messages.DeleteResponse
20 | const ExtendedRequest = messages.ExtensionRequest
21 | const ExtendedResponse = messages.ExtensionResponse
22 | const ModifyRequest = messages.ModifyRequest
23 | const ModifyResponse = messages.ModifyResponse
24 | const ModifyDNRequest = messages.ModifyDnRequest
25 | const ModifyDNResponse = messages.ModifyDnResponse
26 | const SearchRequest = messages.SearchRequest
27 | const SearchEntry = messages.SearchResultEntry
28 | const SearchReference = messages.SearchResultReference
29 | const SearchResponse = require('./search_response')
30 | const UnbindRequest = messages.UnbindRequest
31 | const LDAPResult = messages.LdapResult
32 |
33 | const Protocol = require('@ldapjs/protocol')
34 |
35 | /// --- Globals
36 |
37 | const BerReader = asn1.BerReader
38 |
39 | /// --- API
40 |
41 | function Parser (options = {}) {
42 | assert.object(options)
43 |
44 | EventEmitter.call(this)
45 |
46 | this.buffer = null
47 | this.log = options.log || logger
48 | }
49 | util.inherits(Parser, EventEmitter)
50 |
51 | /**
52 | * The LDAP server/client implementations will receive data from a stream and feed
53 | * it into this method. This method will collect that data into an internal
54 | * growing buffer. As that buffer fills with enough data to constitute a valid
55 | * LDAP message, the data will be parsed, emitted as a message object, and
56 | * reset the buffer to account for any next message in the stream.
57 | */
58 | Parser.prototype.write = function (data) {
59 | if (!data || !Buffer.isBuffer(data)) { throw new TypeError('data (buffer) required') }
60 |
61 | let nextMessage = null
62 | const self = this
63 |
64 | function end () {
65 | if (nextMessage) { return self.write(nextMessage) }
66 |
67 | return true
68 | }
69 |
70 | self.buffer = self.buffer ? Buffer.concat([self.buffer, data]) : data
71 |
72 | let ber = new BerReader(self.buffer)
73 |
74 | let foundSeq = false
75 | try {
76 | foundSeq = ber.readSequence()
77 | } catch (e) {
78 | this.emit('error', e)
79 | }
80 |
81 | if (!foundSeq || ber.remain < ber.length) {
82 | // ENOTENOUGH
83 | return false
84 | } else if (ber.remain > ber.length) {
85 | // ETOOMUCH
86 |
87 | // This is an odd branch. Basically, it is setting `nextMessage` to
88 | // a buffer that represents data part of a message subsequent to the one
89 | // being processed. It then re-creates `ber` as a representation of
90 | // the message being processed and advances its offset to the value
91 | // position of the TLV.
92 |
93 | // Set `nextMessage` to the bytes subsequent to the current message's
94 | // value bytes. That is, slice from the byte immediately following the
95 | // current message's value bytes until the end of the buffer.
96 | nextMessage = self.buffer.slice(ber.offset + ber.length)
97 |
98 | const currOffset = ber.offset
99 | ber = new BerReader(ber.buffer.subarray(0, currOffset + ber.length))
100 | ber.readSequence()
101 |
102 | assert.equal(ber.remain, ber.length)
103 | }
104 |
105 | // If we're here, ber holds the message, and nextMessage is temporarily
106 | // pointing at the next sequence of data (if it exists)
107 | self.buffer = null
108 |
109 | let message
110 | try {
111 | if (Object.prototype.toString.call(ber) === '[object BerReader]') {
112 | // Parse the BER into a JavaScript object representation. The message
113 | // objects require the full sequence in order to construct the object.
114 | // At this point, we have already read the sequence tag and length, so
115 | // we need to rewind the buffer a bit. The `.sequenceToReader` method
116 | // does this for us.
117 | message = messages.LdapMessage.parse(ber.sequenceToReader())
118 | } else {
119 | // Bail here if peer isn't speaking protocol at all
120 | message = this.getMessage(ber)
121 | }
122 |
123 | if (!message) {
124 | return end()
125 | }
126 |
127 | // TODO: find a better way to handle logging now that messages and the
128 | // server are decoupled. ~ jsumners 2023-02-17
129 | message.log = this.log
130 | } catch (e) {
131 | this.emit('error', e, message)
132 | return false
133 | }
134 |
135 | this.emit('message', message)
136 | return end()
137 | }
138 |
139 | Parser.prototype.getMessage = function (ber) {
140 | assert.ok(ber)
141 |
142 | const self = this
143 |
144 | const messageId = ber.readInt()
145 | const type = ber.readSequence()
146 |
147 | let Message
148 | switch (type) {
149 | case Protocol.operations.LDAP_REQ_ABANDON:
150 | Message = AbandonRequest
151 | break
152 |
153 | case Protocol.operations.LDAP_REQ_ADD:
154 | Message = AddRequest
155 | break
156 |
157 | case Protocol.operations.LDAP_RES_ADD:
158 | Message = AddResponse
159 | break
160 |
161 | case Protocol.operations.LDAP_REQ_BIND:
162 | Message = BindRequest
163 | break
164 |
165 | case Protocol.operations.LDAP_RES_BIND:
166 | Message = BindResponse
167 | break
168 |
169 | case Protocol.operations.LDAP_REQ_COMPARE:
170 | Message = CompareRequest
171 | break
172 |
173 | case Protocol.operations.LDAP_RES_COMPARE:
174 | Message = CompareResponse
175 | break
176 |
177 | case Protocol.operations.LDAP_REQ_DELETE:
178 | Message = DeleteRequest
179 | break
180 |
181 | case Protocol.operations.LDAP_RES_DELETE:
182 | Message = DeleteResponse
183 | break
184 |
185 | case Protocol.operations.LDAP_REQ_EXTENSION:
186 | Message = ExtendedRequest
187 | break
188 |
189 | case Protocol.operations.LDAP_RES_EXTENSION:
190 | Message = ExtendedResponse
191 | break
192 |
193 | case Protocol.operations.LDAP_REQ_MODIFY:
194 | Message = ModifyRequest
195 | break
196 |
197 | case Protocol.operations.LDAP_RES_MODIFY:
198 | Message = ModifyResponse
199 | break
200 |
201 | case Protocol.operations.LDAP_REQ_MODRDN:
202 | Message = ModifyDNRequest
203 | break
204 |
205 | case Protocol.operations.LDAP_RES_MODRDN:
206 | Message = ModifyDNResponse
207 | break
208 |
209 | case Protocol.operations.LDAP_REQ_SEARCH:
210 | Message = SearchRequest
211 | break
212 |
213 | case Protocol.operations.LDAP_RES_SEARCH_ENTRY:
214 | Message = SearchEntry
215 | break
216 |
217 | case Protocol.operations.LDAP_RES_SEARCH_REF:
218 | Message = SearchReference
219 | break
220 |
221 | case Protocol.operations.LDAP_RES_SEARCH:
222 | Message = SearchResponse
223 | break
224 |
225 | case Protocol.operations.LDAP_REQ_UNBIND:
226 | Message = UnbindRequest
227 | break
228 |
229 | default:
230 | this.emit('error',
231 | new Error('Op 0x' + (type ? type.toString(16) : '??') +
232 | ' not supported'),
233 | new LDAPResult({
234 | messageId,
235 | protocolOp: type || Protocol.operations.LDAP_RES_EXTENSION
236 | }))
237 |
238 | return false
239 | }
240 |
241 | return new Message({
242 | messageId,
243 | log: self.log
244 | })
245 | }
246 |
247 | /// --- Exports
248 |
249 | module.exports = Parser
250 |
--------------------------------------------------------------------------------
/lib/messages/search_response.js:
--------------------------------------------------------------------------------
1 | // Copyright 2011 Mark Cavage, Inc. All rights reserved.
2 |
3 | const assert = require('assert-plus')
4 |
5 | const Attribute = require('@ldapjs/attribute')
6 | const {
7 | SearchResultEntry: SearchEntry,
8 | SearchResultReference: SearchReference,
9 | SearchResultDone
10 | } = require('@ldapjs/messages')
11 |
12 | const parseDN = require('@ldapjs/dn').DN.fromString
13 |
14 | /// --- API
15 |
16 | class SearchResponse extends SearchResultDone {
17 | attributes
18 | notAttributes
19 | sentEntries
20 |
21 | constructor (options = {}) {
22 | super(options)
23 |
24 | this.attributes = options.attributes ? options.attributes.slice() : []
25 | this.notAttributes = []
26 | this.sentEntries = 0
27 | }
28 | }
29 |
30 | /**
31 | * Allows you to send a SearchEntry back to the client.
32 | *
33 | * @param {Object} entry an instance of SearchEntry.
34 | * @param {Boolean} nofiltering skip filtering notAttributes and '_' attributes.
35 | * Defaults to 'false'.
36 | */
37 | SearchResponse.prototype.send = function (entry, nofiltering) {
38 | if (!entry || typeof (entry) !== 'object') { throw new TypeError('entry (SearchEntry) required') }
39 | if (nofiltering === undefined) { nofiltering = false }
40 | if (typeof (nofiltering) !== 'boolean') { throw new TypeError('noFiltering must be a boolean') }
41 |
42 | const self = this
43 |
44 | const savedAttrs = {}
45 | let save = null
46 | if (entry instanceof SearchEntry || entry instanceof SearchReference) {
47 | if (!entry.messageId) { entry.messageId = this.messageId }
48 | if (entry.messageId !== this.messageId) {
49 | throw new Error('SearchEntry messageId mismatch')
50 | }
51 | } else {
52 | if (!entry.attributes) { throw new Error('entry.attributes required') }
53 |
54 | const all = (self.attributes.indexOf('*') !== -1)
55 | // Filter attributes in a plain object according to the magic `_` prefix
56 | // and presence in `notAttributes`.
57 | Object.keys(entry.attributes).forEach(function (a) {
58 | const _a = a.toLowerCase()
59 | if (!nofiltering && _a.length && _a[0] === '_') {
60 | savedAttrs[a] = entry.attributes[a]
61 | delete entry.attributes[a]
62 | } else if (!nofiltering && self.notAttributes.indexOf(_a) !== -1) {
63 | savedAttrs[a] = entry.attributes[a]
64 | delete entry.attributes[a]
65 | } else if (all) {
66 | // do nothing
67 | } else if (self.attributes.length && self.attributes.indexOf(_a) === -1) {
68 | savedAttrs[a] = entry.attributes[a]
69 | delete entry.attributes[a]
70 | }
71 | })
72 |
73 | save = entry
74 | entry = new SearchEntry({
75 | objectName: typeof (save.dn) === 'string' ? parseDN(save.dn) : save.dn,
76 | messageId: self.messageId,
77 | attributes: Attribute.fromObject(entry.attributes)
78 | })
79 | }
80 |
81 | try {
82 | this.log.debug('%s: sending: %j', this.connection.ldap.id, entry.pojo)
83 |
84 | this.connection.write(entry.toBer().buffer)
85 | this.sentEntries++
86 |
87 | // Restore attributes
88 | Object.keys(savedAttrs).forEach(function (k) {
89 | save.attributes[k] = savedAttrs[k]
90 | })
91 | } catch (e) {
92 | this.log.warn(e, '%s failure to write message %j',
93 | this.connection.ldap.id, this.pojo)
94 | }
95 | }
96 |
97 | SearchResponse.prototype.createSearchEntry = function (object) {
98 | assert.object(object)
99 |
100 | const entry = new SearchEntry({
101 | messageId: this.messageId,
102 | objectName: object.objectName || object.dn,
103 | attributes: object.attributes ?? []
104 | })
105 | return entry
106 | }
107 |
108 | SearchResponse.prototype.createSearchReference = function (uris) {
109 | if (!uris) { throw new TypeError('uris ([string]) required') }
110 |
111 | if (!Array.isArray(uris)) { uris = [uris] }
112 |
113 | const self = this
114 | return new SearchReference({
115 | messageId: self.messageId,
116 | uri: uris
117 | })
118 | }
119 |
120 | /// --- Exports
121 |
122 | module.exports = SearchResponse
123 |
--------------------------------------------------------------------------------
/lib/persistent_search.js:
--------------------------------------------------------------------------------
1 | /// --- Globals
2 |
3 | // var parseDN = require('./dn').parse
4 |
5 | const EntryChangeNotificationControl =
6 | require('./controls').EntryChangeNotificationControl
7 |
8 | /// --- API
9 |
10 | // Cache used to store connected persistent search clients
11 | function PersistentSearch () {
12 | this.clientList = []
13 | }
14 |
15 | PersistentSearch.prototype.addClient = function (req, res, callback) {
16 | if (typeof (req) !== 'object') { throw new TypeError('req must be an object') }
17 | if (typeof (res) !== 'object') { throw new TypeError('res must be an object') }
18 | if (callback && typeof (callback) !== 'function') { throw new TypeError('callback must be a function') }
19 |
20 | const log = req.log
21 |
22 | const client = {}
23 | client.req = req
24 | client.res = res
25 |
26 | log.debug('%s storing client', req.logId)
27 |
28 | this.clientList.push(client)
29 |
30 | log.debug('%s stored client', req.logId)
31 | log.debug('%s total number of clients %s',
32 | req.logId, this.clientList.length)
33 | if (callback) { callback(client) }
34 | }
35 |
36 | PersistentSearch.prototype.removeClient = function (req, res, callback) {
37 | if (typeof (req) !== 'object') { throw new TypeError('req must be an object') }
38 | if (typeof (res) !== 'object') { throw new TypeError('res must be an object') }
39 | if (callback && typeof (callback) !== 'function') { throw new TypeError('callback must be a function') }
40 |
41 | const log = req.log
42 | log.debug('%s removing client', req.logId)
43 | const client = {}
44 | client.req = req
45 | client.res = res
46 |
47 | // remove the client if it exists
48 | this.clientList.forEach(function (element, index, array) {
49 | if (element.req === client.req) {
50 | log.debug('%s removing client from list', req.logId)
51 | array.splice(index, 1)
52 | }
53 | })
54 |
55 | log.debug('%s number of persistent search clients %s',
56 | req.logId, this.clientList.length)
57 | if (callback) { callback(client) }
58 | }
59 |
60 | function getOperationType (requestType) {
61 | switch (requestType) {
62 | case 'AddRequest':
63 | case 'add':
64 | return 1
65 | case 'DeleteRequest':
66 | case 'delete':
67 | return 2
68 | case 'ModifyRequest':
69 | case 'modify':
70 | return 4
71 | case 'ModifyDNRequest':
72 | case 'modrdn':
73 | return 8
74 | default:
75 | throw new TypeError('requestType %s, is an invalid request type',
76 | requestType)
77 | }
78 | }
79 |
80 | function getEntryChangeNotificationControl (req, obj) {
81 | // if we want to return a ECNC
82 | if (req.persistentSearch.value.returnECs) {
83 | const attrs = obj.attributes
84 | const value = {}
85 | value.changeType = getOperationType(attrs.changetype)
86 | // if it's a modDN request, fill in the previous DN
87 | if (value.changeType === 8 && attrs.previousDN) {
88 | value.previousDN = attrs.previousDN
89 | }
90 |
91 | value.changeNumber = attrs.changenumber
92 | return new EntryChangeNotificationControl({ value })
93 | } else {
94 | return false
95 | }
96 | }
97 |
98 | function checkChangeType (req, requestType) {
99 | return (req.persistentSearch.value.changeTypes &
100 | getOperationType(requestType))
101 | }
102 |
103 | /// --- Exports
104 |
105 | module.exports = {
106 | PersistentSearchCache: PersistentSearch,
107 | checkChangeType,
108 | getEntryChangeNotificationControl
109 | }
110 |
--------------------------------------------------------------------------------
/lib/url.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 |
3 | const querystring = require('querystring')
4 | const url = require('url')
5 | const { DN } = require('@ldapjs/dn')
6 | const filter = require('@ldapjs/filter')
7 |
8 | module.exports = {
9 |
10 | parse: function (urlStr, parseDN) {
11 | let parsedURL
12 | try {
13 | parsedURL = new url.URL(urlStr)
14 | } catch (error) {
15 | throw new TypeError(urlStr + ' is an invalid LDAP url (scope)')
16 | }
17 |
18 | if (!parsedURL.protocol || !(parsedURL.protocol === 'ldap:' || parsedURL.protocol === 'ldaps:')) { throw new TypeError(urlStr + ' is an invalid LDAP url (protocol)') }
19 |
20 | const u = {
21 | protocol: parsedURL.protocol,
22 | hostname: parsedURL.hostname,
23 | port: parsedURL.port,
24 | pathname: parsedURL.pathname,
25 | search: parsedURL.search,
26 | href: parsedURL.href
27 | }
28 |
29 | u.secure = (u.protocol === 'ldaps:')
30 |
31 | if (!u.hostname) { u.hostname = 'localhost' }
32 |
33 | if (!u.port) {
34 | u.port = (u.secure ? 636 : 389)
35 | } else {
36 | u.port = parseInt(u.port, 10)
37 | }
38 |
39 | if (u.pathname) {
40 | u.pathname = querystring.unescape(u.pathname.substr(1))
41 | u.DN = parseDN ? DN.fromString(u.pathname) : u.pathname
42 | }
43 |
44 | if (u.search) {
45 | u.attributes = []
46 | const tmp = u.search.substr(1).split('?')
47 | if (tmp && tmp.length) {
48 | if (tmp[0]) {
49 | tmp[0].split(',').forEach(function (a) {
50 | u.attributes.push(querystring.unescape(a.trim()))
51 | })
52 | }
53 | }
54 | if (tmp[1]) {
55 | if (tmp[1] !== 'base' && tmp[1] !== 'one' && tmp[1] !== 'sub') { throw new TypeError(urlStr + ' is an invalid LDAP url (scope)') }
56 | u.scope = tmp[1]
57 | }
58 | if (tmp[2]) {
59 | u.filter = querystring.unescape(tmp[2])
60 | }
61 | if (tmp[3]) {
62 | u.extensions = querystring.unescape(tmp[3])
63 | }
64 |
65 | if (!u.scope) { u.scope = 'base' }
66 | if (!u.filter) { u.filter = filter.parseString('(objectclass=*)') } else { u.filter = filter.parseString(u.filter) }
67 | }
68 |
69 | return u
70 | }
71 |
72 | }
73 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "originalAuthor": "Mark Cavage ",
3 | "name": "ldapjs",
4 | "homepage": "http://ldapjs.org",
5 | "description": "LDAP client and server APIs",
6 | "version": "3.0.7",
7 | "license": "MIT",
8 | "repository": {
9 | "type": "git",
10 | "url": "git://github.com/ldapjs/node-ldapjs.git"
11 | },
12 | "main": "lib/index.js",
13 | "dependencies": {
14 | "@ldapjs/asn1": "^2.0.0",
15 | "@ldapjs/attribute": "^1.0.0",
16 | "@ldapjs/change": "^1.0.0",
17 | "@ldapjs/controls": "^2.1.0",
18 | "@ldapjs/dn": "^1.1.0",
19 | "@ldapjs/filter": "^2.1.1",
20 | "@ldapjs/messages": "^1.3.0",
21 | "@ldapjs/protocol": "^1.2.1",
22 | "abstract-logging": "^2.0.1",
23 | "assert-plus": "^1.0.0",
24 | "backoff": "^2.5.0",
25 | "once": "^1.4.0",
26 | "vasync": "^2.2.1",
27 | "verror": "^1.10.1"
28 | },
29 | "devDependencies": {
30 | "@fastify/pre-commit": "^2.0.2",
31 | "eslint": "^8.44.0",
32 | "eslint-config-standard": "^17.0.0",
33 | "eslint-plugin-import": "^2.27.5",
34 | "eslint-plugin-n": "^16.0.0",
35 | "eslint-plugin-node": "^11.1.0",
36 | "eslint-plugin-promise": "6.1.1",
37 | "front-matter": "^4.0.2",
38 | "get-port": "^5.1.1",
39 | "highlight.js": "^11.7.0",
40 | "marked": "^4.2.12",
41 | "tap": "^16.3.7"
42 | },
43 | "scripts": {
44 | "test": "tap --no-cov -R terse",
45 | "test:ci": "tap --coverage-report=lcovonly -R terse",
46 | "test:cov": "tap -R terse",
47 | "test:cov:html": "tap --coverage-report=html -R terse",
48 | "test:watch": "tap -n -w --no-coverage-report -R terse",
49 | "test:integration": "tap --no-cov -R terse 'test-integration/**/*.test.js'",
50 | "test:integration:local": "docker-compose up -d --wait && npm run test:integration ; docker-compose down",
51 | "lint": "eslint . --fix",
52 | "lint:ci": "eslint .",
53 | "docs": "node scripts/build-docs.js"
54 | },
55 | "pre-commit": [
56 | "lint:ci",
57 | "test"
58 | ]
59 | }
60 |
--------------------------------------------------------------------------------
/scripts/build-docs.js:
--------------------------------------------------------------------------------
1 | const fs = require('fs/promises')
2 | const path = require('path')
3 | const { marked } = require('marked')
4 | const fm = require('front-matter')
5 | const { highlight } = require('highlight.js')
6 |
7 | marked.use({
8 | highlight: (code, lang) => {
9 | if (lang) {
10 | return highlight(code, { language: lang }).value
11 | }
12 |
13 | return code
14 | }
15 | })
16 |
17 | function tocHTML (toc) {
18 | let html = '