Workflow funktioniert nun wieder. Es gab Probleme nach Aenderungen.
Build and Publish Site / docker (push) Successful in 23s

ABER: Die Applikation funktioniert nur lokal. Die deployte Version geht noch nicht.
This commit is contained in:
2026-07-03 13:24:08 +02:00
parent 97a22cf704
commit b518ae8edb
1845 changed files with 292358 additions and 57 deletions
+7
View File
@@ -0,0 +1,7 @@
{
"semi": true,
"singleQuote": true,
"arrowParens": "always",
"printWidth": 100,
"trailingComma": "es5"
}
@@ -0,0 +1,4 @@
{
"recommendations": ["esbenp.prettier-vscode", "dbaeumer.vscode-eslint"],
"unwantedRecommendations": []
}
+28
View File
@@ -0,0 +1,28 @@
{
"editor.tabSize": 2,
"editor.trimAutoWhitespace": true,
"files.insertFinalNewline": true,
"files.trimFinalNewlines": true,
"typescript.preferences.importModuleSpecifier": "relative",
"eslint.run": "onType",
"[typescript]": {
"editor.formatOnSave": true,
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[json]": {
"editor.formatOnSave": true,
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[javascript]": {
"editor.formatOnSave": true,
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[yaml]": {
"editor.formatOnSave": true,
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[markdown]": {
"editor.formatOnSave": true,
"editor.defaultFormatter": "esbenp.prettier-vscode"
}
}
+3
View File
@@ -0,0 +1,3 @@
Trent Mick <trentm@gmail.com> (http://trentm.com)
Jacques Marneweck (https://github.com/jacques)
Vesa Poikajärvi (https://github.com/vesse)
+212
View File
@@ -0,0 +1,212 @@
# node-ldapauth-fork Changelog
## 5.0.3
- [pull request #99] Ensure `groupDnProperty` is included in `attributes`
## 5.0.2
- [pull request #97] Sanitize group search filters
## 5.0.0
- Update `ldapjs` to version 2
## 4.3.3
- [pull request #86] Fix typedef of tlsOptions
## 4.3.2
- [pull request #83] Allow any @types/node version
## 4.3.0
- [issue #59, pull request #80] Add starttls
## 4.2.0
- [issue #69, pull request #71] Defer installation of reconnect event listener
## 4.1.1
- [issue #74] Remove direct moment.js dependency
## 4.1.0
- [pull request #68] Rebind admin client after reconnect
## 4.0.2
- [pull request #49] Re-emit `connectTimeout`
## 4.0.0
- Added TypeScript types
- Switch to Bunyan logger since ldapjs uses Bunyan as well
- Pass all ldapjs client options to it. The available options were taken from the ldapjs TypeScript types.
## 3.0.1
- [pull request #44] Two more ldapjs options passthrough
## 3.0.0
- [issues #20, #39, #25, #26, #41] LdapAuth is now inheriting EventEmitter and re-emits ldapjs error events. This should solve crashing because of network issues or such. Other ldapjs events are not emitted.
## 2.5.5
- [issue #43] Allow empty search base
## 2.5.4
- [pull request #42] Update ldapjs to 1.0.1 (and use ~ in package.json for it)
## 2.5.3
- [pull request #36] `groupSearchFilter` can be a `function(user)` returning the actual filter
## 2.5.2
- [pull request #31] Forward reconnect option to ldapjs
## 2.5.1
- [pull request #33] Check user provided password is not falsy (fixes #32)
## 2.5.0
- Falsy values in bind credentials now passed on to ldapjs (fixes #27)
## 2.4.0
- Update ldapjs to 1.0.0 (fixes #25)
## 2.3.3
- [issue #20] Sanitize user input
## 2.3.2
- [issue #19] Added messages to options asserts
## 2.3.1
- [issue #14] Use bcryptjs instead of the C++ version.
## 2.3.0
- [passport-ldapauth issue #10] Added support for fetching user groups. If `groupSearchBase` and `groupSearchFilter` are defined, a group search is conducted after the user has succesfully authenticated. The found groups are stored to `user._groups`:
```javascript
new LdapAuth({
url: 'ldaps://ldap.example.com:636',
adminDn: 'cn=LdapAdmin,dc=local',
adminPassword: 'LdapAdminPassword',
searchBase: 'dc=users,dc=local',
searchFilter: '(&(objectClass=person)(sAMAccountName={{username}}))',
searchAttributes: ['dn', 'cn', 'givenName', 'name', 'memberOf', 'sAMAccountName'],
groupSearchBase: 'dc=groups,dc=local',
groupSearchFilter: '(member={{dn}})',
groupSearchAttributes: ['dn', 'cn', 'sAMAccountName'],
});
```
## 2.2.19
- [issue #9] Configurable bind parameter. Thanks to @oanuna
## 2.2.18
- [issue #8] Fix options to actually work as documented
## 2.2.17
- Added `bindCredentials` option. Now defaulting to same names as ldapjs.
## 2.2.16
- Added option `includeRaw` for including `entry.raw` to the returned object (relates to ldapjs issue #238)
## 2.2.15
- [issue #5] Handle missing bcrypt and throw a explanatory exception instead
## 2.2.14
- [issue #4] Log error properties code, name, and message instead of the object
## 2.2.12
- [issue #1] Add more ldapjs options
## 2.2.11
- [passport-ldapauth issue #3] Update to ldapjs 0.7.0 fixes unhandled errors when using anonymous binding
## 2.2.10
- Try to bind with empty `adminDn` string (undefined/null equals no admin bind)
## 2.2.9
- [ldapauth issue #13] bcrypt as an optional dependency
## 2.2.8
- [ldapauth issue #2] support anonymous binding
- [ldapauth issue #3] unbind clients in `close()`
- Added option `searchScope`, default to `sub`
## 2.2.7
- Renamed to node-ldapauth-fork
## 2.2.6
- Another readme fix
## 2.2.5
- Readme updated
## 2.2.4
- [ldapauth issues #11, #12] update to ldapjs 0.6.3
- [ldapauth issue #10] use global search/replace for {{username}}
- [ldapauth issue #8] enable defining attributes to fetch from LDAP server
# node-ldapauth Changelog
## 2.2.3 (not yet released)
(nothing yet)
## 2.2.2
- [issue #5] update to bcrypt 0.7.5 (0.7.3 fixes potential mem issues)
## 2.2.1
- Fix a bug where ldapauth `authenticate()` would raise an example on an empty
username.
## 2.2.0
- Update to latest ldapjs (0.5.6) and other deps.
Note: This makes ldapauth only work with node >=0.8 (because of internal dep
in ldapjs 0.5).
## 2.1.0
- Update to ldapjs 0.4 (from 0.3). Crossing fingers that this doesn't cause breakage.
## 2.0.0
- Add `make check` for checking jsstyle.
- [issue #1] Update to bcrypt 0.5. This means increasing the base node from 0.4
to 0.6, hence the major version bump.
## 1.0.2
First working version.
+24
View File
@@ -0,0 +1,24 @@
Modified Work Copyright 2013 Vesa Poikajärvi.
Original Work Copyright 2011 Trent Mick.
All rights reserved.
Permission is hereby granted, free of charge, to any person
obtaining a copy of this software and associated documentation
files (the "Software"), to deal in the Software without
restriction, including without limitation the rights to use,
copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the
Software is furnished to do so, subject to the following
conditions:
The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
OTHER DEALINGS IN THE SOFTWARE.
+131
View File
@@ -0,0 +1,131 @@
# ldapauth-fork
[![Sponsored by Wakeone](https://img.shields.io/badge/sponsored%20by-wakeone-389fc1.svg)](https://wakeone.co)
Fork of [node-ldapauth](https://github.com/trentm/node-ldapauth) - A simple node.js lib to authenticate against an LDAP server.
## About the fork
This fork was originally created and published because of an urgent need to get newer version of [ldapjs](http://ldapjs.org/) in use to [passport-ldapauth](https://github.com/vesse/passport-ldapauth) since the newer version supported passing `tlsOptions` to the TLS module. Since then a lot of issues from the original module ([#2](https://github.com/trentm/node-ldapauth/issues/2), [#3](https://github.com/trentm/node-ldapauth/issues/3), [#8](https://github.com/trentm/node-ldapauth/issues/8), [#10](https://github.com/trentm/node-ldapauth/issues/10), [#11](https://github.com/trentm/node-ldapauth/issues/11), [#12](https://github.com/trentm/node-ldapauth/issues/12), [#13](https://github.com/trentm/node-ldapauth/pull/13)) have been fixed, and new features have been added as well.
Multiple [ldapjs](http://ldapjs.org/) client options have been made available.
## Usage
```javascript
var LdapAuth = require('ldapauth-fork');
var options = {
url: 'ldaps://ldap.example.org:636',
...
};
var auth = new LdapAuth(options);
auth.on('error', function (err) {
console.error('LdapAuth: ', err);
});
...
auth.authenticate(username, password, function(err, user) { ... });
...
auth.close(function(err) { ... })
```
`LdapAuth` inherits from `EventEmitter`.
## Install
npm install ldapauth-fork
## `LdapAuth` Config Options
Required ldapjs client options:
- `url` - LDAP server URL, eg. _ldaps://ldap.example.org:636_, or a list of URLs, e.g. `["ldaps://ldap.example.org:636"]`
ldapauth-fork options:
- `bindDN` - Admin connection DN, e.g. _uid=myapp,ou=users,dc=example,dc=org_. Optional. If not given at all, admin client is not bound. Giving empty string may result in anonymous bind when allowed.
- `bindCredentials` - Password for bindDN.
- `searchBase` - The base DN from which to search for users by username. E.g. _ou=users,dc=example,dc=org_
- `searchFilter` - LDAP search filter with which to find a user by username, e.g. _(uid={{username}})_. Use the literal _{{username}}_ to have the given username interpolated in for the LDAP search.
- `searchAttributes` - Optional, default all. Array of attributes to fetch from LDAP server.
- `bindProperty` - Optional, default _dn_. Property of the LDAP user object to use when binding to verify the password. E.g. _name_, _email_
- `searchScope` - Optional, default _sub_. Scope of the search, one of _base_, _one_, or _sub_.
ldapauth-fork can look for valid users groups too. Related options:
- `groupSearchBase` - Optional. The base DN from which to search for groups. If defined, also `groupSearchFilter` must be defined for the search to work.
- `groupSearchFilter` - Optional. LDAP search filter for groups. Place literal _{{dn}}_ in the filter to have it replaced by the property defined with `groupDnProperty` of the found user object. _{{username}}_ is also available and will be replaced with the _uid_ of the found user. This is useful for example to filter PosixGroups by _memberUid_. Optionally you can also assign a function instead. The found user is passed to the function and it should return a valid search filter for the group search.
- `groupSearchAttributes` - Optional, default all. Array of attributes to fetch from LDAP server.
- `groupDnProperty` - Optional, default _dn_. The property of user object to use in _{{dn}}_ interpolation of `groupSearchFilter`.
- `groupSearchScope` - Optional, default _sub_.
Other ldapauth-fork options:
- `includeRaw` - Optional, default false. Set to true to add property `_raw` containing the original buffers to the returned user object. Useful when you need to handle binary attributes
- `cache` - Optional, default false. If true, then up to 100 credentials at a time will be cached for 5 minutes.
- `log` - Bunyan logger instance, optional. If given this will result in TRACE-level error logging for component:ldapauth. The logger is also passed forward to ldapjs.
Optional ldapjs options, see [ldapjs documentation](https://github.com/mcavage/node-ldapjs/blob/v1.0.1/docs/client.md):
- `tlsOptions` - Needed for TLS connection. See [Node.js documentation](https://nodejs.org/api/tls.html#tls_tls_connect_options_callback)
- `socketPath`
- `timeout`
- `connectTimeout`
- `idleTimeout`
- `reconnect`
- `strictDN`
- `queueSize`
- `queueTimeout`
- `queueDisable`
## How it works
The LDAP authentication flow is usually:
1. Bind the admin client using the given `bindDN` and `bindCredentials`
2. Use the admin client to search for the user by substituting `{{username}}` from the `searchFilter` with given username
3. If user is found, verify the given password by trying to bind the user client with the found LDAP user object and given password
4. If password was correct and group search options were provided, search for the groups of the user
## express/connect basicAuth example
```javascript
var basicAuth = require('basic-auth');
var LdapAuth = require('ldapauth-fork');
var ldap = new LdapAuth({
url: 'ldaps://ldap.example.org:636',
bindDN: 'uid=myadminusername,ou=users,dc=example,dc=org',
bindCredentials: 'mypassword',
searchBase: 'ou=users,dc=example,dc=org',
searchFilter: '(uid={{username}})',
reconnect: true,
});
var rejectBasicAuth = function (res) {
res.statusCode = 401;
res.setHeader('WWW-Authenticate', 'Basic realm="Example"');
res.end('Access denied');
};
var basicAuthMiddleware = function (req, res, next) {
var credentials = basicAuth(req);
if (!credentials) {
return rejectBasicAuth(res);
}
ldap.authenticate(credentials.name, credentials.pass, function (err, user) {
if (err) {
return rejectBasicAuth(res);
}
req.user = user;
next();
});
};
```
## License
MIT
`ldapauth-fork` has been partially sponsored by [Wakeone Ltd](https://wakeone.co/).
+105
View File
@@ -0,0 +1,105 @@
/*
* Copyright 2011 Joyent, Inc. All rights reserved.
*
* An expiring LRU cache.
*
* Usage:
* var Cache = require('amon-common').Cache;
* // size, expiry, log, name
* this.accountCache = new Cache( 100, 300, log, 'account');
* this.accountCache.set('hamish', {...});
* ...
* this.accountCache.get('hamish') // -> {...}
*/
var assert = require('assert');
var LRU = require('lru-cache');
/**
* A LRU and expiring cache.
*
* @param {number} size Max number of entries to cache.
* @param {number} expiry Number of seconds after which to expire entries.
* @param {object} log Optional. All logging is at the Trace level.
* @param {string} name Optional name for this cache. Just used for logging.
* @constructor
*/
function Cache(size, expiry, log, name) {
assert.ok(size !== undefined);
assert.ok(expiry !== undefined);
this.size = size;
this.expiry = expiry * 1000;
this.log = log;
this.name = name ? name + ' ' : '';
this.items = new LRU({ max: this.size });
}
/**
* Clear cache
*
* @returns {undefined}
*/
Cache.prototype.reset = function reset() {
if (this.log) {
this.log.trace('%scache reset', this.name);
}
this.items.reset();
};
/**
* Get object from cache by given key
*
* @param {string} key - The cache key
* @returns {*} The cached value or null if not found
*/
Cache.prototype.get = function get(key) {
assert.ok(key !== undefined);
var cached = this.items.get(key);
if (cached) {
if (new Date().getTime() - cached.ctime <= this.expiry) {
if (this.log) {
this.log.trace('%scache hit: key="%s": %o', this.name, key, cached);
}
return cached.value;
}
}
if (this.log) {
this.log.trace('%scache miss: key="%s"', this.name, key);
}
return null;
};
/**
* Set a value to cache
*
* @param {string} key - Cache key
* @param {*} value - The value to cache
* @returns {*} The given value
*/
Cache.prototype.set = function set(key, value) {
assert.ok(key !== undefined);
var item = {
value: value,
ctime: new Date().getTime(),
};
if (this.log) {
this.log.trace('%scache set: key="%s": %o', this.name, key, item);
}
this.items.set(key, item);
return item;
};
/**
* Delete a single entry from cache
*
* @param {string} key - The cache key
* @returns {undefined}
*/
Cache.prototype.del = function del(key) {
if (this.log) {
this.log.trace('%scache del: key="%s"', this.name, key);
}
this.items.del(key);
};
module.exports = Cache;
+142
View File
@@ -0,0 +1,142 @@
// Type definitions for ldapauth-fork 4.0
// Project: https://github.com/vesse/node-ldapauth-fork
// Definitions by: Vesa Poikajärvi <https://github.com/vesse>
// TypeScript Version: 2.1
/// <reference types="node"/>
import { EventEmitter } from 'events';
import { ClientOptions, ErrorCallback } from 'ldapjs';
import { ConnectionOptions } from 'tls';
declare namespace LdapAuth {
type Scope = 'base' | 'one' | 'sub';
interface Callback {
(error: Error | string, result?: any): void;
}
interface GroupSearchFilterFunction {
/**
* Construct a group search filter from user object
*
* @param user The user retrieved and authenticated from LDAP
*/
(user: any): string;
}
interface Options extends ClientOptions {
/**
* Admin connection DN, e.g. uid=myapp,ou=users,dc=example,dc=org.
* If not given at all, admin client is not bound. Giving empty
* string may result in anonymous bind when allowed.
*
* Note: Not passed to ldapjs, it would bind automatically
*/
bindDN?: string;
/**
* Password for bindDN
*/
bindCredentials?: string;
/**
* The base DN from which to search for users by username.
* E.g. ou=users,dc=example,dc=org
*/
searchBase: string;
/**
* LDAP search filter with which to find a user by username, e.g.
* (uid={{username}}). Use the literal {{username}} to have the
* given username interpolated in for the LDAP search.
*/
searchFilter: string;
/**
* Scope of the search. Default: 'sub'
*/
searchScope?: Scope;
/**
* Array of attributes to fetch from LDAP server. Default: all
*/
searchAttributes?: string[];
/**
* The base DN from which to search for groups. If defined,
* also groupSearchFilter must be defined for the search to work.
*/
groupSearchBase?: string;
/**
* LDAP search filter for groups. Place literal {{dn}} in the filter
* to have it replaced by the property defined with `groupDnProperty`
* of the found user object. Optionally you can also assign a
* function instead. The found user is passed to the function and it
* should return a valid search filter for the group search.
*/
groupSearchFilter?: string | GroupSearchFilterFunction;
/**
* Scope of the search. Default: sub
*/
groupSearchScope?: Scope;
/**
* Array of attributes to fetch from LDAP server. Default: all
*/
groupSearchAttributes?: string[];
/**
* Property of the LDAP user object to use when binding to verify
* the password. E.g. name, email. Default: dn
*/
bindProperty?: string;
/**
* The property of user object to use in '{{dn}}' interpolation of
* groupSearchFilter. Default: 'dn'
*/
groupDnProperty?: string;
/**
* Set to true to add property '_raw' containing the original buffers
* to the returned user object. Useful when you need to handle binary
* attributes
*/
includeRaw?: boolean;
/**
* If true, then up to 100 credentials at a time will be cached for
* 5 minutes.
*/
cache?: boolean;
/**
* If true, then intialize TLS using the starttls mechanism.
*/
starttls?: boolean;
/**
* Provides the secure TLS options passed to tls.connect in ldapjs
*/
tlsOptions?: ConnectionOptions;
}
}
declare class LdapAuth extends EventEmitter {
/**
* @constructor
* @param opts
*/
constructor(opts: LdapAuth.Options);
/**
* Authenticate against LDAP server with given credentials
*
* @param username Username
* @param password Password
* @param callback Standard callback
*/
authenticate(username: string, password: string, callback: LdapAuth.Callback): void;
/**
* Unbind both admin and client connections
*
* @param callback Error callback
*/
close(callback?: ErrorCallback): void;
}
export = LdapAuth;
+454
View File
@@ -0,0 +1,454 @@
var assert = require('assert');
var ldap = require('ldapjs');
var format = require('util').format;
var bcrypt = require('bcryptjs');
var inherits = require('util').inherits;
var EventEmitter = require('events').EventEmitter;
/**
* Copyright 2011 (c) Trent Mick.
* Modified Work Copyright 2013 Vesa Poikajärvi.
*
* LDAP auth.
*
* Usage:
* var LdapAuth = require('ldapauth');
* var auth = new LdapAuth({url: 'ldaps://ldap.example.com:636', ...});
* ...
* auth.authenticate(username, password, function(err, user) { ... });
* ...
* auth.close(function(err) { ... })
*/
/**
* Void callback
*
* @callback voidCallback
* @param {(Error|undefined)} err - Possible error
*/
/**
* Result callback
*
* @callback resultCallback
* @param {(Error|undefined)} err - Possible error
* @param {(Object|undefined)} res - Result
*/
/**
* Get option that may be defined under different names, but accept
* the first one that is actually defined in the given object
*
* @private
* @param {object} obj - Config options
* @param {string[]} keys - List of keys to look for
* @return {*} The value of the first matching key
*/
var getOption = function (obj, keys) {
for (var i = 0; i < keys.length; i++) {
if (keys[i] in obj) {
return obj[keys[i]];
}
}
return undefined;
};
/**
* Sanitize LDAP special characters from input
*
* {@link https://tools.ietf.org/search/rfc4515#section-3}
*
* @private
* @param {string} input - String to sanitize
* @returns {string} Sanitized string
*/
var sanitizeInput = function (input) {
return input
.replace(/\*/g, '\\2a')
.replace(/\(/g, '\\28')
.replace(/\)/g, '\\29')
.replace(/\\/g, '\\5c')
.replace(/\0/g, '\\00')
.replace(/\//g, '\\2f');
};
/**
* Create an LDAP auth class. Primary usage is the `.authenticate` method.
*
* @param {Object} opts - Config options
* @constructor
*/
function LdapAuth(opts) {
this.opts = opts;
assert.ok(opts.url, 'LDAP server URL not defined (opts.url)');
assert.ok(opts.searchFilter, 'Search filter not defined (opts.searchFilter)');
this.log = opts.log && opts.log.child({ component: 'ldapauth' }, true);
this.opts.searchScope || (this.opts.searchScope = 'sub');
this.opts.bindProperty || (this.opts.bindProperty = 'dn');
this.opts.groupSearchScope || (this.opts.groupSearchScope = 'sub');
this.opts.groupDnProperty || (this.opts.groupDnProperty = 'dn');
EventEmitter.call(this);
if (opts.cache) {
// eslint-disable-next-line global-require
var Cache = require('./cache');
this.userCache = new Cache(100, 300, this.log, 'user');
this._salt = bcrypt.genSaltSync();
}
// TODO: This should be fixed somehow
this.clientOpts = {
url: opts.url,
tlsOptions: opts.tlsOptions,
socketPath: opts.socketPath,
log: opts.log,
timeout: opts.timeout,
connectTimeout: opts.connectTimeout,
idleTimeout: opts.idleTimeout,
reconnect: opts.reconnect,
strictDN: opts.strictDN,
queueSize: opts.queueSize,
queueTimeout: opts.queueTimeout,
queueDisable: opts.queueDisable,
};
// Not passed to ldapjs, don't want to autobind
// https://github.com/mcavage/node-ldapjs/blob/v1.0.1/lib/client/client.js#L343-L356
this.bindDN = getOption(opts, ['bindDn', 'bindDN', 'adminDn']);
this.bindCredentials = getOption(opts, ['bindCredentials', 'Credentials', 'adminPassword']);
this._adminClient = ldap.createClient(this.clientOpts);
this._adminBound = false;
this._userClient = ldap.createClient(this.clientOpts);
this._adminClient.on('error', this._handleError.bind(this));
this._userClient.on('error', this._handleError.bind(this));
var self = this;
if (this.opts.starttls) {
// When starttls is enabled, this callback supplants the 'connect' callback
this._adminClient.starttls(this.opts.tlsOptions, this._adminClient.controls, function (err) {
if (err) {
self._handleError(err);
} else {
self._onConnectAdmin();
}
});
this._userClient.starttls(this.opts.tlsOptions, this._userClient.controls, function (err) {
if (err) {
self._handleError(err);
}
});
} else if (opts.reconnect) {
this.once('_installReconnectListener', function () {
self.log && self.log.trace('install reconnect listener');
self._adminClient.on('connect', function () {
self._onConnectAdmin();
});
});
}
this._adminClient.on('connectTimeout', this._handleError.bind(this));
this._userClient.on('connectTimeout', this._handleError.bind(this));
if (opts.groupSearchBase && opts.groupSearchFilter) {
if (typeof opts.groupSearchFilter === 'string') {
var groupSearchFilter = opts.groupSearchFilter;
opts.groupSearchFilter = function (user) {
return groupSearchFilter
.replace(/{{dn}}/g, sanitizeInput(user[opts.groupDnProperty] || ''))
.replace(/{{username}}/g, sanitizeInput(user.uid || ''));
};
}
this._getGroups = this._findGroups;
} else {
// Assign an async identity function so there is no need to branch
// the authenticate function to have cache set up.
this._getGroups = function (user, callback) {
return callback(null, user);
};
}
}
inherits(LdapAuth, EventEmitter);
/**
* Unbind connections
*
* @param {voidCallback} callback - Callback
* @returns {undefined}
*/
LdapAuth.prototype.close = function (callback) {
var self = this;
// It seems to be OK just to call unbind regardless of if the
// client has been bound (e.g. how ldapjs pool destroy does)
self._adminClient.unbind(function () {
self._userClient.unbind(callback);
});
};
/**
* Mark admin client unbound so reconnect works as expected and re-emit the error
*
* @private
* @param {Error} err - The error to be logged and emitted
* @returns {undefined}
*/
LdapAuth.prototype._handleError = function (err) {
this.log && this.log.trace('ldap emitted error: %s', err);
this._adminBound = false;
this.emit('error', err);
};
/**
* Bind adminClient to the admin user on connect
*
* @private
* @param {voidCallback} callback - Callback that checks possible error, optional
* @returns {undefined}
*/
LdapAuth.prototype._onConnectAdmin = function (callback) {
var self = this;
// Anonymous binding
if (typeof self.bindDN === 'undefined' || self.bindDN === null) {
self._adminBound = true;
return callback ? callback() : null;
}
self.log && self.log.trace('ldap authenticate: bind: %s', self.bindDN);
self._adminClient.bind(self.bindDN, self.bindCredentials, function (err) {
if (err) {
self.log && self.log.trace('ldap authenticate: bind error: %s', err);
self._adminBound = false;
return callback ? callback(err) : null;
}
self.log && self.log.trace('ldap authenticate: bind ok');
self._adminBound = true;
if (self.opts.reconnect) {
self.emit('_installReconnectListener');
}
return callback ? callback() : null;
});
};
/**
* Ensure that `this._adminClient` is bound.
*
* @private
* @param {voidCallback} callback - Callback that checks possible error
* @returns {undefined}
*/
LdapAuth.prototype._adminBind = function (callback) {
if (this._adminBound) {
return callback();
}
// Call the connect handler with a callback
return this._onConnectAdmin(callback);
};
/**
* Conduct a search using the admin client. Used for fetching both
* user and group information.
*
* @private
* @param {string} searchBase - LDAP search base
* @param {Object} options - LDAP search options
* @param {string} options.filter - LDAP search filter
* @param {string} options.scope - LDAP search scope
* @param {(string[]|undefined)} options.attributes - Attributes to fetch
* @param {resultCallback} callback - The result handler callback
* @returns {undefined}
*/
LdapAuth.prototype._search = function (searchBase, options, callback) {
var self = this;
self._adminBind(function (bindErr) {
if (bindErr) {
return callback(bindErr);
}
self._adminClient.search(searchBase, options, function (searchErr, searchResult) {
if (searchErr) {
return callback(searchErr);
}
var items = [];
searchResult.on('searchEntry', function (entry) {
items.push(entry.object);
if (self.opts.includeRaw === true) {
items[items.length - 1]._raw = entry.raw;
}
});
searchResult.on('error', callback);
searchResult.on('end', function (result) {
if (result.status !== 0) {
var err = 'non-zero status from LDAP search: ' + result.status;
return callback(err);
}
return callback(null, items);
});
});
});
};
/**
* Find the user record for the given username.
*
* @private
* @param {string} username - Username to search for
* @param {resultCallback} callback - Result handling callback. If user is
* not found but no error happened, result is undefined.
* @returns {undefined}
*/
LdapAuth.prototype._findUser = function (username, callback) {
var self = this;
if (!username) {
return callback(new Error('empty username'));
}
var searchFilter = self.opts.searchFilter.replace(/{{username}}/g, sanitizeInput(username));
var opts = { filter: searchFilter, scope: self.opts.searchScope };
if (self.opts.searchAttributes) {
opts.attributes = self.opts.searchAttributes;
}
// groupDnProperty will be accessed in the user returned by the search, and
// so needs to be requested from the LDAP server.
if (
opts.attributes &&
self.opts.groupDnProperty &&
!opts.attributes.includes(self.opts.groupDnProperty)
) {
opts.attributes.push(self.opts.groupDnProperty);
}
self._search(self.opts.searchBase, opts, function (err, result) {
if (err) {
self.log &&
self.log.trace(
'ldap authenticate: user search error: %s %s %s',
err.code,
err.name,
err.message
);
return callback(err);
}
switch (result.length) {
case 0:
return callback();
case 1:
return callback(null, result[0]);
default:
return callback(
format('unexpected number of matches (%s) for "%s" username', result.length, username)
);
}
});
};
/**
* Find groups for given user
*
* @private
* @param {Object} user - The LDAP user object
* @param {resultCallback} callback - Result handling callback
* @returns {undefined}
*/
LdapAuth.prototype._findGroups = function (user, callback) {
var self = this;
if (!user) {
return callback(new Error('no user'));
}
var searchFilter = self.opts.groupSearchFilter(user);
var opts = { filter: searchFilter, scope: self.opts.groupSearchScope };
if (self.opts.groupSearchAttributes) {
opts.attributes = self.opts.groupSearchAttributes;
}
self._search(self.opts.groupSearchBase, opts, function (err, result) {
if (err) {
self.log &&
self.log.trace(
'ldap authenticate: group search error: %s %s %s',
err.code,
err.name,
err.message
);
return callback(err);
}
user._groups = result;
callback(null, user);
});
};
/**
* Authenticate given credentials against LDAP server
*
* @param {string} username - The username to authenticate
* @param {string} password - The password to verify
* @param {resultCallback} callback - Result handling callback
* @returns {undefined}
*/
LdapAuth.prototype.authenticate = function (username, password, callback) {
var self = this;
if (typeof password === 'undefined' || password === null || password === '') {
return callback(new Error('no password given'));
}
if (self.opts.cache) {
// Check cache. 'cached' is `{password: <hashed-password>, user: <user>}`.
var cached = self.userCache.get(username);
if (cached && bcrypt.compareSync(password, cached.password)) {
return callback(null, cached.user);
}
}
// 1. Find the user DN in question.
self._findUser(username, function (findErr, user) {
if (findErr) {
return callback(findErr);
} else if (!user) {
return callback(format('no such user: "%s"', username));
}
// 2. Attempt to bind as that user to check password.
self._userClient.bind(user[self.opts.bindProperty], password, function (bindErr) {
if (bindErr) {
self.log && self.log.trace('ldap authenticate: bind error: %s', bindErr);
return callback(bindErr);
}
// 3. If requested, fetch user groups
self._getGroups(user, function (groupErr, userWithGroups) {
if (groupErr) {
self.log && self.log.trace('ldap authenticate: group search error %s', groupErr);
return callback(groupErr);
}
if (self.opts.cache) {
bcrypt.hash(password, self._salt, function (err, hash) {
if (err) {
self.log && self.log.trace('ldap authenticate: bcrypt error, not caching %s', err);
} else {
self.userCache.set(username, { password: hash, user: userWithGroups });
}
return callback(null, userWithGroups);
});
} else {
return callback(null, userWithGroups);
}
});
});
});
};
module.exports = LdapAuth;
@@ -0,0 +1,4 @@
node_modules/
coverage/
.nyc_output/
docs/
@@ -0,0 +1,17 @@
module.exports = {
env: {
commonjs: true,
es2021: true,
node: true
},
extends: [
'standard'
],
rules: {
'no-shadow': 'error',
'no-unused-vars': ['error', {
argsIgnorePattern: '^_',
caughtErrorsIgnorePattern: '^_'
}]
}
}
@@ -0,0 +1,18 @@
version: 2
updates:
- package-ecosystem: "github-actions"
# versioning-strategy: increase-if-necessary
directory: "/"
schedule:
interval: "weekly"
day: "saturday"
time: "03:00"
timezone: "America/New_York"
- package-ecosystem: "npm"
versioning-strategy: increase-if-necessary
directory: "/"
schedule:
interval: "weekly"
day: "saturday"
time: "03:00"
timezone: "America/New_York"
@@ -0,0 +1,30 @@
name: 'Update Docs'
on:
push:
branches:
- master
jobs:
docs:
name: Update Docs
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: '14'
- name: Install Packages
run: npm install
- name: Build Docs
run: npm run docs
- name: Deploy 🚢
uses: cpina/github-action-push-to-another-repository@master
env:
API_TOKEN_GITHUB: ${{ secrets.API_TOKEN_GITHUB }}
with:
source-directory: 'public'
destination-github-username: 'ldapjs'
destination-repository-name: 'ldapjs.github.io'
user-email: 'bot@ldapjs.org'
target-branch: 'gh-pages'
@@ -0,0 +1,32 @@
name: 'Integration Tests'
# Notes:
# https://github.community/t5/GitHub-Actions/Github-Actions-services-not-reachable/m-p/30739/highlight/true#M538
on:
pull_request:
branches:
- master
jobs:
baseline:
name: Baseline Tests
runs-on: ubuntu-latest
services:
openldap:
image: ghcr.io/ldapjs/docker-test-openldap/openldap:latest
ports:
- 389:389
- 636:636
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: 'lts/*'
- name: Install Packages
run: npm install
- name: Run Tests
run: npm run test:integration
@@ -0,0 +1,54 @@
name: 'Lint And Test'
on:
push:
branches:
- master
pull_request:
branches:
- master
jobs:
lint:
name: Lint Check
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
- name: Install Packages
run: npm install
- name: Lint Code
run: npm run lint:ci
run_tests:
name: Unit Tests
strategy:
matrix:
os:
- ubuntu-latest
- windows-latest
node:
- 10.13.0
- 10.x
- 12.x
- 14.x
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: ${{ matrix.node }}
- name: Install Packages
run: npm install
- name: Run Tests
run: npm run test:ci
- name: Coveralls Parallel
uses: coverallsapp/github-action@1.1.3
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
parallel: true
- name: Coveralls Finished
uses: coverallsapp/github-action@1.1.3
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
parallel-finished: true
@@ -0,0 +1,4 @@
check-coverage: false
files:
- 'test/**/*.test.js'
@@ -0,0 +1,104 @@
# ldapjs Changelog
## 2.0.0
- Going foward, please see https://github.com/ldapjs/node-ldapjs/releases
## 1.0.2
- Update dtrace-provider dependency
## 1.0.1
- Update dependencies
* assert-plus to 1.0.0
* bunyan to 1.8.3
* dashdash to 1.14.0
* backoff to 2.5.0
* once to 1.4.0
* vasync to 1.6.4
* verror to 1.8.1
* dtrace-provider to 0.7.0
- Drop any semblence of support for node 0.8.x
## 1.0.0
- Update dependencies
* asn1 to 0.2.3
* bunyan to 1.5.1
* dtrace-provider to 0.6.0
- Removed pooled client
- Removed custom formatting for GUIDs
- Completely overhaul DN parsing/formatting
- Add options for format preservation
- Removed `spaced()` and `rndSpaced` from DN API
- Fix parent/child rules regarding empty DNs
- Request routing overhaul
* #154 Route lookups do not depend on object property order
* #111 Null ('') DN will act as catch-all
- Add StartTLS support to client (Sponsored by: DoubleCheck Email Manager)
- Improve robustness of client reconnect logic
- Add 'resultError' event to client
- Update paged search automation in client
- Add Change.apply method for modifying objects
- #143 Preserve raw Buffer value in Control objects
- Test code coverage with node-istanbul
- Convert tests to node-tape
- Add controls for server-side sorting
- #201 Replace nopt with dashdash
- #134 Allow configuration of derefAliases client option
- #197 Properly dispatch unbind requests
- #196 Handle string ports properly in server.listen
- Add basic server API tests
- Store EqualityFilter value as Buffer
- Run full test suite during 'make test'
- #190 Add error code 123 from RFC4370
- #178 Perform strict presence testing on attribute vals
- #183 Accept buffers or strings for cert/key in createServer
- #180 Add '-i, --insecure' option and to all ldapjs-\* CLIs
- #254 Allow simple client bind with empty credentials
## 0.7.1
- #169 Update dependencies
* asn1 to 0.2.1
* pooling to 0.4.6
* assert-plus to 0.1.5
* bunyan to 0.22.1
- #173 Make dtrace-provider an optional dependency
- #142 Improve parser error handling
- #161 Properly handle close events on tls sockets
- #163 Remove buffertools dependency
- #162 Fix error event handling for pooled clients
- #159 Allow ext request message to have a buffer value
- #155 Make \*Filter.matches case insensitive for attrs
## 0.7.0
- #87 Minor update to ClientPool event pass-through
- #145 Update pooling to 0.4.5
- #144 Fix unhandled error during client connection
- Output ldapi:// URLs for UNIX domain sockets
- Support extensible matching of caseIgnore and caseIgnoreSubstrings
- Fix some ClientPool event handling
- Improve DN formatting flexibility
* Add 'spaced' function to DN objects allowing toggle of inter-RDN when
rendering to a string. ('dc=test,dc=tld' vs 'dc=test, dc=tld')
* Detect RDN spacing when parsing DN.
- #128 Fix user can't bind with inmemory example
- #139 Bump required tap version to 0.4.1
- Allow binding ldap server on an ephemeral port
## 0.6.3
- Update bunyan to 0.21.1
- Remove listeners on the right object (s/client/res/)
- Replace log4js with bunyan for binaries
- #127 socket is closed issue with pools
- #122 Allow changing TLS connection options in client
- #120 Fix a bug with formatting digits less than 16.
- #118 Fix "failed to instantiate provider" warnings in console on SmartOS
## 0.6.2 - 0.1.0
**See git history**
@@ -0,0 +1,19 @@
Copyright (c) 2019 LDAPjs, All rights reserved.
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE
@@ -0,0 +1,54 @@
# LDAPjs
[![Build Status](https://github.com/ldapjs/node-ldapjs/workflows/Lint%20And%20Test/badge.svg)](https://github.com/ldapjs/node-ldapjs/actions)
[![Coverage Status](https://coveralls.io/repos/github/ldapjs/node-ldapjs/badge.svg)](https://coveralls.io/github/ldapjs/node-ldapjs/)
LDAPjs makes the LDAP protocol a first class citizen in Node.js.
## Usage
For full docs, head on over to <http://ldapjs.org>.
```javascript
var ldap = require('ldapjs');
var server = ldap.createServer();
server.search('dc=example', function(req, res, next) {
var obj = {
dn: req.dn.toString(),
attributes: {
objectclass: ['organization', 'top'],
o: 'example'
}
};
if (req.filter.matches(obj.attributes))
res.send(obj);
res.end();
});
server.listen(1389, function() {
console.log('ldapjs listening at ' + server.url);
});
```
To run that, assuming you've got the [OpenLDAP](http://www.openldap.org/)
client on your system:
ldapsearch -H ldap://localhost:1389 -x -b dc=example objectclass=*
## Installation
npm install ldapjs
DTrace support is included in ldapjs. To enable it, `npm install dtrace-provider`.
## License
MIT.
## Bugs
See <https://github.com/ldapjs/node-ldapjs/issues>.
@@ -0,0 +1,8 @@
version: '3'
services:
openldap:
image: ghcr.io/ldapjs/docker-test-openldap/openldap:latest
ports:
- 389:389
- 636:636
@@ -0,0 +1 @@
ldapjs.org
@@ -0,0 +1,266 @@
/* ---- general styles */
body {
font: 13px "Lucida Grande", "Lucida Sans Unicode", arial, sans-serif;
line-height: 1.53846; /* 20px */
color: #4a3f2d;
}
:focus:not(:focus-visible) {
outline: 0;
}
h1,h2,h3 {
font-weight:normal;
}
h3{
margin-bottom:0;
}
ul, li {
margin:0px;
padding:0px;
}
ul {
margin-left:40px;
}
ul > li {
list-style:disc;
list-style-position:inside;
margin:10px 0px;
}
hr {
border:none;
width:98%;
margin-left:-10px;
border-top:1px solid #CCCCCC;
border-bottom:1px solid #FFFFFF;
}
code,
pre {
border:1px solid #CCCCCC;
background:#F2F0EE;
-webkit-border-radius:2px;
-moz-border-radius:2px;
border-radius:2px;
white-space:pre-wrap;
}
code {
padding: 0 0.2em;
}
pre {
margin: 1em 0;
padding: .75em;
overflow: auto;
padding:10px 1.2em;
margin-top:0;
margin-bottom:20px;
}
pre code {
border: medium none;
padding: 0;
}
a code {
text-decoration: underline;
}
a {
color:#FD6512;
text-decoration:none;
}
h4 {
font-size: 85%;
margin: 0;
padding: 0;
line-height: 1em;
display: inline;
}
/* ---- header and sidebar */
#header {
background:#C3BDB3;
background:#1C313C;
height:66px;
left:0px;
position:absolute;
top:0px;
width:100%;
z-index:1;
font-size:0.7em;
}
#header h1 {
width: 424px;
height: 35px;
display:block;
background: url(../img/logo.svg) no-repeat;
line-height:2.1em;
padding:0;
padding-left:140px;
margin-top:18px;
margin-left:20px;
color:white;
text-transform: uppercase;
}
#sidebar {
background-color:#EDEBEA;
bottom:0px;
left:0px;
overflow:auto;
padding:20px 0px 0px 15px;
position:absolute;
top:66px;
width:265px;
z-index:1;
}
#content {
top:64px;
bottom:0px;
right:0px;
left:290px;
padding:20px 30px 400px;
position:absolute;
overflow:auto;
z-index:0;
}
#sidebar h1 {
font-size:1.2em;
padding:0px;
margin-top:15px;
margin-bottom:3px;
}
#sidebar ul {
margin:3px 0 10px 0;
}
#sidebar ul ul {
margin:3px 0 5px 10px;
}
#sidebar li {
margin:0;
padding:0;
font-size:0.9em;
}
#sidebar li,
#sidebar li a {
color:#5C5954;
list-style:none;
padding:1px 0px 1px 2px;
}
/* ---- intro */
.intro {
color:#29231A;
padding: 22px 25px;
background: #EDEBEA;
-webkit-border-radius: 5px;
-moz-border-radius: 5px;
-o-border-radius: 5px;
border-radius: 5px;
margin-bottom:40px;
}
.intro h1 {
color: #1C313C;
}
.intro h3 {
margin: 5px 0px 3px;
font-size: 100%;
font-weight: bold;
}
.intro ul {
list-style-type:disc;
padding-left:20px;
margin-left:0;
}
.intro ul li{
margin:0;
}
.intro p {
padding-left:20px;
margin: 5px 0px 3px;
}
h2 {
overflow: auto;
margin-top: 60px;
border-top: 2px solid #979592;
z-index: 3;
}
h1 + h2 {
margin-top: 0px;
}
h2 span {
background: #979592;
float:right;
color:#fff;
margin:0;
margin-left:3px;
padding:0.3em 0.7em;
font-size: 0.55em;
word-spacing: 0.8em; /* separate verb from path */
color:#fff;
}
/*---- print media */
@media print {
body { background:white; color:black; margin:0; }
#sidebar {
display: none;
}
#content {
position: relative;
padding: 5px;
left: 0px;
top: 0px;
}
h1, h2, h4 {
page-break-after: avoid;
}
pre {
page-break-inside: avoid;
}
}
/* tables still need cellspacing="0" in the markup */
table {
border-collapse:collapse; border-spacing:0;
margin: 20px 0;
}
th,
td {
border: solid #aaa;
border-width: 1px 0;
line-height: 23px;
padding: 0 12px;
text-align: left;
vertical-align: text-bottom;
}
th {
border-collapse: separate;
}
tbody tr:nth-child(odd) {
background-color: #f2f0ee;
}
@@ -0,0 +1 @@
<svg xmlns:svg="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" width="125" height="34.1" viewBox="0 0 146.25 39.96"><defs><path d="M-2.21-3.96h150v45.93h-150z"/><clipPath><use xlink:href="#a" overflow="visible"/></clipPath><clipPath><use xlink:href="#c-4" overflow="visible"/></clipPath></defs><clipPath><use xlink:href="#a" overflow="visible"/></clipPath><g clip-path="url(#b-8)" fill="#f60"><defs><path d="m-2.21-3.96 150 0 0 45.93-150 0z"/></defs><clipPath><use xlink:href="#c-4" overflow="visible" x="0" y="0" width="100" height="100"/></clipPath><path d="m15.74 31.29c8.61 0 15.6-6.98 15.6-15.59C31.34 7.08 24.35 0.1 15.74 0.1 7.13 0.1 0.14 7.08 0.14 15.7c0 8.61 6.98 15.6 15.6 15.6" style="fill-rule:evenodd;fill:#f60"/><path d="m12.96 7.35c0-0.32 0.26-0.59 0.59-0.59l4.38 0c0.33 0 0.59 0.26 0.59 0.59l0 5.57 5.57 0c0.33 0 0.59 0.26 0.59 0.59l0 4.38c0 0.33-0.26 0.59-0.59 0.59l-5.57 0 0 5.57c0 0.33-0.26 0.59-0.59 0.59l-4.37 0c-0.32 0-0.59-0.26-0.59-0.59l0-5.57-5.57 0c-0.32 0-0.59-0.26-0.59-0.59l0-4.37c0-0.32 0.26-0.59 0.59-0.59l5.57 0z" style="fill-rule:evenodd;fill:#fff"/></g><g clip-path="url(#b)" fill="#fff"><defs><path d="m-2.21-3.96 150 0 0 45.93-150 0z"/></defs><clipPath><use height="100" width="100" y="0" x="0" overflow="visible" xlink:href="#c"/></clipPath><path d="m35.84 25.26c1.22 0.94 2.63 1.81 4.52 1.81 2.87 0 3.62-1.73 3.62-4.01l0-19.96 3.11 0 0 16.27c0 1.42 0 2.95-0.08 4.36-0.16 3.77-2.08 5.97-6.52 5.97-3.06 0-5.31-1.26-6.21-2.24zm29.51-5.7c0-4.72-1.37-8.02-5.19-8.02-3.73 0-5.42 3.14-5.42 7.39 0 3.89 0.79 8.49 5.27 8.49 3.73 0 5.35-3.57 5.35-7.86M60.41 9.14c2.71 0 8.06 0.87 8.06 9.75 0 7.66-3.81 10.89-8.72 10.89-4.99 0-8.06-3.38-8.06-10.45 0-8.13 4.83-10.18 8.72-10.18m26.88 19.3 0-18.74-2.99 0 0 11.71c0 2.71-1.81 4.6-4.79 4.6-4.17 0-4.44-2.28-4.44-5.27l0-11.04-3.06 0 0 11.87c0 4.48 2.16 6.09 5.5 6.45-1.93 1.26-4.32 3.54-4.32 6.72 0 3.03 1.93 4.95 5.93 4.95 4.17 0 6.68-2 7.66-5.82 0.35-1.38 0.51-3.69 0.51-5.42m-7.86 8.88c3.58 0 4.83-3.1 4.83-7.39l0-3.42c-4.72 1.97-8.13 4.09-8.13 7.82 0 1.93 1.14 2.99 3.3 2.99M99.94 9.14c-5.97 0-9.12 4.79-9.12 10.61 0 5.86 2.59 10.02 8.72 10.02 3.14 0 5.42-1.41 6.41-2.24l-1.18-2c-0.75 0.55-2.4 1.81-5.03 1.81-4.16 0-5.74-3.38-5.89-6.6l1.02 0.04c3.81 0 10.81-0.94 10.81-6.76 0-2.91-2.08-4.87-5.74-4.87m-0.16 2.28c-4.2 0-5.82 3.97-5.93 7.07l0.79 0.04c2.67 0 8.17-0.63 8.17-4.17 0-1.85-1.26-2.95-3.03-2.95m13.01 17.8 0-12.81c0-2.36 2.28-4.83 5.31-4.83 3.3 0 3.93 2.36 3.93 5.27l0 12.38 3.07 0 0-13.32c0-4.48-2.2-6.76-6.25-6.76-2.56 0-4.83 1.18-6.33 3.38l-0.35-2.83-2.63 0 0.2 2.95 0 16.58 3.07 0zm16.82-27.31 0 21.97c0 2.87 0.16 5.9 5.38 5.9 1.57 0 3.3-0.51 4.44-1.3l-0.9-2.04c-0.67 0.39-1.65 0.94-2.99 0.94-1.85 0-2.87-0.82-2.87-3.42l0-11.63 5.58 0 0-2.63-5.58 0 0-7.78zm12.22 1.8c0 1.14 0.91 1.99 2 1.99 1.08 0 1.99-0.85 1.99-1.99 0-1.12-0.91-1.97-1.99-1.97-1.09 0-2 0.85-2 1.97m0.36 0c0-0.95 0.71-1.68 1.64-1.68 0.92 0 1.63 0.73 1.63 1.68 0 0.97-0.71 1.7-1.63 1.7-0.93 0-1.64-0.73-1.64-1.7m0.86 1.17 0.36 0 0-1 0.38 0 0.63 1 0.39 0-0.66-1.02c0.35-0.04 0.61-0.21 0.61-0.63 0-0.44-0.26-0.66-0.81-0.66l-0.9 0 0 2.32zm0.36-2.02 0.48 0c0.24 0 0.51 0.05 0.51 0.36 0 0.37-0.29 0.38-0.61 0.38l-0.38 0z" clip-path="url(#d)" style="fill-rule:evenodd;fill:#fff"/></g></svg>

After

Width:  |  Height:  |  Size: 3.2 KiB

@@ -0,0 +1,39 @@
<!DOCTYPE html>
<html lang="en">
<head>
<title>%(title)s</title>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<link rel="stylesheet" type="text/css" href="media/css/style.css">
<link rel="stylesheet" type="text/css" href="media/css/highlight.css">
</head>
<body>
<div id="header">
<h1>%(title)s Documentation</h1>
</div>
<div id="sidebar">
<div>Sections</div>
<span>
<ul>
<li><div><a href="index.html">Home</a></div></li>
<li><div><a href="guide.html">Guide</a></div></li>
<li><div><a href="examples.html">Examples</a></div></li>
<li><div><a href="client.html">Client API</a></div></li>
<li><div><a href="server.html">Server API</a></div></li>
<li><div><a href="dn.html">DN API</a></div></li>
<li><div><a href="filters.html">Filters API</a></div></li>
<li><div><a href="errors.html">Error API</a></div></li>
</ul>
</span>
<div>Contents</div>
</span>
%(toc_html)s
</div>
<div id="content">
%(content)s
</div><!-- end #content -->
</body>
</html>
@@ -0,0 +1,485 @@
---
title: Client API | ldapjs
---
# ldapjs Client API
<div class="intro">
This document covers the ldapjs client API and assumes that you are familiar
with LDAP. If you're not, read the [guide](guide.html) first.
</div>
# Create a client
The code to create a new client looks like:
```js
const ldap = require('ldapjs');
const client = ldap.createClient({
url: ['ldap://127.0.0.1:1389', 'ldap://127.0.0.2:1389']
});
client.on('error', (err) => {
// handle connection error
})
```
You can use `ldap://` or `ldaps://`; the latter would connect over SSL (note
that this will not use the LDAP TLS extended operation, but literally an SSL
connection to port 636, as in LDAP v2). The full set of options to create a
client is:
|Attribute |Description |
|---------------|-----------------------------------------------------------|
|url |A string or array of valid LDAP URL(s) (proto/host/port) |
|socketPath |Socket path if using AF\_UNIX sockets |
|log |A compatible logger instance (Default: no-op logger) |
|timeout |Milliseconds client should let operations live for before timing out (Default: Infinity)|
|connectTimeout |Milliseconds client should wait before timing out on TCP connections (Default: OS default)|
|tlsOptions |Additional options passed to TLS connection layer when connecting via `ldaps://` (See: The TLS docs for node.js)|
|idleTimeout |Milliseconds after last activity before client emits idle event|
|strictDN |Force strict DN parsing for client methods (Default is true)|
|reconnect |Try to reconnect when the connection gets lost (Default is false)|
### url
This parameter takes a single connection string or an array of connection strings
as an input. In case an array is provided, the client tries to connect to the
servers in given order. To achieve random server strategy (e.g. to distribute
the load among the servers), please shuffle the array before passing it as an
argument.
### Note On Logger
A passed in logger is expected to conform to the [Bunyan](https://www.npmjs.com/package/bunyan)
API. Specifically, the logger is expected to have a `child()` method. If a logger
is supplied that does not have such a method, then a shim version is added
that merely returns the passed in logger.
Known compatible loggers are:
+ [Bunyan](https://www.npmjs.com/package/bunyan)
+ [Pino](https://www.npmjs.com/package/pino)
### Note On Error Handling
The client is an `EventEmitter`. If you don't register an error handler and
e.g. a connection error occurs, Node.js will print a stack trace and exit the
process ([reference](https://nodejs.org/api/events.html#error-events)).
## Connection management
As LDAP is a stateful protocol (as opposed to HTTP), having connections torn
down from underneath you can be difficult to deal with. Several mechanisms
have been provided to mitigate this trouble.
### Reconnect
You can provide a Boolean option indicating if a reconnect should be tried. For
more sophisticated control, you can provide an Object with the properties
`initialDelay` (default: `100`), `maxDelay` (default: `10000`) and
`failAfter` (default: `Infinity`).
After the reconnect you maybe need to [bind](#bind) again.
## Client events
The client is an `EventEmitter` and can emit the following events:
|Event |Description |
|---------------|----------------------------------------------------------|
|error |General error |
|connectRefused |Server refused connection. Most likely bad authentication |
|connectTimeout |Server timeout |
|connectError |Socket connection error |
|setupError |Setup error after successful connection |
|socketTimeout |Socket timeout |
|resultError |Search result error |
|timeout |Search result timeout |
|destroy |After client is disconnected |
|end |Socket end event |
|close |Socket closed |
|connect |Client connected |
|idle |Idle timeout reached |
## Common patterns
The last two parameters in every API are `controls` and `callback`. `controls`
can be either a single instance of a `Control` or an array of `Control` objects.
You can, and probably will, omit this option.
Almost every operation has the callback form of `function(err, res)` where err
will be an instance of an `LDAPError` (you can use `instanceof` to switch).
You probably won't need to check the `res` parameter, but it's there if you do.
# bind
`bind(dn, password, controls, callback)`
Performs a bind operation against the LDAP server.
The bind API only allows LDAP 'simple' binds (equivalent to HTTP Basic
Authentication) for now. Note that all client APIs can optionally take an array
of `Control` objects. You probably don't need them though...
Example:
```js
client.bind('cn=root', 'secret', (err) => {
assert.ifError(err);
});
```
# add
`add(dn, entry, controls, callback)`
Performs an add operation against the LDAP server.
Allows you to add an entry (which is just a plain JS object), and as always,
controls are optional.
Example:
```js
const entry = {
cn: 'foo',
sn: 'bar',
email: ['foo@bar.com', 'foo1@bar.com'],
objectclass: 'fooPerson'
};
client.add('cn=foo, o=example', entry, (err) => {
assert.ifError(err);
});
```
# compare
`compare(dn, attribute, value, controls, callback)`
Performs an LDAP compare operation with the given attribute and value against
the entry referenced by dn.
Example:
```js
client.compare('cn=foo, o=example', 'sn', 'bar', (err, matched) => {
assert.ifError(err);
console.log('matched: ' + matched);
});
```
# del
`del(dn, controls, callback)`
Deletes an entry from the LDAP server.
Example:
```js
client.del('cn=foo, o=example', (err) => {
assert.ifError(err);
});
```
# exop
`exop(name, value, controls, callback)`
Performs an LDAP extended operation against an LDAP server. `name` is typically
going to be an OID (well, the RFC says it must be; however, ldapjs has no such
restriction). `value` is completely arbitrary, and is whatever the exop says it
should be.
Example (performs an LDAP 'whois' extended op):
```js
client.exop('1.3.6.1.4.1.4203.1.11.3', (err, value, res) => {
assert.ifError(err);
console.log('whois: ' + value);
});
```
# modify
`modify(name, changes, controls, callback)`
Performs an LDAP modify operation against the LDAP server. This API requires
you to pass in a `Change` object, which is described below. Note that you can
pass in a single `Change` or an array of `Change` objects.
Example:
```js
const change = new ldap.Change({
operation: 'add',
modification: {
pets: ['cat', 'dog']
}
});
client.modify('cn=foo, o=example', change, (err) => {
assert.ifError(err);
});
```
## Change
A `Change` object maps to the LDAP protocol of a modify change, and requires you
to set the `operation` and `modification`. The `operation` is a string, and
must be one of:
| Operation | Description |
|-----------|-------------|
| replace | Replaces the attribute referenced in `modification`. If the modification has no values, it is equivalent to a delete. |
| add | Adds the attribute value(s) referenced in `modification`. The attribute may or may not already exist. |
| delete | Deletes the attribute (and all values) referenced in `modification`. |
`modification` is just a plain old JS object with the values you want.
# modifyDN
`modifyDN(dn, newDN, controls, callback)`
Performs an LDAP modifyDN (rename) operation against an entry in the LDAP
server. A couple points with this client API:
* There is no ability to set "keep old dn." It's always going to flag the old
dn to be purged.
* The client code will automatically figure out if the request is a "new
superior" request ("new superior" means move to a different part of the tree,
as opposed to just renaming the leaf).
Example:
```js
client.modifyDN('cn=foo, o=example', 'cn=bar', (err) => {
assert.ifError(err);
});
```
# search
`search(base, options, controls, callback)`
Performs a search operation against the LDAP server.
The search operation is more complex than the other operations, so this one
takes an `options` object for all the parameters. However, ldapjs makes some
defaults for you so that if you pass nothing in, it's pretty much equivalent
to an HTTP GET operation (i.e., base search against the DN, filter set to
always match).
Like every other operation, `base` is a DN string.
Options can be a string representing a valid LDAP filter or an object
containing the following fields:
|Attribute |Description |
|-----------|---------------------------------------------------|
|scope |One of `base`, `one`, or `sub`. Defaults to `base`.|
|filter |A string version of an LDAP filter (see below), or a programatically constructed `Filter` object. Defaults to `(objectclass=*)`.|
|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.|
|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.|
|sizeLimit |the maximum number of entries to return. Defaults to 0 (unlimited).|
|timeLimit |the maximum amount of time the server should take in responding, in seconds. Defaults to 10. Lots of servers will ignore this.|
|paged |enable and/or configure automatic result paging|
Responses inside callback of the `search` method are an `EventEmitter` where you will get a notification for
each `searchEntry` that comes back from the server. You will additionally be able to listen for a `searchRequest`
, `searchReference`, `error` and `end` event.
`searchRequest` is emitted immediately after every `SearchRequest` is sent with a `SearchRequest` parameter. You can do operations
like `client.abandon` with `searchRequest.messageID` to abandon this search request. Note that the `error` event will
only be for client/TCP errors, not LDAP error codes like the other APIs. You'll want to check the LDAP status code
(likely for `0`) on the `end` event to assert success. LDAP search results can give you a lot of status codes, such as
time or size exceeded, busy, inappropriate matching, etc., which is why this method doesn't try to wrap up the code
matching.
Example:
```js
const opts = {
filter: '(&(l=Seattle)(email=*@foo.com))',
scope: 'sub',
attributes: ['dn', 'sn', 'cn']
};
client.search('o=example', opts, (err, res) => {
assert.ifError(err);
res.on('searchRequest', (searchRequest) => {
console.log('searchRequest: ', searchRequest.messageID);
});
res.on('searchEntry', (entry) => {
console.log('entry: ' + JSON.stringify(entry.object));
});
res.on('searchReference', (referral) => {
console.log('referral: ' + referral.uris.join());
});
res.on('error', (err) => {
console.error('error: ' + err.message);
});
res.on('end', (result) => {
console.log('status: ' + result.status);
});
});
```
## Filter Strings
The easiest way to write search filters is to write them compliant with RFC2254,
which is "The string representation of LDAP search filters." Note that
ldapjs doesn't support extensible matching, since it's one of those features
that almost nobody actually uses in practice.
Assuming you don't really want to read the RFC, search filters in LDAP are
basically are a "tree" of attribute/value assertions, with the tree specified
in prefix notation. For example, let's start simple, and build up a complicated
filter. The most basic filter is equality, so let's assume you want to search
for an attribute `email` with a value of `foo@bar.com`. The syntax would be:
```
(email=foo@bar.com)
```
ldapjs requires all filters to be surrounded by '()' blocks. Ok, that was easy.
Let's now assume that you want to find all records where the email is actually
just anything in the "@bar.com" domain and the location attribute is set to
Seattle:
```
(&(email=*@bar.com)(l=Seattle))
```
Now our filter is actually three LDAP filters. We have an `and` filter (single
amp `&`), an `equality` filter `(the l=Seattle)`, and a `substring` filter.
Substrings are wildcard filters. They use `*` as the wildcard. You can put more
than one wildcard for a given string. For example you could do `(email=*@*bar.com)`
to match any email of @bar.com or its subdomains like `"example@foo.bar.com"`.
Now, let's say we also want to set our filter to include a
specification that either the employeeType *not* be a manager nor a secretary:
```
(&(email=*@bar.com)(l=Seattle)(!(|(employeeType=manager)(employeeType=secretary))))
```
The `not` character is represented as a `!`, the `or` as a single pipe `|`.
It gets a little bit complicated, but it's actually quite powerful, and lets you
find almost anything you're looking for.
## Paging
Many LDAP server enforce size limits upon the returned result set (commonly
1000). In order to retrieve results beyond this limit, a `PagedResultControl`
is passed between the client and server to iterate through the entire dataset.
While callers could choose to do this manually via the `controls` parameter to
`search()`, ldapjs has internal mechanisms to easily automate the process. The
most simple way to use the paging automation is to set the `paged` option to
true when performing a search:
```js
const opts = {
filter: '(objectclass=commonobject)',
scope: 'sub',
paged: true,
sizeLimit: 200
};
client.search('o=largedir', opts, (err, res) => {
assert.ifError(err);
res.on('searchEntry', (entry) => {
// do per-entry processing
});
res.on('page', (result) => {
console.log('page end');
});
res.on('error', (resErr) => {
assert.ifError(resErr);
});
res.on('end', (result) => {
console.log('done ');
});
});
```
This will enable paging with a default page size of 199 (`sizeLimit` - 1) and
will output all of the resulting objects via the `searchEntry` event. At the
end of each result during the operation, a `page` event will be emitted as
well (which includes the intermediate `searchResult` object).
For those wanting more precise control over the process, an object with several
parameters can be provided for the `paged` option. The `pageSize` parameter
sets the size of result pages requested from the server. If no value is
specified, it will fall back to the default (100 or `sizeLimit` - 1, to obey
the RFC). The `pagePause` parameter allows back-pressure to be exerted on the
paged search operation by pausing at the end of each page. When enabled, a
callback function is passed as an additional parameter to `page` events. The
client will wait to request the next page until that callback is executed.
Here is an example where both of those parameters are used:
```js
const queue = new MyWorkQueue(someSlowWorkFunction);
const opts = {
filter: '(objectclass=commonobject)',
scope: 'sub',
paged: {
pageSize: 250,
pagePause: true
},
};
client.search('o=largerdir', opts, (err, res) => {
assert.ifError(err);
res.on('searchEntry', (entry) => {
// Submit incoming objects to queue
queue.push(entry);
});
res.on('page', (result, cb) => {
// Allow the queue to flush before fetching next page
queue.cbWhenFlushed(cb);
});
res.on('error', (resErr) => {
assert.ifError(resErr);
});
res.on('end', (result) => {
console.log('done');
});
});
```
# starttls
`starttls(options, controls, callback)`
Attempt to secure existing LDAP connection via STARTTLS.
Example:
```js
const opts = {
ca: [fs.readFileSync('mycacert.pem')]
};
client.starttls(opts, (err, res) => {
assert.ifError(err);
// Client communication now TLS protected
});
```
# unbind
`unbind(callback)`
Performs an unbind operation against the LDAP server.
Note that unbind operation is not an opposite operation
for bind. Unbinding results in disconnecting the client
regardless of whether a bind operation was performed.
The `callback` argument is optional as unbind does
not have a response.
Example:
```js
client.unbind((err) => {
assert.ifError(err);
});
```
@@ -0,0 +1,127 @@
---
title: DN API | ldapjs
---
# ldapjs DN API
<div class="intro">
This document covers the ldapjs DN API and assumes that you are familiar
with LDAP. If you're not, read the [guide](guide.html) first.
</div>
DNs are LDAP distinguished names, and are composed of a set of RDNs (relative
distinguished names). [RFC2253](http://www.ietf.org/rfc/rfc2253.txt) has the
complete specification, but basically an RDN is an attribute value assertion
with `=` as the seperator, like: `cn=foo` where 'cn' is 'commonName' and 'foo'
is the value. You can have compound RDNs by using the `+` character:
`cn=foo+sn=bar`. As stated above, DNs are a set of RDNs, typically separated
with the `,` character, like: `cn=foo, ou=people, o=example`. This uniquely
identifies an entry in the tree, and is read "bottom up".
# parseDN(dnString)
The `parseDN` API converts a string representation of a DN into an ldapjs DN
object; in most cases this will be handled for you under the covers of the
ldapjs framework, but if you need it, it's there.
```js
const parseDN = require('ldapjs').parseDN;
const dn = parseDN('cn=foo+sn=bar, ou=people, o=example');
console.log(dn.toString());
```
# DN
The DN object is largely what you'll be interacting with, since all the server
APIs are setup to give you a DN object.
## childOf(dn)
Returns a boolean indicating whether 'this' is a child of the passed in dn. The
`dn` argument can be either a string or a DN.
```js
server.add('o=example', (req, res, next) => {
if (req.dn.childOf('ou=people, o=example')) {
...
} else {
...
}
});
```
## parentOf(dn)
The inverse of `childOf`; returns a boolean on whether or not `this` is a parent
of the passed in dn. Like `childOf`, can take either a string or a DN.
```js
server.add('o=example', (req, res, next) => {
const dn = parseDN('ou=people, o=example');
if (dn.parentOf(req.dn)) {
...
} else {
...
}
});
```
## equals(dn)
Returns a boolean indicating whether `this` is equivalent to the passed in `dn`
argument. `dn` can be a string or a DN.
```js
server.add('o=example', (req, res, next) => {
if (req.dn.equals('cn=foo, ou=people, o=example')) {
...
} else {
...
}
});
```
## parent()
Returns a DN object that is the direct parent of `this`. If there is no parent
this can return `null` (e.g. `parseDN('o=example').parent()` will return null).
## format(options)
Convert a DN object to string according to specified formatting options. These
options are divided into two types. Preservation Options use data recorded
during parsing to preserve details of the original DN. Modification options
alter string formatting defaults. Preservation options _always_ take
precedence over Modification Options.
Preservation Options:
- `keepOrder`: Order of multi-value RDNs.
- `keepQuote`: RDN values which were quoted will remain so.
- `keepSpace`: Leading/trailing spaces will be output.
- `keepCase`: Parsed attribute name will be output instead of lowercased version.
Modification Options:
- `upperName`: RDN names will be uppercased instead of lowercased.
- `skipSpace`: Disable trailing space after RDN separators
## setFormat(options)
Sets the default `options` for string formatting when `toString` is called.
It accepts the same parameters as `format`.
## toString()
Returns the string representation of `this`.
```js
server.add('o=example', (req, res, next) => {
console.log(req.dn.toString());
});
```
@@ -0,0 +1,94 @@
---
title: Errors API | ldapjs
---
# ldapjs Errors API
<div class="intro">
This document covers the ldapjs errors API and assumes that you are familiar
with LDAP. If you're not, read the [guide](guide.html) first.
</div>
All errors in the ldapjs framework extend from an abstract error type called
`LDAPError`. In addition to the properties listed below, all errors will have
a `stack` property correctly set.
In general, you'll be using the errors in ldapjs like:
```js
const ldap = require('ldapjs');
const db = {};
server.add('o=example', (req, res, next) => {
const parent = req.dn.parent();
if (parent) {
if (!db[parent.toString()])
return next(new ldap.NoSuchObjectError(parent.toString()));
}
if (db[req.dn.toString()])
return next(new ldap.EntryAlreadyExistsError(req.dn.toString()));
...
});
```
I.e., if you just pass them into the `next()` handler, ldapjs will automatically
return the appropriate LDAP error message, and stop the handler chain.
All errors will have the following properties:
## code
Returns the LDAP status code associated with this error.
## name
The name of this error.
## message
The message that will be returned to the client.
# Complete list of LDAPError subclasses
* OperationsError
* ProtocolError
* TimeLimitExceededError
* SizeLimitExceededError
* CompareFalseError
* CompareTrueError
* AuthMethodNotSupportedError
* StrongAuthRequiredError
* ReferralError
* AdminLimitExceededError
* UnavailableCriticalExtensionError
* ConfidentialityRequiredError
* SaslBindInProgressError
* NoSuchAttributeError
* UndefinedAttributeTypeError
* InappropriateMatchingError
* ConstraintViolationError
* AttributeOrValueExistsError
* InvalidAttriubteSyntaxError
* NoSuchObjectError
* AliasProblemError
* InvalidDnSyntaxError
* AliasDerefProblemError
* InappropriateAuthenticationError
* InvalidCredentialsError
* InsufficientAccessRightsError
* BusyError
* UnavailableError
* UnwillingToPerformError
* LoopDetectError
* NamingViolationError
* ObjectclassViolationError
* NotAllowedOnNonLeafError
* NotAllowedOnRdnError
* EntryAlreadyExistsError
* ObjectclassModsProhibitedError
* AffectsMultipleDsasError
* OtherError
@@ -0,0 +1,625 @@
---
title: Examples | ldapjs
---
# ldapjs Examples
<div class="intro">
This page contains a (hopefully) growing list of sample code to get you started
with ldapjs.
</div>
# In-memory server
```js
const ldap = require('ldapjs');
///--- Shared handlers
function authorize(req, res, next) {
/* Any user may search after bind, only cn=root has full power */
const isSearch = (req instanceof ldap.SearchRequest);
if (!req.connection.ldap.bindDN.equals('cn=root') && !isSearch)
return next(new ldap.InsufficientAccessRightsError());
return next();
}
///--- Globals
const SUFFIX = 'o=joyent';
const db = {};
const server = ldap.createServer();
server.bind('cn=root', (req, res, next) => {
if (req.dn.toString() !== 'cn=root' || req.credentials !== 'secret')
return next(new ldap.InvalidCredentialsError());
res.end();
return next();
});
server.add(SUFFIX, authorize, (req, res, next) => {
const dn = req.dn.toString();
if (db[dn])
return next(new ldap.EntryAlreadyExistsError(dn));
db[dn] = req.toObject().attributes;
res.end();
return next();
});
server.bind(SUFFIX, (req, res, next) => {
const dn = req.dn.toString();
if (!db[dn])
return next(new ldap.NoSuchObjectError(dn));
if (!db[dn].userpassword)
return next(new ldap.NoSuchAttributeError('userPassword'));
if (db[dn].userpassword.indexOf(req.credentials) === -1)
return next(new ldap.InvalidCredentialsError());
res.end();
return next();
});
server.compare(SUFFIX, authorize, (req, res, next) => {
const dn = req.dn.toString();
if (!db[dn])
return next(new ldap.NoSuchObjectError(dn));
if (!db[dn][req.attribute])
return next(new ldap.NoSuchAttributeError(req.attribute));
const matches = false;
const vals = db[dn][req.attribute];
for (const value of vals) {
if (value === req.value) {
matches = true;
break;
}
}
res.end(matches);
return next();
});
server.del(SUFFIX, authorize, (req, res, next) => {
const dn = req.dn.toString();
if (!db[dn])
return next(new ldap.NoSuchObjectError(dn));
delete db[dn];
res.end();
return next();
});
server.modify(SUFFIX, authorize, (req, res, next) => {
const dn = req.dn.toString();
if (!req.changes.length)
return next(new ldap.ProtocolError('changes required'));
if (!db[dn])
return next(new ldap.NoSuchObjectError(dn));
const entry = db[dn];
for (const change of req.changes) {
mod = change.modification;
switch (change.operation) {
case 'replace':
if (!entry[mod.type])
return next(new ldap.NoSuchAttributeError(mod.type));
if (!mod.vals || !mod.vals.length) {
delete entry[mod.type];
} else {
entry[mod.type] = mod.vals;
}
break;
case 'add':
if (!entry[mod.type]) {
entry[mod.type] = mod.vals;
} else {
for (const v of mod.vals) {
if (entry[mod.type].indexOf(v) === -1)
entry[mod.type].push(v);
}
}
break;
case 'delete':
if (!entry[mod.type])
return next(new ldap.NoSuchAttributeError(mod.type));
delete entry[mod.type];
break;
}
}
res.end();
return next();
});
server.search(SUFFIX, authorize, (req, res, next) => {
const dn = req.dn.toString();
if (!db[dn])
return next(new ldap.NoSuchObjectError(dn));
let scopeCheck;
switch (req.scope) {
case 'base':
if (req.filter.matches(db[dn])) {
res.send({
dn: dn,
attributes: db[dn]
});
}
res.end();
return next();
case 'one':
scopeCheck = (k) => {
if (req.dn.equals(k))
return true;
const parent = ldap.parseDN(k).parent();
return (parent ? parent.equals(req.dn) : false);
};
break;
case 'sub':
scopeCheck = (k) => {
return (req.dn.equals(k) || req.dn.parentOf(k));
};
break;
}
const keys = Object.keys(db);
for (const key of keys) {
if (!scopeCheck(key))
return;
if (req.filter.matches(db[key])) {
res.send({
dn: key,
attributes: db[key]
});
}
}
res.end();
return next();
});
///--- Fire it up
server.listen(1389, () => {
console.log('LDAP server up at: %s', server.url);
});
```
# /etc/passwd server
```js
const fs = require('fs');
const ldap = require('ldapjs');
const { spawn } = require('child_process');
///--- Shared handlers
function authorize(req, res, next) {
if (!req.connection.ldap.bindDN.equals('cn=root'))
return next(new ldap.InsufficientAccessRightsError());
return next();
}
function loadPasswdFile(req, res, next) {
fs.readFile('/etc/passwd', 'utf8', (err, data) => {
if (err)
return next(new ldap.OperationsError(err.message));
req.users = {};
const lines = data.split('\n');
for (const line of lines) {
if (!line || /^#/.test(line))
continue;
const record = line.split(':');
if (!record || !record.length)
continue;
req.users[record[0]] = {
dn: 'cn=' + record[0] + ', ou=users, o=myhost',
attributes: {
cn: record[0],
uid: record[2],
gid: record[3],
description: record[4],
homedirectory: record[5],
shell: record[6] || '',
objectclass: 'unixUser'
}
};
}
return next();
});
}
const pre = [authorize, loadPasswdFile];
///--- Mainline
const server = ldap.createServer();
server.bind('cn=root', (req, res, next) => {
if (req.dn.toString() !== 'cn=root' || req.credentials !== 'secret')
return next(new ldap.InvalidCredentialsError());
res.end();
return next();
});
server.add('ou=users, o=myhost', pre, (req, res, next) => {
if (!req.dn.rdns[0].cn)
return next(new ldap.ConstraintViolationError('cn required'));
if (req.users[req.dn.rdns[0].cn])
return next(new ldap.EntryAlreadyExistsError(req.dn.toString()));
const entry = req.toObject().attributes;
if (entry.objectclass.indexOf('unixUser') === -1)
return next(new ldap.ConstraintViolationError('entry must be a unixUser'));
const opts = ['-m'];
if (entry.description) {
opts.push('-c');
opts.push(entry.description[0]);
}
if (entry.homedirectory) {
opts.push('-d');
opts.push(entry.homedirectory[0]);
}
if (entry.gid) {
opts.push('-g');
opts.push(entry.gid[0]);
}
if (entry.shell) {
opts.push('-s');
opts.push(entry.shell[0]);
}
if (entry.uid) {
opts.push('-u');
opts.push(entry.uid[0]);
}
opts.push(entry.cn[0]);
const useradd = spawn('useradd', opts);
const messages = [];
useradd.stdout.on('data', (data) => {
messages.push(data.toString());
});
useradd.stderr.on('data', (data) => {
messages.push(data.toString());
});
useradd.on('exit', (code) => {
if (code !== 0) {
let msg = '' + code;
if (messages.length)
msg += ': ' + messages.join();
return next(new ldap.OperationsError(msg));
}
res.end();
return next();
});
});
server.modify('ou=users, o=myhost', pre, (req, res, next) => {
if (!req.dn.rdns[0].cn || !req.users[req.dn.rdns[0].cn])
return next(new ldap.NoSuchObjectError(req.dn.toString()));
if (!req.changes.length)
return next(new ldap.ProtocolError('changes required'));
const user = req.users[req.dn.rdns[0].cn].attributes;
let mod;
for (const change of req.changes) {
mod = change.modification;
switch (change.operation) {
case 'replace':
if (mod.type !== 'userpassword' || !mod.vals || !mod.vals.length)
return next(new ldap.UnwillingToPerformError('only password updates ' +
'allowed'));
break;
case 'add':
case 'delete':
return next(new ldap.UnwillingToPerformError('only replace allowed'));
}
}
const passwd = spawn('chpasswd', ['-c', 'MD5']);
passwd.stdin.end(user.cn + ':' + mod.vals[0], 'utf8');
passwd.on('exit', (code) => {
if (code !== 0)
return next(new ldap.OperationsError('' + code));
res.end();
return next();
});
});
server.del('ou=users, o=myhost', pre, (req, res, next) => {
if (!req.dn.rdns[0].cn || !req.users[req.dn.rdns[0].cn])
return next(new ldap.NoSuchObjectError(req.dn.toString()));
const userdel = spawn('userdel', ['-f', req.dn.rdns[0].cn]);
const messages = [];
userdel.stdout.on('data', (data) => {
messages.push(data.toString());
});
userdel.stderr.on('data', (data) => {
messages.push(data.toString());
});
userdel.on('exit', (code) => {
if (code !== 0) {
let msg = '' + code;
if (messages.length)
msg += ': ' + messages.join();
return next(new ldap.OperationsError(msg));
}
res.end();
return next();
});
});
server.search('o=myhost', pre, (req, res, next) => {
const keys = Object.keys(req.users);
for (const k of keys) {
if (req.filter.matches(req.users[k].attributes))
res.send(req.users[k]);
}
res.end();
return next();
});
// LDAP "standard" listens on 389, but whatever.
server.listen(1389, '127.0.0.1', () => {
console.log('/etc/passwd LDAP server up at: %s', server.url);
});
```
# Address Book
This example is courtesy of [Diogo Resende](https://github.com/dresende) and
illustrates setting up an address book for typical mail clients such as
Thunderbird or Evolution over a MySQL database.
```js
// MySQL test: (create on database 'abook' with username 'abook' and password 'abook')
//
// CREATE TABLE IF NOT EXISTS `users` (
// `id` int(5) unsigned NOT NULL AUTO_INCREMENT,
// `username` varchar(50) NOT NULL,
// `password` varchar(50) NOT NULL,
// PRIMARY KEY (`id`),
// KEY `username` (`username`)
// ) ENGINE=InnoDB DEFAULT CHARSET=utf8;
// INSERT INTO `users` (`username`, `password`) VALUES
// ('demo', 'demo');
// CREATE TABLE IF NOT EXISTS `contacts` (
// `id` int(5) unsigned NOT NULL AUTO_INCREMENT,
// `user_id` int(5) unsigned NOT NULL,
// `name` varchar(100) NOT NULL,
// `email` varchar(255) NOT NULL,
// PRIMARY KEY (`id`),
// KEY `user_id` (`user_id`)
// ) ENGINE=InnoDB DEFAULT CHARSET=utf8;
// INSERT INTO `contacts` (`user_id`, `name`, `email`) VALUES
// (1, 'John Doe', 'john.doe@example.com'),
// (1, 'Jane Doe', 'jane.doe@example.com');
//
const ldap = require('ldapjs');
const mysql = require("mysql");
const server = ldap.createServer();
const addrbooks = {};
const userinfo = {};
const ldap_port = 389;
const basedn = "dc=example, dc=com";
const company = "Example";
const db = mysql.createClient({
user: "abook",
password: "abook",
database: "abook"
});
db.query("SELECT c.*,u.username,u.password " +
"FROM contacts c JOIN users u ON c.user_id=u.id",
(err, contacts) => {
if (err) {
console.log("Error fetching contacts", err);
process.exit(1);
}
for (const contact of contacts) {
if (!addrbooks.hasOwnProperty(contact.username)) {
addrbooks[contact.username] = [];
userinfo["cn=" + contact.username + ", " + basedn] = {
abook: addrbooks[contact.username],
pwd: contact.password
};
}
const p = contact.name.indexOf(" ");
if (p != -1)
contact.firstname = contact.name.substr(0, p);
p = contact.name.lastIndexOf(" ");
if (p != -1)
contact.surname = contact.name.substr(p + 1);
addrbooks[contact.username].push({
dn: "cn=" + contact.name + ", " + basedn,
attributes: {
objectclass: [ "top" ],
cn: contact.name,
mail: contact.email,
givenname: contact.firstname,
sn: contact.surname,
ou: company
}
});
}
server.bind(basedn, (req, res, next) => {
const username = req.dn.toString();
const password = req.credentials;
if (!userinfo.hasOwnProperty(username) ||
userinfo[username].pwd != password) {
return next(new ldap.InvalidCredentialsError());
}
res.end();
return next();
});
server.search(basedn, (req, res, next) => {
const binddn = req.connection.ldap.bindDN.toString();
if (userinfo.hasOwnProperty(binddn)) {
for (const abook of userinfo[binddn].abook) {
if (req.filter.matches(abook.attributes))
res.send(abook);
}
}
res.end();
});
server.listen(ldap_port, () => {
console.log("Addressbook started at %s", server.url);
});
});
```
To test out this example, try:
```shell
$ ldapsearch -H ldap://localhost:389 -x -D cn=demo,dc=example,dc=com \
-w demo -b "dc=example,dc=com" objectclass=*
```
# Multi-threaded Server
This example demonstrates multi-threading via the `cluster` module utilizing a `net` server for initial socket receipt. An alternate example demonstrating use of the `connectionRouter` `serverOptions` hook is available in the `examples` directory.
```js
const cluster = require('cluster');
const ldap = require('ldapjs');
const net = require('net');
const os = require('os');
const threads = [];
threads.getNext = function () {
return (Math.floor(Math.random() * this.length));
};
const serverOptions = {
port: 1389
};
if (cluster.isMaster) {
const server = net.createServer(serverOptions, (socket) => {
socket.pause();
console.log('ldapjs client requesting connection');
let routeTo = threads.getNext();
threads[routeTo].send({ type: 'connection' }, socket);
});
for (let i = 0; i < os.cpus().length; i++) {
let thread = cluster.fork({
'id': i
});
thread.id = i;
thread.on('message', function (msg) {
});
threads.push(thread);
}
server.listen(serverOptions.port, function () {
console.log('ldapjs listening at ldap://127.0.0.1:' + serverOptions.port);
});
} else {
const server = ldap.createServer(serverOptions);
let threadId = process.env.id;
process.on('message', (msg, socket) => {
switch (msg.type) {
case 'connection':
server.newConnection(socket);
socket.resume();
console.log('ldapjs client connection accepted on ' + threadId.toString());
}
});
server.search('dc=example', function (req, res, next) {
console.log('ldapjs search initiated on ' + threadId.toString());
var obj = {
dn: req.dn.toString(),
attributes: {
objectclass: ['organization', 'top'],
o: 'example'
}
};
if (req.filter.matches(obj.attributes))
res.send(obj);
res.end();
});
}
```
@@ -0,0 +1,317 @@
---
title: Filters API | ldapjs
---
# ldapjs Filters API
<div class="intro">
This document covers the ldapjs filters API and assumes that you are familiar
with LDAP. If you're not, read the [guide](guide.html) first.
</div>
LDAP search filters are really the backbone of LDAP search operations, and
ldapjs tries to get you in "easy" with them if your dataset is small, and also
lets you introspect them if you want to write a "query planner". For reference,
make sure to read over [RFC2254](http://www.ietf.org/rfc/rfc2254.txt), as this
explains the LDAPv3 text filter representation.
ldapjs gives you a distinct object type mapping to each filter that is
context-sensitive. However, _all_ filters have a `matches()` method on them, if
that's all you need. Most filters will have an `attribute` property on them,
since "simple" filters all operate on an attribute/value assertion. The
"complex" filters are really aggregations of other filters (i.e. 'and'), and so
these don't provide that property.
All Filters in the ldapjs framework extend from `Filter`, which wil have the
property `type` available; this will return a string name for the filter, and
will be one of:
# parseFilter(filterString)
Parses an [RFC2254](http://www.ietf.org/rfc/rfc2254.txt) filter string into an
ldapjs object(s). If the filter is "complex", it will be a "tree" of objects.
For example:
```js
const parseFilter = require('ldapjs').parseFilter;
const f = parseFilter('(objectclass=*)');
```
Is a "simple" filter, and would just return a `PresenceFilter` object. However,
```js
const f = parseFilter('(&(employeeType=manager)(l=Seattle))');
```
Would return an `AndFilter`, which would have a `filters` array of two
`EqualityFilter` objects.
`parseFilter` will throw if an invalid string is passed in (that is, a
syntactically invalid string).
# EqualityFilter
The equality filter is used to check exact matching of attribute/value
assertions. This object will have an `attribute` and `value` property, and the
`name` property will be `equal`.
The string syntax for an equality filter is `(attr=value)`.
The `matches()` method will return true IFF the passed in object has a
key matching `attribute` and a value matching `value`.
```js
const f = new EqualityFilter({
attribute: 'cn',
value: 'foo'
});
f.matches({cn: 'foo'}); => true
f.matches({cn: 'bar'}); => false
```
Equality matching uses "strict" type JavaScript comparison, and by default
everything in ldapjs (and LDAP) is a UTF-8 string. If you want comparison
of numbers, or something else, you'll need to use a middleware interceptor
that transforms values of objects.
# PresenceFilter
The presence filter is used to check if an object has an attribute at all, with
any value. This object will have an `attribute` property, and the `name`
property will be `present`.
The string syntax for a presence filter is `(attr=*)`.
The `matches()` method will return true IFF the passed in object has a
key matching `attribute`.
```js
const f = new PresenceFilter({
attribute: 'cn'
});
f.matches({cn: 'foo'}); => true
f.matches({sn: 'foo'}); => false
```
# SubstringFilter
The substring filter is used to do wildcard matching of a string value. This
object will have an `attribute` property and then it will have an `initial`
property, which is the prefix match, an `any` which will be an array of strings
that are to be found _somewhere_ in the target string, and a `final` property,
which will be the suffix match of the string. `any` and `final` are both
optional. The `name` property will be `substring`.
The string syntax for a presence filter is `(attr=foo*bar*cat*dog)`, which would
map to:
```js
{
initial: 'foo',
any: ['bar', 'cat'],
final: 'dog'
}
```
The `matches()` method will return true IFF the passed in object has a
key matching `attribute` and the "regex" matches the value
```js
const f = new SubstringFilter({
attribute: 'cn',
initial: 'foo',
any: ['bar'],
final: 'baz'
});
f.matches({cn: 'foobigbardogbaz'}); => true
f.matches({sn: 'fobigbardogbaz'}); => false
```
# GreaterThanEqualsFilter
The ge filter is used to do comparisons and ordering based on the value type. As
mentioned elsewhere, by default everything in LDAP and ldapjs is a string, so
this filter's `matches()` would be using lexicographical ordering of strings.
If you wanted `>=` semantics over numeric values, you would need to add some
middleware to convert values before comparison (and the value of the filter).
Note that the ldapjs schema middleware will do this.
The GreaterThanEqualsFilter will have an `attribute` property, a `value`
property and the `name` property will be `ge`.
The string syntax for a ge filter is:
```
(cn>=foo)
```
The `matches()` method will return true IFF the passed in object has a
key matching `attribute` and the value is `>=` this filter's `value`.
```js
const f = new GreaterThanEqualsFilter({
attribute: 'cn',
value: 'foo',
});
f.matches({cn: 'foobar'}); => true
f.matches({cn: 'abc'}); => false
```
# LessThanEqualsFilter
The le filter is used to do comparisons and ordering based on the value type. As
mentioned elsewhere, by default everything in LDAP and ldapjs is a string, so
this filter's `matches()` would be using lexicographical ordering of strings.
If you wanted `<=` semantics over numeric values, you would need to add some
middleware to convert values before comparison (and the value of the filter).
Note that the ldapjs schema middleware will do this.
The string syntax for a le filter is:
```
(cn<=foo)
```
The LessThanEqualsFilter will have an `attribute` property, a `value`
property and the `name` property will be `le`.
The `matches()` method will return true IFF the passed in object has a
key matching `attribute` and the value is `<=` this filter's `value`.
```js
const f = new LessThanEqualsFilter({
attribute: 'cn',
value: 'foo',
});
f.matches({cn: 'abc'}); => true
f.matches({cn: 'foobar'}); => false
```
# AndFilter
The and filter is a complex filter that simply contains "child" filters. The
object will have a `filters` property which is an array of `Filter` objects. The
`name` property will be `and`.
The string syntax for an and filter is (assuming below we're and'ing two
equality filters):
```
(&(cn=foo)(sn=bar))
```
The `matches()` method will return true IFF the passed in object matches all
the filters in the `filters` array.
```js
const f = new AndFilter({
filters: [
new EqualityFilter({
attribute: 'cn',
value: 'foo'
}),
new EqualityFilter({
attribute: 'sn',
value: 'bar'
})
]
});
f.matches({cn: 'foo', sn: 'bar'}); => true
f.matches({cn: 'foo', sn: 'baz'}); => false
```
# OrFilter
The or filter is a complex filter that simply contains "child" filters. The
object will have a `filters` property which is an array of `Filter` objects. The
`name` property will be `or`.
The string syntax for an or filter is (assuming below we're or'ing two
equality filters):
```
(|(cn=foo)(sn=bar))
```
The `matches()` method will return true IFF the passed in object matches *any*
of the filters in the `filters` array.
```js
const f = new OrFilter({
filters: [
new EqualityFilter({
attribute: 'cn',
value: 'foo'
}),
new EqualityFilter({
attribute: 'sn',
value: 'bar'
})
]
});
f.matches({cn: 'foo', sn: 'baz'}); => true
f.matches({cn: 'bar', sn: 'baz'}); => false
```
# NotFilter
The not filter is a complex filter that contains a single "child" filter. The
object will have a `filter` property which is an instance of a `Filter` object.
The `name` property will be `not`.
The string syntax for a not filter is (assuming below we're not'ing an
equality filter):
```
(!(cn=foo))
```
The `matches()` method will return true IFF the passed in object does not match
the filter in the `filter` property.
```js
const f = new NotFilter({
filter: new EqualityFilter({
attribute: 'cn',
value: 'foo'
})
});
f.matches({cn: 'bar'}); => true
f.matches({cn: 'foo'}); => false
```
# ApproximateFilter
The approximate filter is used to check "approximate" matching of
attribute/value assertions. This object will have an `attribute` and
`value` property, and the `name` property will be `approx`.
As a side point, this is a useless filter. It's really only here if you have
some whacky client that's sending this. It just does an exact match (which
is what ActiveDirectory does too).
The string syntax for an equality filter is `(attr~=value)`.
The `matches()` method will return true IFF the passed in object has a
key matching `attribute` and a value exactly matching `value`.
```js
const f = new ApproximateFilter({
attribute: 'cn',
value: 'foo'
});
f.matches({cn: 'foo'}); => true
f.matches({cn: 'bar'}); => false
```
@@ -0,0 +1,697 @@
---
title: LDAP Guide | ldapjs
---
# LDAP Guide
<div class="intro">
This guide was written assuming that you (1) don't know anything about ldapjs,
and perhaps more importantly (2) know little, if anything about LDAP. If you're
already an LDAP whiz, please don't read this and feel it's condescending. Most
people don't know how LDAP works, other than that "it's that thing that has my
password."
By the end of this guide, we'll have a simple LDAP server that accomplishes a
"real" task.
</div>
# What exactly is LDAP?
If you haven't already read the
[wikipedia](http://en.wikipedia.org/wiki/Lightweight_Directory_Access_Protocol)
entry (which you should go do right now), LDAP is the "Lightweight Directory
Access Protocol". A directory service basically breaks down as follows:
* A directory is a tree of entries (similar to but different than an FS).
* Every entry has a unique name in the tree.
* An entry is a set of attributes.
* An attribute is a key/value(s) pairing (multivalue is natural).
It might be helpful to visualize:
```
o=example
/ \
ou=users ou=groups
/ | | \
cn=john cn=jane cn=dudes cn=dudettes
/
keyid=foo
```
Let's say we wanted to look at the record cn=john:
```shell
dn: cn=john, ou=users, o=example
cn: john
sn: smith
email: john@example.com
email: john.smith@example.com
objectClass: person
```
A few things to note:
* All names in a directory tree are actually referred to as a _distinguished
name_, or _dn_ for short. A dn is comprised of attributes that lead to that
node in the tree, as shown above (the syntax is foo=bar, ...).
* The root of the tree is at the right of the _dn_, which is inverted from a
filesystem hierarchy.
* Every entry in the tree is an _instance of_ an _objectclass_.
* An _objectclass_ is a schema concept; think of it like a table in a
traditional ORM.
* An _objectclass_ defines what _attributes_ an entry can have (on the ORM
analogy, an _attribute_ would be like a column).
That's it. LDAP, then, is the protocol for interacting with the directory tree,
and it's comprehensively specified for common operations, like
add/update/delete and importantly, search. Really, the power of LDAP comes
through the search operations defined in the protocol, which are richer
than HTTP query string filtering, but less powerful than full SQL. You can
think of LDAP as a NoSQL/document store with a well-defined query syntax.
So, why isn't LDAP more popular for a lot of applications? Like anything else
that has "simple" or "lightweight" in the name, it's not really that
lightweight. In particular, almost all of the implementations of LDAP stem
from the original University of Michigan codebase written in 1996. At that
time, the original intention of LDAP was to be an IP-accessible gateway to the
much more complex X.500 directories, which means that a lot of that
baggage has carried through to today. That makes for a high barrier to entry,
when most applications just don't need most of those features.
## How is ldapjs any different?
Well, on the one hand, since ldapjs has to be 100% wire compatible with LDAP to
be useful, it's not. On the other hand, there are no forced assumptions about
what you need and don't need for your use of a directory system. For example,
want to run with no-schema in OpenLDAP/389DS/et al? Good luck. Most of the
server implementations support arbitrary "backends" for persistence, but really
you'll be using [BDB](http://www.oracle.com/technetwork/database/berkeleydb/overview/index.html).
Want to run schema-less in ldapjs, or wire it up with some mongoose models? No
problem. Want to back it to redis? Should be able to get some basics up in a
day or two.
Basically, the ldapjs philosophy is to deal with the "muck" of LDAP, and then
get out of the way so you can just use the "good parts."
# Ok, cool. Learn me some LDAP!
With the initial fluff out of the way, let's do something crazy to teach
you some LDAP. Let's put an LDAP server up over the top of your (Linux) host's
/etc/passwd and /etc/group files. Usually sysadmins "go the other way," and
replace /etc/passwd with a
[PAM](http://en.wikipedia.org/wiki/Pluggable_authentication_module "Pluggable
authentication module") module to LDAP. While this is probably not a super
useful real-world use case, it will teach you some of the basics. If it is
useful to you, then that's gravy.
## Install
If you don't already have node.js and npm, clearly you need those, so follow
the steps at [nodejs.org](http://nodejs.org) and [npmjs.org](http://npmjs.org),
respectively. After that, run:
```shell
$ npm install ldapjs
```
Rather than overload you with client-side programming for now, we'll use
the OpenLDAP CLI to interact with our server. It's almost certainly already
installed on your system, but if not, you can get it from brew/apt/yum/your
package manager here.
To get started, open some file, and let's get the library loaded and a server
created:
```js
const ldap = require('ldapjs');
const server = ldap.createServer();
server.listen(1389, () => {
console.log('/etc/passwd LDAP server up at: %s', server.url);
});
```
And run that. Doing anything will give you errors (LDAP "No Such Object")
since we haven't added any support in yet, but go ahead and try it anyway:
```shell
$ ldapsearch -H ldap://localhost:1389 -x -b "o=myhost" objectclass=*
```
Before we go any further, note that the complete code for the server we are
about to build up is on the [examples](examples.html) page.
## Bind
So, lesson #1 about LDAP: unlike HTTP, it's connection-oriented; that means that
you authenticate (in LDAP nomenclature this is called a _bind_), and all
subsequent operations operate at the level of priviledge you established during
a bind. You can bind any number of times on a single connection and change that
identity. Technically, it's optional, and you can support _anonymous_
operations from clients, but (1) you probably don't want that, and (2) most
LDAP clients will initiate a bind anyway (OpenLDAP will), so let's add it in
and get it out of our way.
What we're going to do is add a "root" user to our LDAP server. This root user
has no correspondence to our Unix root user, it's just something we're making up
and going to use for allowing an (LDAP) admin to do anything. To do so, add
this code into your file:
```js
server.bind('cn=root', (req, res, next) => {
if (req.dn.toString() !== 'cn=root' || req.credentials !== 'secret')
return next(new ldap.InvalidCredentialsError());
res.end();
return next();
});
```
Not very secure, but this is a demo. What we did there was "mount" a tree in
the ldapjs server, and add a handler for the _bind_ method. If you've ever used
express, this pattern should be really familiar; you can add any number of
handlers in, as we'll see later.
On to the meat of the method. What's up with this?
```js
if (req.dn.toString() !== 'cn=root' || req.credentials !== 'secret')
```
The first part `req.dn.toString() !== 'cn=root'`: you're probably thinking
"WTF?!? Does ldapjs allow something other than cn=root into this handler?" Sort
of. It allows cn=root *and any children* into that handler. So the entries
`cn=root` and `cn=evil, cn=root` would both match and flow into this handler.
Hence that check. The second check `req.credentials` is probably obvious, but
it brings up an important point, and that is the `req`, `res` objects in ldapjs
are not homogenous across server operation types. Unlike HTTP, there's not a
single message format, so each of the operations has fields and functions
appropriate to that type. The LDAP bind operation has `credentials`, which are
a string representation of the client's password. This is logically the same as
HTTP Basic Authentication (there are other mechanisms, but that's out of scope
for a getting started guide). Ok, if either of those checks failed, we pass a
new ldapjs `Error` back into the server, and it will (1) halt the chain, and (2)
send the proper error code back to the client.
Lastly, assuming that this request was ok, we just end the operation with
`res.end()`. The `return next()` isn't strictly necessary, since here we only
have one handler in the chain, but it's good habit to always do that, so if you
add another handler in later you won't get bit by it not being invoked.
Blah blah, let's try running the ldap client again, first with a bad password:
```shell
$ ldapsearch -H ldap://localhost:1389 -x -D cn=root -w foo -b "o=myhost" objectclass=*
ldap_bind: Invalid credentials (49)
matched DN: cn=root
additional info: Invalid Credentials
```
And again with the correct one:
```shell
$ ldapsearch -H ldap://localhost:1389 -x -D cn=root -w secret -LLL -b "o=myhost" objectclass=*
No such object (32)
Additional information: No tree found for: o=myhost
```
Don't worry about all the flags we're passing into OpenLDAP, that's just to make
their CLI less annonyingly noisy. This time, we got another `No such object`
error, but it's for the tree `o=myhost`. That means our bind went through, and
our search failed, since we haven't yet added a search handler. Just one more
small thing to do first.
Remember earlier I said there were no authorization rules baked into LDAP? Well,
we added a bind route, so the only user that can authenticate is `cn=root`, but
what if the remote end doesn't authenticate at all? Right, nothing says they
*have to* bind, that's just what the common clients do. Let's add a quick
authorization handler that we'll use in all our subsequent routes:
```js
function authorize(req, res, next) {
if (!req.connection.ldap.bindDN.equals('cn=root'))
return next(new ldap.InsufficientAccessRightsError());
return next();
}
```
Should be pretty self-explanatory, but as a reminder, LDAP is connection
oriented, so we check that the connection remote user was indeed our `cn=root`
(by default ldapjs will have a DN of `cn=anonymous` if the client didn't bind).
## Search
We said we wanted to allow LDAP operations over /etc/passwd, so let's detour
for a moment to explain an /etc/passwd record.
```shell
jsmith:x:1001:1000:Joe Smith,Room 1007,(234)555-8910,(234)555-0044,email:/home/jsmith:/bin/sh
```
The sample record above maps to:
|Field |Description |
|-------------------|-----------------------------------|
|jsmith |Username |
|x |Placeholder for password hash |
|1001 |Numeric UID |
|1000 |Numeric Primary GID |
|'Joe Smith,...' |DisplayName |
|/home/jsmith |Home directory |
|/bin/sh |Shell |
Let's write some handlers to parse that and transform it into an LDAP search
record (note, you'll need to add `const fs = require('fs');` at the top of the
source file).
First, make a handler that just loads the "user database" in a "pre" handler:
```js
function loadPasswdFile(req, res, next) {
fs.readFile('/etc/passwd', 'utf8', (err, data) => {
if (err)
return next(new ldap.OperationsError(err.message));
req.users = {};
const lines = data.split('\n');
for (const line of lines) {
if (!line || /^#/.test(line))
continue;
const record = line.split(':');
if (!record || !record.length)
continue;
req.users[record[0]] = {
dn: 'cn=' + record[0] + ', ou=users, o=myhost',
attributes: {
cn: record[0],
uid: record[2],
gid: record[3],
description: record[4],
homedirectory: record[5],
shell: record[6] || '',
objectclass: 'unixUser'
}
};
}
return next();
});
}
```
Ok, all that did is tack the /etc/passwd records onto req.users so that any
subsequent handler doesn't have to reload the file. Next, let's write a search
handler to process that:
```js
const pre = [authorize, loadPasswdFile];
server.search('o=myhost', pre, (req, res, next) => {
const keys = Object.keys(req.users);
for (const k of keys) {
if (req.filter.matches(req.users[k].attributes))
res.send(req.users[k]);
}
res.end();
return next();
});
```
And try running:
```shell
$ ldapsearch -H ldap://localhost:1389 -x -D cn=root -w secret -LLL -b "o=myhost" cn=root
dn: cn=root, ou=users, o=myhost
cn: root
uid: 0
gid: 0
description: System Administrator
homedirectory: /var/root
shell: /bin/sh
objectclass: unixUser
```
Sweet! Try this out too:
```shell
$ ldapsearch -H ldap://localhost:1389 -x -D cn=root -w secret -LLL -b "o=myhost" objectclass=*
...
```
You should have seen an entry for every record in /etc/passwd with the second.
What all did we do here? A lot. Let's break this down...
### What did I just do on the command line?
Let's start with looking at what you even asked for:
```shell
$ ldapsearch -H ldap://localhost:1389 -x -D cn=root -w secret -LLL -b "o=myhost" cn=root
```
We can throw away `ldapsearch -H -x -D -w -LLL`, as those just specify the URL
to connect to, the bind credentials and the `-LLL` just quiets down OpenLDAP.
That leaves us with: `-b "o=myhost" cn=root`.
The `-b o=myhost` tells our LDAP server where to _start_ looking in
the tree for entries that might match the search filter, which above is
`cn=root`.
In this little LDAP example, we're mostly throwing out any qualification of the
"tree," since there's not actually a tree in /etc/passwd (we will extend later
with /etc/group). Remember how I said ldapjs gets out of the way and doesn't
force anything on you? Here's an example. If we wanted an LDAP server to run
over the filesystem, we actually would use this, but here, meh.
Next, `cn=root` is the search "filter". LDAP has a rich specification of
filters, where you can specify `and`, `or`, `not`, `>=`, `<=`, `equal`,
`wildcard`, `present` and a few other esoteric things. Really, `equal`,
`wildcard`, `present` and the boolean operators are all you'll likely ever need.
So, the filter `cn=root` is an "equality" filter, and says to only return
entries that have attributes that match that. In the second invocation, we used
a 'presence' filter, to say 'return any entries that have an objectclass'
attribute, which in LDAP parlance is saying "give me everything."
### The code
In the code above, let's ignore the fs and split stuff, since really all we
did was read in /etc/passwd line by line. After that, we looked at each record
and made the cheesiest transform ever, which is making up a "search entry." A
search entry _must_ have a DN so the client knows what record it is, and a set
of attributes. So that's why we did this:
```js
const entry = {
dn: 'cn=' + record[0] + ', ou=users, o=myhost',
attributes: {
cn: record[0],
uid: record[2],
gid: record[3],
description: record[4],
homedirectory: record[5],
shell: record[6] || '',
objectclass: 'unixUser'
}
};
```
Next, we let ldapjs do all the hard work of figuring out LDAP search filters
for us by calling `req.filter.matches`. If it matched, we return the whole
record with `res.send`. In this little example we're running O(n), so for
something big and/or slow, you'd have to do some work to effectively write a
query planner (or just not support it...). For some reference code, check out
`node-ldapjs-riak`, which takes on the fairly difficult task of writing a 'full'
LDAP server over riak.
To demonstrate what ldapjs is doing for you, let's find all users who have a
shell set to `/bin/false` and whose name starts with `p` (I'm doing this
on Ubuntu). Then, let's say we only care about their login name and primary
group id. We'd do this:
```shell
$ ldapsearch -H ldap://localhost:1389 -x -D cn=root -w secret -LLL -b "o=myhost" "(&(shell=/bin/false)(cn=p*))" cn gid
dn: cn=proxy, ou=users, o=myhost
cn: proxy
gid: 13
dn: cn=pulse, ou=users, o=myhost
cn: pulse
gid: 114
```
## Add
This is going to be a little bit ghetto, since what we're going to do is just
use node's child process module to spawn calls to `adduser`. Go ahead and add
the following code in as another handler (you'll need a
`const { spawn } = require('child_process');` at the top of your file):
```js
server.add('ou=users, o=myhost', pre, (req, res, next) => {
if (!req.dn.rdns[0].attrs.cn)
return next(new ldap.ConstraintViolationError('cn required'));
if (req.users[req.dn.rdns[0].attrs.cn.value])
return next(new ldap.EntryAlreadyExistsError(req.dn.toString()));
const entry = req.toObject().attributes;
if (entry.objectclass.indexOf('unixUser') === -1)
return next(new ldap.ConstraintViolationError('entry must be a unixUser'));
const opts = ['-m'];
if (entry.description) {
opts.push('-c');
opts.push(entry.description[0]);
}
if (entry.homedirectory) {
opts.push('-d');
opts.push(entry.homedirectory[0]);
}
if (entry.gid) {
opts.push('-g');
opts.push(entry.gid[0]);
}
if (entry.shell) {
opts.push('-s');
opts.push(entry.shell[0]);
}
if (entry.uid) {
opts.push('-u');
opts.push(entry.uid[0]);
}
opts.push(entry.cn[0]);
const useradd = spawn('useradd', opts);
const messages = [];
useradd.stdout.on('data', (data) => {
messages.push(data.toString());
});
useradd.stderr.on('data', (data) => {
messages.push(data.toString());
});
useradd.on('exit', (code) => {
if (code !== 0) {
let msg = '' + code;
if (messages.length)
msg += ': ' + messages.join();
return next(new ldap.OperationsError(msg));
}
res.end();
return next();
});
});
```
Then, you'll need to be root to have this running, so start your server with
`sudo` (or be root, whatever). Now, go ahead and create a file called
`user.ldif` with the following contents:
```shell
dn: cn=ldapjs, ou=users, o=myhost
objectClass: unixUser
cn: ldapjs
shell: /bin/bash
description: Created via ldapadd
```
Now go ahead and invoke with:
```shell
$ ldapadd -H ldap://localhost:1389 -x -D cn=root -w secret -f ./user.ldif
adding new entry "cn=ldapjs, ou=users, o=myhost"
```
Let's confirm he got added with an ldapsearch:
```shell
$ ldapsearch -H ldap://localhost:1389 -LLL -x -D cn=root -w secret -b "ou=users, o=myhost" cn=ldapjs
dn: cn=ldapjs, ou=users, o=myhost
cn: ldapjs
uid: 1001
gid: 1001
description: Created via ldapadd
homedirectory: /home/ldapjs
shell: /bin/bash
objectclass: unixUser
```
As before, here's a breakdown of the code:
```js
server.add('ou=users, o=myhost', pre, (req, res, next) => {
if (!req.dn.rdns[0].attrs.cn)
return next(new ldap.ConstraintViolationError('cn required'));
if (req.users[req.dn.rdns[0].attrs.cn.value])
return next(new ldap.EntryAlreadyExistsError(req.dn.toString()));
const entry = req.toObject().attributes;
if (entry.objectclass.indexOf('unixUser') === -1)
return next(new ldap.ConstraintViolationError('entry must be a unixUser'));
});
```
A few new things:
* We mounted this handler at `ou=users, o=myhost`. Why? What if we want to
extend this little project with groups? We probably want those under a
different part of the tree.
* We did some really minimal schema enforcement by:
+ Checking that the leaf RDN (relative distinguished name) was a _cn_
attribute.
+ We then did `req.toObject()`. As mentioned before, each of the req/res
objects have special APIs that make sense for that operation. Without getting
into the details, the LDAP add operation on the wire doesn't look like a JS
object, and we want to support both the LDAP nerd that wants to see what
got sent, and the "easy" case. So use `.toObject()`. Note we also filtered
out to the `attributes` portion of the object since that's all we're really
looking at.
+ Lastly, we did a super minimal check to see if the entry was of type
`unixUser`. Frankly for this case, it's kind of useless, but it does illustrate
one point: attribute names are case-insensitive, so ldapjs converts them all to
lower case (note the client sent _objectClass_ over the wire).
After that, we really just delegated off to the _useradd_ command. As far as I
know, there is not a node.js module that wraps up `getpwent` and friends,
otherwise we'd use that.
Now, what's missing? Oh, right, we need to let you set a password. Well, let's
support that via the _modify_ command.
## Modify
Unlike HTTP, "partial" document updates are fully specified as part of the
RFC, so appending, removing, or replacing a single attribute is pretty natural.
Go ahead and add the following code into your source file:
```js
server.modify('ou=users, o=myhost', pre, (req, res, next) => {
if (!req.dn.rdns[0].attrs.cn || !req.users[req.dn.rdns[0].attrs.cn.value])
return next(new ldap.NoSuchObjectError(req.dn.toString()));
if (!req.changes.length)
return next(new ldap.ProtocolError('changes required'));
const user = req.users[req.dn.rdns[0].attrs.cn.value].attributes;
let mod;
for (const i = 0; i < req.changes.length; i++) {
mod = req.changes[i].modification;
switch (req.changes[i].operation) {
case 'replace':
if (mod.type !== 'userpassword' || !mod.vals || !mod.vals.length)
return next(new ldap.UnwillingToPerformError('only password updates ' +
'allowed'));
break;
case 'add':
case 'delete':
return next(new ldap.UnwillingToPerformError('only replace allowed'));
}
}
const passwd = spawn('chpasswd', ['-c', 'MD5']);
passwd.stdin.end(user.cn + ':' + mod.vals[0], 'utf8');
passwd.on('exit', (code) => {
if (code !== 0)
return next(new ldap.OperationsError(code));
res.end();
return next();
});
});
```
Basically, we made sure the remote client was targeting an entry that exists,
ensuring that they were asking to "replace" the `userPassword` attribute (which
is the 'standard' LDAP attribute for passwords; if you think it's easier to use
'password', knock yourself out), and then just delegating to the `chpasswd`
command (which lets you change a user's password over stdin). Next, go ahead
and create a `passwd.ldif` file:
```shell
dn: cn=ldapjs, ou=users, o=myhost
changetype: modify
replace: userPassword
userPassword: secret
-
```
And then run the OpenLDAP CLI:
```shell
$ ldapmodify -H ldap://localhost:1389 -x -D cn=root -w secret -f ./passwd.ldif
```
You should now be able to login to your box as the ldapjs user. Let's get
the last "mainline" piece of work out of the way, and delete the user.
## Delete
Delete is pretty straightforward. The client gives you a dn to delete, and you
delete it :). Add the following code into your server:
```js
server.del('ou=users, o=myhost', pre, (req, res, next) => {
if (!req.dn.rdns[0].attrs.cn || !req.users[req.dn.rdns[0].attrs.cn.value])
return next(new ldap.NoSuchObjectError(req.dn.toString()));
const userdel = spawn('userdel', ['-f', req.dn.rdns[0].attrs.cn.value]);
const messages = [];
userdel.stdout.on('data', (data) => {
messages.push(data.toString());
});
userdel.stderr.on('data', (data) => {
messages.push(data.toString());
});
userdel.on('exit', (code) => {
if (code !== 0) {
let msg = '' + code;
if (messages.length)
msg += ': ' + messages.join();
return next(new ldap.OperationsError(msg));
}
res.end();
return next();
});
});
```
And then run the following command:
```shell
$ ldapdelete -H ldap://localhost:1389 -x -D cn=root -w secret "cn=ldapjs, ou=users, o=myhost"
```
# Where to go from here
The complete source code for this example server is available in
[examples](examples.html). Make sure to read up on the [server](server.html)
and [client](client.html) APIs. If you're looking for a "drop in" solution,
take a look at [ldapjs-riak](https://github.com/mcavage/node-ldapjs-riak).
[Mozilla](https://wiki.mozilla.org/Mozilla_LDAP_SDK_Programmer%27s_Guide/Understanding_LDAP)
still maintains some web pages with LDAP overviews if you look around, if you're
looking for more tutorials. After that, you'll need to work your way through
the [RFCs](http://tools.ietf.org/html/rfc4510) as you work through the APIs in
ldapjs.
@@ -0,0 +1,95 @@
---
title: ldapjs
---
<div id="indextagline">
Reimagining <a href="http://tools.ietf.org/html/rfc4510" id="indextaglink">LDAP</a> for <a id="indextaglink" href="http://nodejs.org">Node.js</a>
</div>
# Overview
<div class="intro">
ldapjs is a pure JavaScript, from-scratch framework for implementing
[LDAP](http://tools.ietf.org/html/rfc4510) clients and servers in
[Node.js](http://nodejs.org). It is intended for developers used to interacting
with HTTP services in node and [restify](http://restify.com).
</div>
```js
const ldap = require('ldapjs');
const server = ldap.createServer();
server.search('o=example', (req, res, next) => {
const obj = {
dn: req.dn.toString(),
attributes: {
objectclass: ['organization', 'top'],
o: 'example'
}
};
if (req.filter.matches(obj.attributes))
res.send(obj);
res.end();
});
server.listen(1389, () => {
console.log('LDAP server listening at %s', server.url);
});
```
Try hitting that with:
```shell
$ ldapsearch -H ldap://localhost:1389 -x -b o=example objectclass=*
```
# Features
ldapjs implements most of the common operations in the LDAP v3 RFC(s), for
both client and server. It is 100% wire-compatible with the LDAP protocol
itself, and is interoperable with [OpenLDAP](http://openldap.org) and any other
LDAPv3-compliant implementation. ldapjs gives you a powerful routing and
"intercepting filter" pattern for implementing server(s). It is intended
that you can build LDAP over anything you want, not just traditional databases.
# Getting started
```shell
$ npm install ldapjs
```
If you're new to LDAP, check out the [guide](guide.html). Otherwise, the
API documentation is:
|Section |Content |
|---------------------------|-------------------------------------------|
|[Server API](server.html) |Reference for implementing LDAP servers. |
|[Client API](client.html) |Reference for implementing LDAP clients. |
|[DN API](dn.html) |API reference for the DN class. |
|[Filter API](filters.html) |API reference for LDAP search filters. |
|[Error API](errors.html) |Listing of all ldapjs Error objects. |
|[Examples](examples.html) |Collection of sample/getting started code. |
# More information
- License:[MIT](http://opensource.org/licenses/mit-license.php)
- Code: [ldapjs/node-ldapjs](https://github.com/ldapjs/node-ldapjs)
# What's not in the box?
Since most developers and system(s) adminstrators struggle with some of the
esoteric features of LDAP, not all features in LDAP are implemented here.
Specifically:
* LDIF
* Aliases
* Attributes by OID
* Extensible matching
There are a few others, but those are the "big" ones.
@@ -0,0 +1,607 @@
---
title: Server API | ldapjs
---
# ldapjs Server API
<div class="intro">
This document covers the ldapjs server API and assumes that you are familiar
with LDAP. If you're not, read the [guide](guide.html) first.
</div>
# Create a server
The code to create a new server looks like:
```js
const server = ldap.createServer();
```
The full list of options is:
||log||You can optionally pass in a Bunyan compatible logger instance the client will use to acquire a child logger.||
||certificate||A PEM-encoded X.509 certificate; will cause this server to run in TLS mode.||
||key||A PEM-encoded private key that corresponds to _certificate_ for SSL.||
### Note On Logger
The passed in logger is expected to conform to the Log4j standard API.
Internally, [abstract-logging](https://www.npmjs.com/packages/abstract-logging) is
used to implement the interface. As a result, no log messages will be generated
unless an external logger is supplied.
Known compatible loggers are:
+ [Bunyan](https://www.npmjs.com/package/bunyan)
+ [Pino](https://www.npmjs.com/package/pino)
## Properties on the server object
### maxConnections
Set this property to reject connections when the server's connection count gets
high.
### connections (getter only) - DEPRECATED
The number of concurrent connections on the server. This property is deprecated,
please use server.getConnections() instead.
### url
Returns the fully qualified URL this server is listening on. For example:
`ldaps://10.1.2.3:1636`. If you haven't yet called `listen`, it will always
return `ldap://localhost:389`.
### Event: 'close'
`function() {}`
Emitted when the server closes.
## Listening for requests
The LDAP server API wraps up and mirrors the node.js `server.listen` family of
APIs.
After calling `listen`, the property `url` on the server object itself will be
available.
Example:
```js
server.listen(389, '127.0.0.1', function() {
console.log('LDAP server listening at: ' + server.url);
});
```
### Port and Host
`listen(port, [host], [callback])`
Begin accepting connections on the specified port and host. If the host is
omitted, the server will accept connections directed to any IPv4 address
(INADDR\_ANY).
This function is asynchronous. The last parameter callback will be called when
the server has been bound.
### Unix Domain Socket
`listen(path, [callback])`
Start a UNIX socket server listening for connections on the given path.
This function is asynchronous. The last parameter callback will be called when
the server has been bound.
### File descriptor
`listenFD(fd)`
Start a server listening for connections on the given file descriptor.
This file descriptor must have already had the `bind(2)` and `listen(2)` system
calls invoked on it. Additionally, it must be set non-blocking; try
`fcntl(fd, F_SETFL, O_NONBLOCK)`.
## Inspecting server state
### server.getConnections(callback)
The LDAP server API mirrors the [Node.js `server.getConnections` API](https://nodejs.org/dist/latest-v12.x/docs/api/net.html#net_server_getconnections_callback). Callback
should take two arguments err and count.
# Routes
The LDAP server API is meant to be the LDAP-equivalent of the express/restify
paradigm of programming. Essentially every method is of the form
`OP(req, res, next)` where OP is one of bind, add, del, etc. You can chain
handlers together by calling `next()` and ordering your functions in the
definition of the route. For example:
```js
function authorize(req, res, next) {
if (!req.connection.ldap.bindDN.equals('cn=root'))
return next(new ldap.InsufficientAccessRightsError());
return next();
}
server.search('o=example', authorize, function(req, res, next) { ... });
```
Note that ldapjs is also slightly different, since it's often going to be backed
to a DB-like entity, in that it also has an API where you can pass in a
'backend' object. This is necessary if there are persistent connection pools,
caching, etc. that need to be placed in an object.
For example [ldapjs-riak](https://github.com/mcavage/node-ldapjs-riak) is a
complete implementation of the LDAP protocol over
[Riak](https://github.com/basho/riak). Getting an LDAP server up with riak
looks like:
```js
const ldap = require('ldapjs');
const ldapRiak = require('ldapjs-riak');
const server = ldap.createServer();
const backend = ldapRiak.createBackend({
"host": "localhost",
"port": 8098,
"bucket": "example",
"indexes": ["l", "cn"],
"uniqueIndexes": ["uid"],
"numConnections": 5
});
server.add("o=example",
backend,
backend.add());
...
```
The first parameter to an ldapjs route is always the point in the
tree to mount the handler chain at. The second argument is _optionally_ a
backend object. After that you can pass in an arbitrary combination of
functions in the form `f(req, res, next)` or arrays of functions of the same
signature (ldapjs will unroll them).
Unlike HTTP, LDAP operations do not have a heterogeneous wire format, so each
operation requires specific methods/fields on the request/response
objects. However, there is a `.use()` method availabe, similar to
that on express/connect, allowing you to chain up "middleware":
```js
server.use(function(req, res, next) {
console.log('hello world');
return next();
});
```
## Common Request Elements
All request objects have the `dn` getter on it, which is "context-sensitive"
and returns the point in the tree that the operation wants to operate on. The
LDAP protocol itself sadly doesn't define operations this way, and has a unique
name for just about every op. So, ldapjs calls it `dn`. The DN object itself
is documented at [DN](dn.html).
All requests have an optional array of `Control` objects. `Control` will have
the properties `type` (string), `criticality` (boolean), and optionally, a
string `value`.
All request objects will have a `connection` object, which is the `net.Socket`
associated to this request. Off the `connection` object is an `ldap` object.
The most important property to pay attention to is the `bindDN` property
which will be an instance of an `ldap.DN` object. This is what the client
authenticated as on this connection. If the client didn't bind, then a DN object
will be there defaulted to `cn=anonymous`.
Additionally, request will have a `logId` parameter you can use to uniquely
identify the request/connection pair in logs (includes the LDAP messageID).
## Common Response Elements
All response objects will have an `end` method on them. By default, calling
`res.end()` with no arguments will return SUCCESS (0x00) to the client
(with the exception of `compare` which will return COMPARE\_TRUE (0x06)). You
can pass in a status code to the `end()` method to return an alternate status
code.
However, it's more common/easier to use the `return next(new LDAPError())`
pattern, since ldapjs will fill in the extra LDAPResult fields like matchedDN
and error message for you.
## Errors
ldapjs includes an exception hierarchy that directly corresponds to the RFC list
of error codes. The complete list is documented in [errors](errors.html). But
the paradigm is something defined like CONSTRAINT\_VIOLATION in the RFC would be
`ConstraintViolationError` in ldapjs. Upon calling `next(new LDAPError())`,
ldapjs will _stop_ calling your handler chain. For example:
```js
server.search('o=example',
(req, res, next) => { return next(); },
(req, res, next) => { return next(new ldap.OperationsError()); },
(req, res, next) => { res.end(); }
);
```
In the code snipped above, the third handler would never get invoked.
# Bind
Adds a mount in the tree to perform LDAP binds with. Example:
```js
server.bind('ou=people, o=example', (req, res, next) => {
console.log('bind DN: ' + req.dn.toString());
console.log('bind PW: ' + req.credentials);
res.end();
});
```
## BindRequest
BindRequest objects have the following properties:
### version
The LDAP protocol version the client is requesting to run this connection on.
Note that ldapjs only supports LDAP version 3.
### name
The DN the client is attempting to bind as (note this is the same as the `dn`
property).
### authentication
The method of authentication. Right now only `simple` is supported.
### credentials
The credentials to go with the `name/authentication` pair. For `simple`, this
will be the plain-text password.
## BindResponse
No extra methods above an `LDAPResult` API call.
# Add
Adds a mount in the tree to perform LDAP adds with.
```js
server.add('ou=people, o=example', (req, res, next) => {
console.log('DN: ' + req.dn.toString());
console.log('Entry attributes: ' + req.toObject().attributes);
res.end();
});
```
## AddRequest
AddRequest objects have the following properties:
### entry
The DN the client is attempting to add (this is the same as the `dn`
property).
### attributes
The set of attributes in this entry. This will be an array of
`Attribute` objects (which have a type and an array of values). This directly
maps to how the request came in off the wire. It's likely you'll want to use
`toObject()` and iterate that way, since that will transform an AddRequest into
a standard JavaScript object.
### toObject()
This operation will return a plain JavaScript object from the request that looks
like:
```js
{
dn: 'cn=foo, o=example', // string, not DN object
attributes: {
cn: ['foo'],
sn: ['bar'],
objectclass: ['person', 'top']
}
}
```
## AddResponse
No extra methods above an `LDAPResult` API call.
# Search
Adds a handler for the LDAP search operation.
```js
server.search('o=example', (req, res, next) => {
console.log('base object: ' + req.dn.toString());
console.log('scope: ' + req.scope);
console.log('filter: ' + req.filter.toString());
res.end();
});
```
## SearchRequest
SearchRequest objects have the following properties:
### baseObject
The DN the client is attempting to start the search at (equivalent to `dn`).
### scope
(string) one of:
* base
* one
* sub
### derefAliases
An integer (defined in the LDAP protocol). Defaults to '0' (meaning
never deref).
### sizeLimit
The number of entries to return. Defaults to '0' (unlimited). ldapjs doesn't
currently automatically enforce this, but probably will at some point.
### timeLimit
Maximum amount of time the server should take in sending search entries.
Defaults to '0' (unlimited).
### typesOnly
Whether to return only the names of attributes, and not the values. Defaults to
'false'. ldapjs will take care of this for you.
### filter
The [filter](filters.html) object that the client requested. Notably this has
a `matches()` method on it that you can leverage. For an example of
introspecting a filter, take a look at the ldapjs-riak source.
### attributes
An optional list of attributes to restrict the returned result sets to. ldapjs
will automatically handle this for you.
## SearchResponse
### send(entry)
Allows you to send a `SearchEntry` object. You do not need to
explicitly pass in a `SearchEntry` object, and can instead just send a plain
JavaScript object that matches the format used from `AddRequest.toObject()`.
```js
server.search('o=example', (req, res, next) => {
const obj = {
dn: 'o=example',
attributes: {
objectclass: ['top', 'organization'],
o: ['example']
}
};
if (req.filter.matches(obj))
res.send(obj)
res.end();
});
```
# modify
Allows you to handle an LDAP modify operation.
```js
server.modify('o=example', (req, res, next) => {
console.log('DN: ' + req.dn.toString());
console.log('changes:');
for (const c of req.changes) {
console.log(' operation: ' + c.operation);
console.log(' modification: ' + c.modification.toString());
}
res.end();
});
```
## ModifyRequest
ModifyRequest objects have the following properties:
### object
The DN the client is attempting to update (this is the same as the `dn`
property).
### changes
An array of `Change` objects the client is attempting to perform. See below for
details on the `Change` object.
## Change
The `Change` object will have the following properties:
### operation
A string, and will be one of: 'add', 'delete', or 'replace'.
### modification
Will be an `Attribute` object, which will have a 'type' (string) field, and
'vals', which will be an array of string values.
## ModifyResponse
No extra methods above an `LDAPResult` API call.
# del
Allows you to handle an LDAP delete operation.
```js
server.del('o=example', (req, res, next) => {
console.log('DN: ' + req.dn.toString());
res.end();
});
```
## DeleteRequest
### entry
The DN the client is attempting to delete (this is the same as the `dn`
property).
## DeleteResponse
No extra methods above an `LDAPResult` API call.
# compare
Allows you to handle an LDAP compare operation.
```js
server.compare('o=example', (req, res, next) => {
console.log('DN: ' + req.dn.toString());
console.log('attribute name: ' + req.attribute);
console.log('attribute value: ' + req.value);
res.end(req.value === 'foo');
});
```
## CompareRequest
### entry
The DN the client is attempting to compare (this is the same as the `dn`
property).
### attribute
The string name of the attribute to compare values of.
### value
The string value of the attribute to compare.
## CompareResponse
The `end()` method for compare takes a boolean, as opposed to a numeric code
(you can still pass in a numeric LDAP status code if you want). Beyond
that, there are no extra methods above an `LDAPResult` API call.
# modifyDN
Allows you to handle an LDAP modifyDN operation.
```js
server.modifyDN('o=example', (req, res, next) => {
console.log('DN: ' + req.dn.toString());
console.log('new RDN: ' + req.newRdn.toString());
console.log('deleteOldRDN: ' + req.deleteOldRdn);
console.log('new superior: ' +
(req.newSuperior ? req.newSuperior.toString() : ''));
res.end();
});
```
## ModifyDNRequest
### entry
The DN the client is attempting to rename (this is the same as the `dn`
property).
### newRdn
The leaf RDN the client wants to rename this entry to. This will be a DN object.
### deleteOldRdn
Whether or not to delete the old RDN (i.e., rename vs copy). Defaults to 'true'.
### newSuperior
Optional (DN). If the modifyDN operation wishes to relocate the entry in the
tree, the `newSuperior` field will contain the new parent.
## ModifyDNResponse
No extra methods above an `LDAPResult` API call.
# exop
Allows you to handle an LDAP extended operation. Extended operations are pretty
much arbitrary extensions, by definition. Typically the extended 'name' is an
OID, but ldapjs makes no such restrictions; it just needs to be a string.
Unlike the other operations, extended operations don't map to any location in
the tree, so routing here will be exact match, as opposed to subtree.
```js
// LDAP whoami
server.exop('1.3.6.1.4.1.4203.1.11.3', (req, res, next) => {
console.log('name: ' + req.name);
console.log('value: ' + req.value);
res.value = 'u:xxyyz@EXAMPLE.NET';
res.end();
return next();
});
```
## ExtendedRequest
### name
Will always be a match to the route-defined name. Clients must include this
in their requests.
### value
Optional string. The arbitrary blob the client sends for this extended
operation.
## ExtendedResponse
### name
The name of the extended operation. ldapjs will automatically set this.
### value
The arbitrary (string) value to send back as part of the response.
# unbind
ldapjs by default provides an unbind handler that just disconnects the client
and cleans up any internals (in ldapjs core). You can override this handler
if you need to clean up any items in your backend, or perform any other cleanup
tasks you need to.
```js
server.unbind((req, res, next) => {
res.end();
});
```
Note that the LDAP unbind operation actually doesn't send any response (by
definition in the RFC), so the UnbindResponse is really just a stub that
ultimately calls `net.Socket.end()` for you. There are no properties available
on either the request or response objects, except, of course, for `end()` on the
response.
@@ -0,0 +1,65 @@
const cluster = require('cluster')
const ldap = require('ldapjs')
const net = require('net')
const os = require('os')
const threads = []
threads.getNext = function () {
return (Math.floor(Math.random() * this.length))
}
const serverOptions = {
port: 1389
}
if (cluster.isMaster) {
const server = net.createServer(serverOptions, (socket) => {
socket.pause()
console.log('ldapjs client requesting connection')
const routeTo = threads.getNext()
threads[routeTo].send({ type: 'connection' }, socket)
})
for (let i = 0; i < os.cpus().length; i++) {
const thread = cluster.fork({
id: i
})
thread.id = i
thread.on('message', function () {
})
threads.push(thread)
}
server.listen(serverOptions.port, function () {
console.log('ldapjs listening at ldap://127.0.0.1:' + serverOptions.port)
})
} else {
const server = ldap.createServer(serverOptions)
const threadId = process.env.id
process.on('message', (msg, socket) => {
switch (msg.type) {
case 'connection':
server.newConnection(socket)
socket.resume()
console.log('ldapjs client connection accepted on ' + threadId.toString())
}
})
server.search('dc=example', function (req, res) {
console.log('ldapjs search initiated on ' + threadId.toString())
const obj = {
dn: req.dn.toString(),
attributes: {
objectclass: ['organization', 'top'],
o: 'example'
}
}
if (req.filter.matches(obj.attributes)) { res.send(obj) }
res.end()
})
}
@@ -0,0 +1,65 @@
const cluster = require('cluster')
const ldap = require('ldapjs')
const os = require('os')
const threads = []
threads.getNext = function () {
return (Math.floor(Math.random() * this.length))
}
const serverOptions = {
connectionRouter: (socket) => {
socket.pause()
console.log('ldapjs client requesting connection')
const routeTo = threads.getNext()
threads[routeTo].send({ type: 'connection' }, socket)
}
}
const server = ldap.createServer(serverOptions)
if (cluster.isMaster) {
for (let i = 0; i < os.cpus().length; i++) {
const thread = cluster.fork({
id: i
})
thread.id = i
thread.on('message', function () {
})
threads.push(thread)
}
server.listen(1389, function () {
console.log('ldapjs listening at ' + server.url)
})
} else {
const threadId = process.env.id
serverOptions.connectionRouter = () => {
console.log('should not be hit')
}
process.on('message', (msg, socket) => {
switch (msg.type) {
case 'connection':
server.newConnection(socket)
socket.resume()
console.log('ldapjs client connection accepted on ' + threadId.toString())
}
})
server.search('dc=example', function (req, res) {
console.log('ldapjs search initiated on ' + threadId.toString())
const obj = {
dn: req.dn.toString(),
attributes: {
objectclass: ['organization', 'top'],
o: 'example'
}
}
if (req.filter.matches(obj.attributes)) { res.send(obj) }
res.end()
})
}
@@ -0,0 +1,177 @@
const ldap = require('../lib/index')
/// --- Shared handlers
function authorize (req, res, next) {
/* Any user may search after bind, only cn=root has full power */
const isSearch = (req instanceof ldap.SearchRequest)
if (!req.connection.ldap.bindDN.equals('cn=root') && !isSearch) { return next(new ldap.InsufficientAccessRightsError()) }
return next()
}
/// --- Globals
const SUFFIX = 'o=smartdc'
const db = {}
const server = ldap.createServer()
server.bind('cn=root', function (req, res, next) {
if (req.dn.toString() !== 'cn=root' || req.credentials !== 'secret') { return next(new ldap.InvalidCredentialsError()) }
res.end()
return next()
})
server.add(SUFFIX, authorize, function (req, res, next) {
const dn = req.dn.toString()
if (db[dn]) { return next(new ldap.EntryAlreadyExistsError(dn)) }
db[dn] = req.toObject().attributes
res.end()
return next()
})
server.bind(SUFFIX, function (req, res, next) {
const dn = req.dn.toString()
if (!db[dn]) { return next(new ldap.NoSuchObjectError(dn)) }
if (!db[dn].userpassword) { return next(new ldap.NoSuchAttributeError('userPassword')) }
if (db[dn].userpassword.indexOf(req.credentials) === -1) { return next(new ldap.InvalidCredentialsError()) }
res.end()
return next()
})
server.compare(SUFFIX, authorize, function (req, res, next) {
const dn = req.dn.toString()
if (!db[dn]) { return next(new ldap.NoSuchObjectError(dn)) }
if (!db[dn][req.attribute]) { return next(new ldap.NoSuchAttributeError(req.attribute)) }
let matches = false
const vals = db[dn][req.attribute]
for (let i = 0; i < vals.length; i++) {
if (vals[i] === req.value) {
matches = true
break
}
}
res.end(matches)
return next()
})
server.del(SUFFIX, authorize, function (req, res, next) {
const dn = req.dn.toString()
if (!db[dn]) { return next(new ldap.NoSuchObjectError(dn)) }
delete db[dn]
res.end()
return next()
})
server.modify(SUFFIX, authorize, function (req, res, next) {
const dn = req.dn.toString()
if (!req.changes.length) { return next(new ldap.ProtocolError('changes required')) }
if (!db[dn]) { return next(new ldap.NoSuchObjectError(dn)) }
const entry = db[dn]
let mod
for (let i = 0; i < req.changes.length; i++) {
mod = req.changes[i].modification
switch (req.changes[i].operation) {
case 'replace':
if (!entry[mod.type]) { return next(new ldap.NoSuchAttributeError(mod.type)) }
if (!mod.vals || !mod.vals.length) {
delete entry[mod.type]
} else {
entry[mod.type] = mod.vals
}
break
case 'add':
if (!entry[mod.type]) {
entry[mod.type] = mod.vals
} else {
mod.vals.forEach(function (v) {
if (entry[mod.type].indexOf(v) === -1) { entry[mod.type].push(v) }
})
}
break
case 'delete':
if (!entry[mod.type]) { return next(new ldap.NoSuchAttributeError(mod.type)) }
delete entry[mod.type]
break
}
}
res.end()
return next()
})
server.search(SUFFIX, authorize, function (req, res, next) {
const dn = req.dn.toString()
if (!db[dn]) { return next(new ldap.NoSuchObjectError(dn)) }
let scopeCheck
switch (req.scope) {
case 'base':
if (req.filter.matches(db[dn])) {
res.send({
dn: dn,
attributes: db[dn]
})
}
res.end()
return next()
case 'one':
scopeCheck = function (k) {
if (req.dn.equals(k)) { return true }
const parent = ldap.parseDN(k).parent()
return (parent ? parent.equals(req.dn) : false)
}
break
case 'sub':
scopeCheck = function (k) {
return (req.dn.equals(k) || req.dn.parentOf(k))
}
break
}
Object.keys(db).forEach(function (key) {
if (!scopeCheck(key)) { return }
if (req.filter.matches(db[key])) {
res.send({
dn: key,
attributes: db[key]
})
}
})
res.end()
return next()
})
/// --- Fire it up
server.listen(1389, function () {
console.log('LDAP server up at: %s', server.url)
})
@@ -0,0 +1,24 @@
#!/usr/sbin/dtrace -s
#pragma D option quiet
BEGIN
{
printf("%-8s %-8s %-16s %-15s %-15s %s\n",
"LATENCY", "OPTYPE", "REMOTE IP", "BIND DN", "REQ DN",
"STATUS");
}
ldapjs*:::server-*-start
{
starts[arg0] = timestamp;
}
ldapjs*:::server-*-done
/starts[arg0]/
{
printf("%6dms %-8s %-16s %-15s %-15s %d\n",
(timestamp - starts[arg0]) / 1000000, strtok(probename + 7, "-"),
copyinstr(arg1), copyinstr(arg2), copyinstr(arg3), arg4);
starts[arg0] = 0;
}
@@ -0,0 +1,54 @@
// Copyright 2015 Joyent, Inc.
const assert = require('assert')
const util = require('util')
const isDN = require('./dn').DN.isDN
const isAttribute = require('./attribute').isAttribute
/// --- Helpers
// Copied from mcavage/node-assert-plus
function _assert (arg, type, name) {
name = name || type
throw new assert.AssertionError({
message: util.format('%s (%s) required', name, type),
actual: typeof (arg),
expected: type,
operator: '===',
stackStartFunction: _assert.caller
})
}
/// --- API
function stringDN (input, name) {
if (isDN(input) || typeof (input) === 'string') { return }
_assert(input, 'DN or string', name)
}
function optionalStringDN (input, name) {
if (input === undefined || isDN(input) || typeof (input) === 'string') { return }
_assert(input, 'DN or string', name)
}
function optionalDN (input, name) {
if (input !== undefined && !isDN(input)) { _assert(input, 'DN', name) }
}
function optionalArrayOfAttribute (input, name) {
if (input === undefined) { return }
if (!Array.isArray(input) ||
input.some(function (v) { return !isAttribute(v) })) {
_assert(input, 'array of Attribute', name)
}
}
/// --- Exports
module.exports = {
stringDN: stringDN,
optionalStringDN: optionalStringDN,
optionalDN: optionalDN,
optionalArrayOfAttribute: optionalArrayOfAttribute
}
@@ -0,0 +1,160 @@
// Copyright 2011 Mark Cavage, Inc. All rights reserved.
const assert = require('assert')
const asn1 = require('asn1')
const Protocol = require('./protocol')
/// --- API
function Attribute (options) {
if (options) {
if (typeof (options) !== 'object') { throw new TypeError('options must be an object') }
if (options.type && typeof (options.type) !== 'string') { throw new TypeError('options.type must be a string') }
} else {
options = {}
}
this.type = options.type || ''
this._vals = []
if (options.vals !== undefined && options.vals !== null) { this.vals = options.vals }
}
module.exports = Attribute
Object.defineProperties(Attribute.prototype, {
buffers: {
get: function getBuffers () {
return this._vals
},
configurable: false
},
json: {
get: function getJson () {
return {
type: this.type,
vals: this.vals
}
},
configurable: false
},
vals: {
get: function getVals () {
const eType = _bufferEncoding(this.type)
return this._vals.map(function (v) {
return v.toString(eType)
})
},
set: function setVals (vals) {
const self = this
this._vals = []
if (Array.isArray(vals)) {
vals.forEach(function (v) {
self.addValue(v)
})
} else {
self.addValue(vals)
}
},
configurable: false
}
})
Attribute.prototype.addValue = function addValue (val) {
if (Buffer.isBuffer(val)) {
this._vals.push(val)
} else {
this._vals.push(Buffer.from(val + '', _bufferEncoding(this.type)))
}
}
/* BEGIN JSSTYLED */
Attribute.compare = function compare (a, b) {
if (!(Attribute.isAttribute(a)) || !(Attribute.isAttribute(b))) {
throw new TypeError('can only compare Attributes')
}
if (a.type < b.type) return -1
if (a.type > b.type) return 1
if (a.vals.length < b.vals.length) return -1
if (a.vals.length > b.vals.length) return 1
for (let i = 0; i < a.vals.length; i++) {
if (a.vals[i] < b.vals[i]) return -1
if (a.vals[i] > b.vals[i]) return 1
}
return 0
}
/* END JSSTYLED */
Attribute.prototype.parse = function parse (ber) {
assert.ok(ber)
ber.readSequence()
this.type = ber.readString()
if (ber.peek() === Protocol.LBER_SET) {
if (ber.readSequence(Protocol.LBER_SET)) {
const end = ber.offset + ber.length
while (ber.offset < end) { this._vals.push(ber.readString(asn1.Ber.OctetString, true)) }
}
}
return true
}
Attribute.prototype.toBer = function toBer (ber) {
assert.ok(ber)
ber.startSequence()
ber.writeString(this.type)
ber.startSequence(Protocol.LBER_SET)
if (this._vals.length) {
this._vals.forEach(function (b) {
ber.writeByte(asn1.Ber.OctetString)
ber.writeLength(b.length)
for (let i = 0; i < b.length; i++) { ber.writeByte(b[i]) }
})
} else {
ber.writeStringArray([])
}
ber.endSequence()
ber.endSequence()
return ber
}
Attribute.prototype.toString = function () {
return JSON.stringify(this.json)
}
Attribute.toBer = function (attr, ber) {
return Attribute.prototype.toBer.call(attr, ber)
}
Attribute.isAttribute = function isAttribute (attr) {
if (!attr || typeof (attr) !== 'object') {
return false
}
if (attr instanceof Attribute) {
return true
}
if ((typeof (attr.toBer) === 'function') &&
(typeof (attr.type) === 'string') &&
(Array.isArray(attr.vals)) &&
(attr.vals.filter(function (item) {
return (typeof (item) === 'string' ||
Buffer.isBuffer(item))
}).length === attr.vals.length)) {
return true
}
return false
}
function _bufferEncoding (type) {
/* JSSTYLED */
return /;binary$/.test(type) ? 'base64' : 'utf8'
}
@@ -0,0 +1,213 @@
// Copyright 2011 Mark Cavage, Inc. All rights reserved.
const assert = require('assert-plus')
const Attribute = require('./attribute')
// var Protocol = require('./protocol')
/// --- API
function Change (options) {
if (options) {
assert.object(options)
assert.optionalString(options.operation)
} else {
options = {}
}
this._modification = false
this.operation = options.operation || options.type || 'add'
this.modification = options.modification || {}
}
Object.defineProperties(Change.prototype, {
operation: {
get: function getOperation () {
switch (this._operation) {
case 0x00: return 'add'
case 0x01: return 'delete'
case 0x02: return 'replace'
default:
throw new Error('0x' + this._operation.toString(16) + ' is invalid')
}
},
set: function setOperation (val) {
assert.string(val)
switch (val.toLowerCase()) {
case 'add':
this._operation = 0x00
break
case 'delete':
this._operation = 0x01
break
case 'replace':
this._operation = 0x02
break
default:
throw new Error('Invalid operation type: 0x' + val.toString(16))
}
},
configurable: false
},
modification: {
get: function getModification () {
return this._modification
},
set: function setModification (val) {
if (Attribute.isAttribute(val)) {
this._modification = val
return
}
// Does it have an attribute-like structure
if (Object.keys(val).length === 2 &&
typeof (val.type) === 'string' &&
Array.isArray(val.vals)) {
this._modification = new Attribute({
type: val.type,
vals: val.vals
})
return
}
const keys = Object.keys(val)
if (keys.length > 1) {
throw new Error('Only one attribute per Change allowed')
} else if (keys.length === 0) {
return
}
const k = keys[0]
const _attr = new Attribute({ type: k })
if (Array.isArray(val[k])) {
val[k].forEach(function (v) {
_attr.addValue(v.toString())
})
} else if (Buffer.isBuffer(val[k])) {
_attr.addValue(val[k])
} else if (val[k] !== undefined && val[k] !== null) {
_attr.addValue(val[k].toString())
}
this._modification = _attr
},
configurable: false
},
json: {
get: function getJSON () {
return {
operation: this.operation,
modification: this._modification ? this._modification.json : {}
}
},
configurable: false
}
})
Change.isChange = function isChange (change) {
if (!change || typeof (change) !== 'object') {
return false
}
if ((change instanceof Change) ||
((typeof (change.toBer) === 'function') &&
(change.modification !== undefined) &&
(change.operation !== undefined))) {
return true
}
return false
}
Change.compare = function (a, b) {
if (!Change.isChange(a) || !Change.isChange(b)) { throw new TypeError('can only compare Changes') }
if (a.operation < b.operation) { return -1 }
if (a.operation > b.operation) { return 1 }
return Attribute.compare(a.modification, b.modification)
}
/**
* Apply a Change to properties of an object.
*
* @param {Object} change the change to apply.
* @param {Object} obj the object to apply it to.
* @param {Boolean} scalar convert single-item arrays to scalars. Default: false
*/
Change.apply = function apply (change, obj, scalar) {
assert.string(change.operation)
assert.string(change.modification.type)
assert.ok(Array.isArray(change.modification.vals))
assert.object(obj)
const type = change.modification.type
const vals = change.modification.vals
let data = obj[type]
if (data !== undefined) {
if (!Array.isArray(data)) {
data = [data]
}
} else {
data = []
}
switch (change.operation) {
case 'replace':
if (vals.length === 0) {
// replace empty is a delete
delete obj[type]
return obj
} else {
data = vals
}
break
case 'add': {
// add only new unique entries
const newValues = vals.filter(function (entry) {
return (data.indexOf(entry) === -1)
})
data = data.concat(newValues)
break
}
case 'delete':
data = data.filter(function (entry) {
return (vals.indexOf(entry) === -1)
})
if (data.length === 0) {
// Erase the attribute if empty
delete obj[type]
return obj
}
break
default:
break
}
if (scalar && data.length === 1) {
// store single-value outputs as scalars, if requested
obj[type] = data[0]
} else {
obj[type] = data
}
return obj
}
Change.prototype.parse = function (ber) {
assert.ok(ber)
ber.readSequence()
this._operation = ber.readEnumeration()
this._modification = new Attribute()
this._modification.parse(ber)
return true
}
Change.prototype.toBer = function (ber) {
assert.ok(ber)
ber.startSequence()
ber.writeEnumeration(this._operation)
ber = this._modification.toBer(ber)
ber.endSequence()
return ber
}
/// --- Exports
module.exports = Change
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,7 @@
'use strict'
module.exports = {
// https://tools.ietf.org/html/rfc4511#section-4.1.1
// Message identifiers are an integer between (0, maxint).
MAX_MSGID: Math.pow(2, 31) - 1
}
@@ -0,0 +1,23 @@
'use strict'
const logger = require('../logger')
const Client = require('./client')
module.exports = {
Client: Client,
createClient: function createClient (options) {
if (isObject(options) === false) throw TypeError('options (object) required')
if (options.url && typeof options.url !== 'string' && !Array.isArray(options.url)) throw TypeError('options.url (string|array) required')
if (options.socketPath && typeof options.socketPath !== 'string') throw TypeError('options.socketPath must be a string')
if ((options.url && options.socketPath) || !(options.url || options.socketPath)) throw TypeError('options.url ^ options.socketPath (String) required')
if (!options.log) options.log = logger
if (isObject(options.log) !== true) throw TypeError('options.log must be an object')
if (!options.log.child) options.log.child = function () { return options.log }
return new Client(options)
}
}
function isObject (input) {
return Object.prototype.toString.apply(input) === '[object Object]'
}
@@ -0,0 +1,25 @@
'use strict'
const { MAX_MSGID } = require('../constants')
/**
* Compare a reference id with another id to determine "greater than or equal"
* between the two values according to a sliding window.
*
* @param {integer} ref
* @param {integer} comp
*
* @returns {boolean} `true` if the `comp` value is >= to the `ref` value
* within the computed window, otherwise `false`.
*/
module.exports = function geWindow (ref, comp) {
let max = ref + Math.floor(MAX_MSGID / 2)
const min = ref
if (max >= MAX_MSGID) {
// Handle roll-over
max = max - MAX_MSGID - 1
return ((comp <= max) || (comp >= min))
} else {
return ((comp <= max) && (comp >= min))
}
}
@@ -0,0 +1,23 @@
'use strict'
const { MAX_MSGID } = require('../constants')
/**
* Returns a function that generates message identifiers. According to RFC 4511
* the identifers should be `(0, MAX_MSGID)`. The returned function handles
* this and wraps around when the maximum has been reached.
*
* @param {integer} [start=0] Starting number in the identifier sequence.
*
* @returns {function} This function accepts no parameters and returns an
* increasing sequence identifier each invocation until it reaches the maximum
* identifier. At this point the sequence starts over.
*/
module.exports = function idGeneratorFactory (start = 0) {
let currentID = start
return function nextID () {
const id = currentID + 1
currentID = (id >= MAX_MSGID) ? 1 : id
return currentID
}
}
@@ -0,0 +1,151 @@
'use strict'
const idGeneratorFactory = require('./id-generator')
const purgeAbandoned = require('./purge-abandoned')
/**
* Returns a message tracker object that keeps track of which message
* identifiers correspond to which message handlers. Also handles keeping track
* of abandoned messages.
*
* @param {object} options
* @param {string} options.id An identifier for the tracker.
* @param {object} options.parser An object that will be used to parse messages.
*
* @returns {MessageTracker}
*/
module.exports = function messageTrackerFactory (options) {
if (Object.prototype.toString.call(options) !== '[object Object]') {
throw Error('options object is required')
}
if (!options.id || typeof options.id !== 'string') {
throw Error('options.id string is required')
}
if (!options.parser || Object.prototype.toString.call(options.parser) !== '[object Object]') {
throw Error('options.parser object is required')
}
let currentID = 0
const nextID = idGeneratorFactory()
const messages = new Map()
const abandoned = new Map()
/**
* @typedef {object} MessageTracker
* @property {string} id The identifier of the tracker as supplied via the options.
* @property {object} parser The parser object given by the the options.
*/
const tracker = {
id: options.id,
parser: options.parser
}
/**
* Count of messages awaiting response.
*
* @alias pending
* @memberof! MessageTracker#
*/
Object.defineProperty(tracker, 'pending', {
get () {
return messages.size
}
})
/**
* Move a specific message to the abanded track.
*
* @param {integer} msgID The identifier for the message to move.
*
* @memberof MessageTracker
* @method abandon
*/
tracker.abandon = function abandonMessage (msgID) {
if (messages.has(msgID) === false) return false
abandoned.set(msgID, {
age: currentID,
cb: messages.get(msgID)
})
return messages.delete(msgID)
}
/**
* Retrieves the message handler for a message. Removes abandoned messages
* that have been given time to be resolved.
*
* @param {integer} msgID The identifier for the message to get the handler for.
*
* @memberof MessageTracker
* @method fetch
*/
tracker.fetch = function fetchMessage (msgID) {
const messageCB = messages.get(msgID)
if (messageCB) {
purgeAbandoned(msgID, abandoned)
return messageCB
}
// We sent an abandon request but the server either wasn't able to process
// it or has not received it yet. Therefore, we received a response for the
// abandoned message. So we must return the abandoned message's callback
// to be processed normally.
const abandonedMsg = abandoned.get(msgID)
if (abandonedMsg) {
return abandonedMsg.cb
}
return null
}
/**
* Removes all message tracks, cleans up the abandoned track, and invokes
* a callback for each message purged.
*
* @param {function} cb A function with the signature `(msgID, handler)`.
*
* @memberof MessageTracker
* @method purge
*/
tracker.purge = function purgeMessages (cb) {
messages.forEach((val, key) => {
purgeAbandoned(key, abandoned)
tracker.remove(key)
cb(key, val)
})
}
/**
* Removes a message from all tracking.
*
* @param {integer} msgID The identifier for the message to remove from tracking.
*
* @memberof MessageTracker
* @method remove
*/
tracker.remove = function removeMessage (msgID) {
if (messages.delete(msgID) === false) {
abandoned.delete(msgID)
}
}
/**
* Add a message handler to be tracked.
*
* @param {object} message The message object to be tracked. This object will
* have a new property added to it: `messageID`.
* @param {function} callback The handler for the message.
*
* @memberof MessageTracker
* @method track
*/
tracker.track = function trackMessage (message, callback) {
currentID = nextID()
// This side effect is not ideal but the client doesn't attach the tracker
// to itself until after the `.connect` method has fired. If this can be
// refactored later, then we can possibly get rid of this side effect.
message.messageID = currentID
messages.set(currentID, callback)
}
return tracker
}
@@ -0,0 +1,34 @@
'use strict'
const { AbandonedError } = require('../../errors')
const geWindow = require('./ge-window')
/**
* Given a `msgID` and a set of `abandoned` messages, remove any abandoned
* messages that existed _prior_ to the specified `msgID`. For example, let's
* assume the server has sent 3 messages:
*
* 1. A search message.
* 2. An abandon message for the search message.
* 3. A new search message.
*
* When the response for message #1 comes in, if it does, it will be processed
* normally due to the specification. Message #2 will not receive a response, or
* if the server does send one since the spec sort of allows it, we won't do
* anything with it because we just discard that listener. Now the response
* for message #3 comes in. At this point, we will issue a purge of responses
* by passing in `msgID = 3`. This result is that we will remove the tracking
* for message #1.
*
* @param {integer} msgID An upper bound for the messages to be purged.
* @param {Map} abandoned A set of abandoned messages. Each message is an object
* `{ age: <id>, cb: <func> }` where `age` was the current message id when the
* abandon message was sent.
*/
module.exports = function purgeAbandoned (msgID, abandoned) {
abandoned.forEach((val, key) => {
if (geWindow(val.age, msgID) === false) return
val.cb(new AbandonedError('client request abandoned'))
abandoned.delete(key)
})
}
@@ -0,0 +1,36 @@
'use strict'
/**
* Adds requests to the queue. If a timeout has been added to the queue then
* this will freeze the queue with the newly added item, flush it, and then
* unfreeze it when the queue has been cleared.
*
* @param {object} message An LDAP message object.
* @param {object} expect An expectation object.
* @param {object} emitter An event emitter or `null`.
* @param {function} cb A callback to invoke when the request is finished.
*
* @returns {boolean} `true` if the requested was queued. `false` if the queue
* is not accepting any requests.
*/
module.exports = function enqueue (message, expect, emitter, cb) {
if (this._queue.length >= this.size || this._frozen) {
return false
}
this._queue.add({ message, expect, emitter, cb })
if (this.timeout === 0) return true
if (this._timer === null) return true
// A queue can have a specified time allotted for it to be cleared. If that
// time has been reached, reject new entries until the queue has been cleared.
this._timer = setTimeout(queueTimeout.bind(this), this.timeout)
return true
function queueTimeout () {
this.freeze()
this.purge()
}
}
@@ -0,0 +1,24 @@
'use strict'
/**
* Invokes all requests in the queue by passing them to the supplied callback
* function and then clears all items from the queue.
*
* @param {function} cb A function used to handle the requests.
*/
module.exports = function flush (cb) {
if (this._timer) {
clearTimeout(this._timer)
this._timer = null
}
// We must get a local copy of the queue and clear it before iterating it.
// The client will invoke this flush function _many_ times. If we try to
// iterate it without a local copy and clearing first then we will overflow
// the stack.
const requests = Array.from(this._queue.values())
this._queue.clear()
for (const req of requests) {
cb(req.message, req.expect, req.emitter, req.cb)
}
}
@@ -0,0 +1,39 @@
'use strict'
const enqueue = require('./enqueue')
const flush = require('./flush')
const purge = require('./purge')
/**
* Builds a request queue object and returns it.
*
* @param {object} [options]
* @param {integer} [options.size] Maximum size of the request queue. Must be
* a number greater than `0` if supplied. Default: `Infinity`.
* @param {integer} [options.timeout] Time in milliseconds a queue has to
* complete the requests it contains.
*
* @returns {object} A queue instance.
*/
module.exports = function requestQueueFactory (options) {
const opts = Object.assign({}, options)
const q = {
size: (opts.size > 0) ? opts.size : Infinity,
timeout: (opts.timeout > 0) ? opts.timeout : 0,
_queue: new Set(),
_timer: null,
_frozen: false
}
q.enqueue = enqueue.bind(q)
q.flush = flush.bind(q)
q.purge = purge.bind(q)
q.freeze = function freeze () {
this._frozen = true
}
q.thaw = function thaw () {
this._frozen = false
}
return q
}
@@ -0,0 +1,12 @@
'use strict'
const { TimeoutError } = require('../../errors')
/**
* Flushes the queue by rejecting all pending requests with a timeout error.
*/
module.exports = function purge () {
this.flush(function flushCB (a, b, c, cb) {
cb(new TimeoutError('request queue timeout'))
})
}
@@ -0,0 +1,173 @@
'use strict'
const EventEmitter = require('events').EventEmitter
const util = require('util')
const assert = require('assert-plus')
// var dn = require('../dn')
// var messages = require('../messages/index')
// var Protocol = require('../protocol')
const PagedControl = require('../controls/paged_results_control.js')
const CorkedEmitter = require('../corked_emitter.js')
/// --- API
/**
* Handler object for paged search operations.
*
* Provided to consumers in place of the normal search EventEmitter it adds the
* following new events:
* 1. page - Emitted whenever the end of a result page is encountered.
* If this is the last page, 'end' will also be emitted.
* The event passes two arguments:
* 1. The result object (similar to 'end')
* 2. A callback function optionally used to continue the search
* operation if the pagePause option was specified during
* initialization.
* 2. pageError - Emitted if the server does not support paged search results
* If there are no listeners for this event, the 'error' event
* will be emitted (and 'end' will not be). By listening to
* 'pageError', a successful search that lacks paging will be
* able to emit 'end'.
*/
function SearchPager (opts) {
assert.object(opts)
assert.func(opts.callback)
assert.number(opts.pageSize)
assert.func(opts.sendRequest)
CorkedEmitter.call(this, {})
this.callback = opts.callback
this.controls = opts.controls
this.pageSize = opts.pageSize
this.pagePause = opts.pagePause
this.sendRequest = opts.sendRequest
this.controls.forEach(function (control) {
if (control.type === PagedControl.OID) {
// The point of using SearchPager is not having to do this.
// Toss an error if the pagedResultsControl is present
throw new Error('redundant pagedResultControl')
}
})
this.finished = false
this.started = false
const emitter = new EventEmitter()
emitter.on('searchRequest', this.emit.bind(this, 'searchRequest'))
emitter.on('searchEntry', this.emit.bind(this, 'searchEntry'))
emitter.on('end', this._onEnd.bind(this))
emitter.on('error', this._onError.bind(this))
this.childEmitter = emitter
}
util.inherits(SearchPager, CorkedEmitter)
module.exports = SearchPager
/**
* Start the paged search.
*/
SearchPager.prototype.begin = function begin () {
// Starting first page
this._nextPage(null)
}
SearchPager.prototype._onEnd = function _onEnd (res) {
const self = this
let cookie = null
res.controls.forEach(function (control) {
if (control.type === PagedControl.OID) {
cookie = control.value.cookie
}
})
// Pass a noop callback by default for page events
const nullCb = function () { }
if (cookie === null) {
// paged search not supported
this.finished = true
this.emit('page', res, nullCb)
const err = new Error('missing paged control')
err.name = 'PagedError'
if (this.listeners('pageError').length > 0) {
this.emit('pageError', err)
// If the consumer as subscribed to pageError, SearchPager is absolved
// from deliverying the fault via the 'error' event. Emitting an 'end'
// event after 'error' breaks the contract that the standard client
// provides, so it's only a possibility if 'pageError' is used instead.
this.emit('end', res)
} else {
this.emit('error', err)
// No end event possible per explaination above.
}
return
}
if (cookie.length === 0) {
// end of paged results
this.finished = true
this.emit('page', nullCb)
this.emit('end', res)
} else {
if (this.pagePause) {
// Wait to fetch next page until callback is invoked
// Halt page fetching if called with error
this.emit('page', res, function (err) {
if (!err) {
self._nextPage(cookie)
} else {
// the paged search has been canceled so emit an end
self.emit('end', res)
}
})
} else {
this.emit('page', res, nullCb)
this._nextPage(cookie)
}
}
}
SearchPager.prototype._onError = function _onError (err) {
this.finished = true
this.emit('error', err)
}
/**
* Initiate a search for the next page using the returned cookie value.
*/
SearchPager.prototype._nextPage = function _nextPage (cookie) {
const controls = this.controls.slice(0)
controls.push(new PagedControl({
value: {
size: this.pageSize,
cookie: cookie
}
}))
this.sendRequest(controls, this.childEmitter, this._sendCallback.bind(this))
}
/**
* Callback provided to the client API for successful transmission.
*/
SearchPager.prototype._sendCallback = function _sendCallback (err) {
if (err) {
this.finished = true
if (!this.started) {
// EmitSend error during the first page, bail via callback
this.callback(err, null)
} else {
this.emit('error', err)
}
} else {
// search successfully send
if (!this.started) {
this.started = true
// send self as emitter as the client would
this.callback(null, this)
}
}
}
@@ -0,0 +1,61 @@
// Copyright 2011 Mark Cavage, Inc. All rights reserved.
const assert = require('assert-plus')
// var asn1 = require('asn1')
// var Protocol = require('../protocol')
/// --- Globals
// var Ber = asn1.Ber
/// --- API
function Control (options) {
assert.optionalObject(options)
options = options || {}
assert.optionalString(options.type)
assert.optionalBool(options.criticality)
if (options.value) {
assert.buffer(options.value)
}
this.type = options.type || ''
this.criticality = options.critical || options.criticality || false
this.value = options.value || null
}
Object.defineProperties(Control.prototype, {
json: {
get: function getJson () {
const obj = {
controlType: this.type,
criticality: this.criticality,
controlValue: this.value
}
return (typeof (this._json) === 'function' ? this._json(obj) : obj)
}
}
})
Control.prototype.toBer = function toBer (ber) {
assert.ok(ber)
ber.startSequence()
ber.writeString(this.type || '')
ber.writeBoolean(this.criticality)
if (typeof (this._toBer) === 'function') {
this._toBer(ber)
} else {
if (this.value) { ber.writeString(this.value) }
}
ber.endSequence()
}
Control.prototype.toString = function toString () {
return this.json
}
/// --- Exports
module.exports = Control
@@ -0,0 +1,83 @@
const assert = require('assert-plus')
const util = require('util')
const asn1 = require('asn1')
const Control = require('./control')
/// --- Globals
const BerReader = asn1.BerReader
const BerWriter = asn1.BerWriter
/// --- API
function EntryChangeNotificationControl (options) {
assert.optionalObject(options)
options = options || {}
options.type = EntryChangeNotificationControl.OID
if (options.value) {
if (Buffer.isBuffer(options.value)) {
this.parse(options.value)
} else if (typeof (options.value) === 'object') {
this._value = options.value
} else {
throw new TypeError('options.value must be a Buffer or Object')
}
options.value = null
}
Control.call(this, options)
}
util.inherits(EntryChangeNotificationControl, Control)
Object.defineProperties(EntryChangeNotificationControl.prototype, {
value: {
get: function () { return this._value || {} },
configurable: false
}
})
EntryChangeNotificationControl.prototype.parse = function parse (buffer) {
assert.ok(buffer)
const ber = new BerReader(buffer)
if (ber.readSequence()) {
this._value = {
changeType: ber.readInt()
}
// if the operation was moddn, then parse the optional previousDN attr
if (this._value.changeType === 8) { this._value.previousDN = ber.readString() }
this._value.changeNumber = ber.readInt()
return true
}
return false
}
EntryChangeNotificationControl.prototype._toBer = function (ber) {
assert.ok(ber)
if (!this._value) { return }
const writer = new BerWriter()
writer.startSequence()
writer.writeInt(this.value.changeType)
if (this.value.previousDN) { writer.writeString(this.value.previousDN) }
writer.writeInt(parseInt(this.value.changeNumber, 10))
writer.endSequence()
ber.writeBuffer(writer.buffer, 0x04)
}
EntryChangeNotificationControl.prototype._json = function (obj) {
obj.controlValue = this.value
return obj
}
EntryChangeNotificationControl.OID = '2.16.840.1.113730.3.4.7'
/// --- Exports
module.exports = EntryChangeNotificationControl
@@ -0,0 +1,86 @@
// Copyright 2011 Mark Cavage, Inc. All rights reserved.
const assert = require('assert')
const Ber = require('asn1').Ber
const Control = require('./control')
const EntryChangeNotificationControl =
require('./entry_change_notification_control')
const PersistentSearchControl = require('./persistent_search_control')
const PagedResultsControl = require('./paged_results_control')
const ServerSideSortingRequestControl =
require('./server_side_sorting_request_control.js')
const ServerSideSortingResponseControl =
require('./server_side_sorting_response_control.js')
const VirtualListViewRequestControl =
require('./virtual_list_view_request_control.js')
const VirtualListViewResponseControl =
require('./virtual_list_view_response_control.js')
/// --- API
module.exports = {
getControl: function getControl (ber) {
assert.ok(ber)
if (ber.readSequence() === null) { return null }
let type
const opts = {
criticality: false,
value: null
}
if (ber.length) {
const end = ber.offset + ber.length
type = ber.readString()
if (ber.offset < end) {
if (ber.peek() === Ber.Boolean) { opts.criticality = ber.readBoolean() }
}
if (ber.offset < end) { opts.value = ber.readString(Ber.OctetString, true) }
}
let control
switch (type) {
case PersistentSearchControl.OID:
control = new PersistentSearchControl(opts)
break
case EntryChangeNotificationControl.OID:
control = new EntryChangeNotificationControl(opts)
break
case PagedResultsControl.OID:
control = new PagedResultsControl(opts)
break
case ServerSideSortingRequestControl.OID:
control = new ServerSideSortingRequestControl(opts)
break
case ServerSideSortingResponseControl.OID:
control = new ServerSideSortingResponseControl(opts)
break
case VirtualListViewRequestControl.OID:
control = new VirtualListViewRequestControl(opts)
break
case VirtualListViewResponseControl.OID:
control = new VirtualListViewResponseControl(opts)
break
default:
opts.type = type
control = new Control(opts)
break
}
return control
},
Control: Control,
EntryChangeNotificationControl: EntryChangeNotificationControl,
PagedResultsControl: PagedResultsControl,
PersistentSearchControl: PersistentSearchControl,
ServerSideSortingRequestControl: ServerSideSortingRequestControl,
ServerSideSortingResponseControl: ServerSideSortingResponseControl,
VirtualListViewRequestControl: VirtualListViewRequestControl,
VirtualListViewResponseControl: VirtualListViewResponseControl
}
@@ -0,0 +1,82 @@
const assert = require('assert-plus')
const util = require('util')
const asn1 = require('asn1')
const Control = require('./control')
/// --- Globals
const BerReader = asn1.BerReader
const BerWriter = asn1.BerWriter
/// --- API
function PagedResultsControl (options) {
assert.optionalObject(options)
options = options || {}
options.type = PagedResultsControl.OID
if (options.value) {
if (Buffer.isBuffer(options.value)) {
this.parse(options.value)
} else if (typeof (options.value) === 'object') {
this._value = options.value
} else {
throw new TypeError('options.value must be a Buffer or Object')
}
options.value = null
}
Control.call(this, options)
}
util.inherits(PagedResultsControl, Control)
Object.defineProperties(PagedResultsControl.prototype, {
value: {
get: function () { return this._value || {} },
configurable: false
}
})
PagedResultsControl.prototype.parse = function parse (buffer) {
assert.ok(buffer)
const ber = new BerReader(buffer)
if (ber.readSequence()) {
this._value = {}
this._value.size = ber.readInt()
this._value.cookie = ber.readString(asn1.Ber.OctetString, true)
// readString returns '' instead of a zero-length buffer
if (!this._value.cookie) { this._value.cookie = Buffer.alloc(0) }
return true
}
return false
}
PagedResultsControl.prototype._toBer = function (ber) {
assert.ok(ber)
if (!this._value) { return }
const writer = new BerWriter()
writer.startSequence()
writer.writeInt(this.value.size)
if (this.value.cookie && this.value.cookie.length > 0) {
writer.writeBuffer(this.value.cookie, asn1.Ber.OctetString)
} else {
writer.writeString('') // writeBuffer rejects zero-length buffers
}
writer.endSequence()
ber.writeBuffer(writer.buffer, 0x04)
}
PagedResultsControl.prototype._json = function (obj) {
obj.controlValue = this.value
return obj
}
PagedResultsControl.OID = '1.2.840.113556.1.4.319'
/// --- Exports
module.exports = PagedResultsControl
@@ -0,0 +1,82 @@
// Copyright 2011 Mark Cavage, Inc. All rights reserved.
const assert = require('assert-plus')
const util = require('util')
const asn1 = require('asn1')
const Control = require('./control')
/// --- Globals
const BerReader = asn1.BerReader
const BerWriter = asn1.BerWriter
/// --- API
function PersistentSearchControl (options) {
assert.optionalObject(options)
options = options || {}
options.type = PersistentSearchControl.OID
if (options.value) {
if (Buffer.isBuffer(options.value)) {
this.parse(options.value)
} else if (typeof (options.value) === 'object') {
this._value = options.value
} else {
throw new TypeError('options.value must be a Buffer or Object')
}
options.value = null
}
Control.call(this, options)
}
util.inherits(PersistentSearchControl, Control)
Object.defineProperties(PersistentSearchControl.prototype, {
value: {
get: function () { return this._value || {} },
configurable: false
}
})
PersistentSearchControl.prototype.parse = function parse (buffer) {
assert.ok(buffer)
const ber = new BerReader(buffer)
if (ber.readSequence()) {
this._value = {
changeTypes: ber.readInt(),
changesOnly: ber.readBoolean(),
returnECs: ber.readBoolean()
}
return true
}
return false
}
PersistentSearchControl.prototype._toBer = function (ber) {
assert.ok(ber)
if (!this._value) { return }
const writer = new BerWriter()
writer.startSequence()
writer.writeInt(this.value.changeTypes)
writer.writeBoolean(this.value.changesOnly)
writer.writeBoolean(this.value.returnECs)
writer.endSequence()
ber.writeBuffer(writer.buffer, 0x04)
}
PersistentSearchControl.prototype._json = function (obj) {
obj.controlValue = this.value
return obj
}
PersistentSearchControl.OID = '2.16.840.1.113730.3.4.3'
/// --- Exports
module.exports = PersistentSearchControl
@@ -0,0 +1,108 @@
const assert = require('assert-plus')
const util = require('util')
const asn1 = require('asn1')
const Control = require('./control')
/// --- Globals
const BerReader = asn1.BerReader
const BerWriter = asn1.BerWriter
/// --- API
function ServerSideSortingRequestControl (options) {
assert.optionalObject(options)
options = options || {}
options.type = ServerSideSortingRequestControl.OID
if (options.value) {
if (Buffer.isBuffer(options.value)) {
this.parse(options.value)
} else if (Array.isArray(options.value)) {
assert.arrayOfObject(options.value, 'options.value must be Objects')
for (let i = 0; i < options.value.length; i++) {
if (Object.prototype.hasOwnProperty.call(options.value[i], 'attributeType') === false) {
throw new Error('Missing required key: attributeType')
}
}
this._value = options.value
} else if (typeof (options.value) === 'object') {
if (Object.prototype.hasOwnProperty.call(options.value, 'attributeType') === false) {
throw new Error('Missing required key: attributeType')
}
this._value = [options.value]
} else {
throw new TypeError('options.value must be a Buffer, Array or Object')
}
options.value = null
}
Control.call(this, options)
}
util.inherits(ServerSideSortingRequestControl, Control)
Object.defineProperties(ServerSideSortingRequestControl.prototype, {
value: {
get: function () { return this._value || [] },
configurable: false
}
})
ServerSideSortingRequestControl.prototype.parse = function parse (buffer) {
assert.ok(buffer)
const ber = new BerReader(buffer)
let item
if (ber.readSequence(0x30)) {
this._value = []
while (ber.readSequence(0x30)) {
item = {}
item.attributeType = ber.readString(asn1.Ber.OctetString)
if (ber.peek() === 0x80) {
item.orderingRule = ber.readString(0x80)
}
if (ber.peek() === 0x81) {
item.reverseOrder = (ber._readTag(0x81) !== 0)
}
this._value.push(item)
}
return true
}
return false
}
ServerSideSortingRequestControl.prototype._toBer = function (ber) {
assert.ok(ber)
if (!this._value || this.value.length === 0) { return }
const writer = new BerWriter()
writer.startSequence(0x30)
for (let i = 0; i < this.value.length; i++) {
const item = this.value[i]
writer.startSequence(0x30)
if (item.attributeType) {
writer.writeString(item.attributeType, asn1.Ber.OctetString)
}
if (item.orderingRule) {
writer.writeString(item.orderingRule, 0x80)
}
if (item.reverseOrder) {
writer.writeBoolean(item.reverseOrder, 0x81)
}
writer.endSequence()
}
writer.endSequence()
ber.writeBuffer(writer.buffer, 0x04)
}
ServerSideSortingRequestControl.prototype._json = function (obj) {
obj.controlValue = this.value
return obj
}
ServerSideSortingRequestControl.OID = '1.2.840.113556.1.4.473'
/// ---Exports
module.exports = ServerSideSortingRequestControl
@@ -0,0 +1,100 @@
const assert = require('assert-plus')
const util = require('util')
const asn1 = require('asn1')
const Control = require('./control')
const CODES = require('../errors/codes')
/// --- Globals
const BerReader = asn1.BerReader
const BerWriter = asn1.BerWriter
const VALID_CODES = [
CODES.LDAP_SUCCESS,
CODES.LDAP_OPERATIONS_ERROR,
CODES.LDAP_TIME_LIMIT_EXCEEDED,
CODES.LDAP_STRONG_AUTH_REQUIRED,
CODES.LDAP_ADMIN_LIMIT_EXCEEDED,
CODES.LDAP_NO_SUCH_ATTRIBUTE,
CODES.LDAP_INAPPROPRIATE_MATCHING,
CODES.LDAP_INSUFFICIENT_ACCESS_RIGHTS,
CODES.LDAP_BUSY,
CODES.LDAP_UNWILLING_TO_PERFORM,
CODES.LDAP_OTHER
]
function ServerSideSortingResponseControl (options) {
assert.optionalObject(options)
options = options || {}
options.type = ServerSideSortingResponseControl.OID
options.criticality = false
if (options.value) {
if (Buffer.isBuffer(options.value)) {
this.parse(options.value)
} else if (typeof (options.value) === 'object') {
if (VALID_CODES.indexOf(options.value.result) === -1) {
throw new Error('Invalid result code')
}
if (options.value.failedAttribute &&
typeof (options.value.failedAttribute) !== 'string') {
throw new Error('failedAttribute must be String')
}
this._value = options.value
} else {
throw new TypeError('options.value must be a Buffer or Object')
}
options.value = null
}
Control.call(this, options)
}
util.inherits(ServerSideSortingResponseControl, Control)
Object.defineProperties(ServerSideSortingResponseControl.prototype, {
value: {
get: function () { return this._value || {} },
configurable: false
}
})
ServerSideSortingResponseControl.prototype.parse = function parse (buffer) {
assert.ok(buffer)
const ber = new BerReader(buffer)
if (ber.readSequence(0x30)) {
this._value = {}
this._value.result = ber.readEnumeration()
if (ber.peek() === 0x80) {
this._value.failedAttribute = ber.readString(0x80)
}
return true
}
return false
}
ServerSideSortingResponseControl.prototype._toBer = function (ber) {
assert.ok(ber)
if (!this._value || this.value.length === 0) { return }
const writer = new BerWriter()
writer.startSequence(0x30)
writer.writeEnumeration(this.value.result)
if (this.value.result !== CODES.LDAP_SUCCESS && this.value.failedAttribute) {
writer.writeString(this.value.failedAttribute, 0x80)
}
writer.endSequence()
ber.writeBuffer(writer.buffer, 0x04)
}
ServerSideSortingResponseControl.prototype._json = function (obj) {
obj.controlValue = this.value
return obj
}
ServerSideSortingResponseControl.OID = '1.2.840.113556.1.4.474'
/// --- Exports
module.exports = ServerSideSortingResponseControl
@@ -0,0 +1,94 @@
const assert = require('assert-plus')
const util = require('util')
const asn1 = require('asn1')
const Control = require('./control')
/// --- Globals
const BerReader = asn1.BerReader
const BerWriter = asn1.BerWriter
/// --- API
function VirtualListViewControl (options) {
assert.optionalObject(options)
options = options || {}
options.type = VirtualListViewControl.OID
if (options.value) {
if (Buffer.isBuffer(options.value)) {
this.parse(options.value)
} else if (typeof (options.value) === 'object') {
if (Object.prototype.hasOwnProperty.call(options.value, 'beforeCount') === false) {
throw new Error('Missing required key: beforeCount')
}
if (Object.prototype.hasOwnProperty.call(options.value, 'afterCount') === false) {
throw new Error('Missing required key: afterCount')
}
this._value = options.value
} else {
throw new TypeError('options.value must be a Buffer or Object')
}
options.value = null
}
Control.call(this, options)
}
util.inherits(VirtualListViewControl, Control)
Object.defineProperties(VirtualListViewControl.prototype, {
value: {
get: function () { return this._value || [] },
configurable: false
}
})
VirtualListViewControl.prototype.parse = function parse (buffer) {
assert.ok(buffer)
const ber = new BerReader(buffer)
if (ber.readSequence()) {
this._value = {}
this._value.beforeCount = ber.readInt()
this._value.afterCount = ber.readInt()
if (ber.peek() === 0xa0) {
if (ber.readSequence(0xa0)) {
this._value.targetOffset = ber.readInt()
this._value.contentCount = ber.readInt()
}
}
if (ber.peek() === 0x81) {
this._value.greaterThanOrEqual = ber.readString(0x81)
}
return true
}
return false
}
VirtualListViewControl.prototype._toBer = function (ber) {
assert.ok(ber)
if (!this._value || this.value.length === 0) {
return
}
const writer = new BerWriter()
writer.startSequence(0x30)
writer.writeInt(this.value.beforeCount)
writer.writeInt(this.value.afterCount)
if (this.value.targetOffset !== undefined) {
writer.startSequence(0xa0)
writer.writeInt(this.value.targetOffset)
writer.writeInt(this.value.contentCount)
writer.endSequence()
} else if (this.value.greaterThanOrEqual !== undefined) {
writer.writeString(this.value.greaterThanOrEqual, 0x81)
}
writer.endSequence()
ber.writeBuffer(writer.buffer, 0x04)
}
VirtualListViewControl.prototype._json = function (obj) {
obj.controlValue = this.value
return obj
}
VirtualListViewControl.OID = '2.16.840.1.113730.3.4.9'
/// ---Exports
module.exports = VirtualListViewControl
@@ -0,0 +1,112 @@
const assert = require('assert-plus')
const util = require('util')
const asn1 = require('asn1')
const Control = require('./control')
const CODES = require('../errors/codes')
/// --- Globals
const BerReader = asn1.BerReader
const BerWriter = asn1.BerWriter
const VALID_CODES = [
CODES.LDAP_SUCCESS,
CODES.LDAP_OPERATIONS_ERROR,
CODES.LDAP_UNWILLING_TO_PERFORM,
CODES.LDAP_INSUFFICIENT_ACCESS_RIGHTS,
CODES.LDAP_BUSY,
CODES.LDAP_TIME_LIMIT_EXCEEDED,
CODES.LDAP_ADMIN_LIMIT_EXCEEDED,
CODES.LDAP_SORT_CONTROL_MISSING,
CODES.LDAP_INDEX_RANGE_ERROR,
CODES.LDAP_CONTROL_ERROR,
CODES.LDAP_OTHER
]
function VirtualListViewResponseControl (options) {
assert.optionalObject(options)
options = options || {}
options.type = VirtualListViewResponseControl.OID
options.criticality = false
if (options.value) {
if (Buffer.isBuffer(options.value)) {
this.parse(options.value)
} else if (typeof (options.value) === 'object') {
if (VALID_CODES.indexOf(options.value.result) === -1) {
throw new Error('Invalid result code')
}
this._value = options.value
} else {
throw new TypeError('options.value must be a Buffer or Object')
}
options.value = null
}
Control.call(this, options)
}
util.inherits(VirtualListViewResponseControl, Control)
Object.defineProperties(VirtualListViewResponseControl.prototype, {
value: {
get: function () { return this._value || {} },
configurable: false
}
})
VirtualListViewResponseControl.prototype.parse = function parse (buffer) {
assert.ok(buffer)
const ber = new BerReader(buffer)
if (ber.readSequence()) {
this._value = {}
if (ber.peek(0x02)) {
this._value.targetPosition = ber.readInt()
}
if (ber.peek(0x02)) {
this._value.contentCount = ber.readInt()
}
this._value.result = ber.readEnumeration()
this._value.cookie = ber.readString(asn1.Ber.OctetString, true)
// readString returns '' instead of a zero-length buffer
if (!this._value.cookie) {
this._value.cookie = Buffer.alloc(0)
}
return true
}
return false
}
VirtualListViewResponseControl.prototype._toBer = function (ber) {
assert.ok(ber)
if (!this._value || this.value.length === 0) {
return
}
const writer = new BerWriter()
writer.startSequence()
if (this.value.targetPosition !== undefined) {
writer.writeInt(this.value.targetPosition)
}
if (this.value.contentCount !== undefined) {
writer.writeInt(this.value.contentCount)
}
writer.writeEnumeration(this.value.result)
if (this.value.cookie && this.value.cookie.length > 0) {
writer.writeBuffer(this.value.cookie, asn1.Ber.OctetString)
} else {
writer.writeString('') // writeBuffer rejects zero-length buffers
}
writer.endSequence()
ber.writeBuffer(writer.buffer, 0x04)
}
VirtualListViewResponseControl.prototype._json = function (obj) {
obj.controlValue = this.value
return obj
}
VirtualListViewResponseControl.OID = '2.16.840.1.113730.3.4.10'
/// --- Exports
module.exports = VirtualListViewResponseControl
@@ -0,0 +1,50 @@
'use strict'
const EventEmitter = require('events').EventEmitter
/**
* A CorkedEmitter is a variant of an EventEmitter where events emitted
* wait for the appearance of the first listener of any kind. That is,
* a CorkedEmitter will store all .emit()s it receives, to be replayed
* later when an .on() is applied.
* It is meant for situations where the consumers of the emitter are
* unable to register listeners right away, and cannot afford to miss
* any events emitted from the start.
* Note that, whenever the first emitter (for any event) appears,
* the emitter becomes uncorked and works as usual for ALL events, and
* will not cache anything anymore. This is necessary to avoid
* re-ordering emits - either everything is being buffered, or nothing.
*/
function CorkedEmitter () {
const self = this
EventEmitter.call(self)
/**
* An array of arguments objects (array-likes) to emit on open.
*/
self._outstandingEmits = []
/**
* Whether the normal flow of emits is restored yet.
*/
self._opened = false
// When the first listener appears, we enqueue an opening.
// It is not done immediately, so that other listeners can be
// registered in the same critical section.
self.once('newListener', function () {
setImmediate(function releaseStoredEvents () {
self._opened = true
self._outstandingEmits.forEach(function (args) {
self.emit.apply(self, args)
})
})
})
}
CorkedEmitter.prototype = Object.create(EventEmitter.prototype)
CorkedEmitter.prototype.emit = function emit (eventName) {
if (this._opened || eventName === 'newListener') {
EventEmitter.prototype.emit.apply(this, arguments)
} else {
this._outstandingEmits.push(arguments)
}
}
module.exports = CorkedEmitter
@@ -0,0 +1,473 @@
// Copyright 2011 Mark Cavage, Inc. All rights reserved.
const assert = require('assert-plus')
/// --- Helpers
function invalidDN (name) {
const e = new Error()
e.name = 'InvalidDistinguishedNameError'
e.message = name
return e
}
function isAlphaNumeric (c) {
const re = /[A-Za-z0-9]/
return re.test(c)
}
function isWhitespace (c) {
const re = /\s/
return re.test(c)
}
function repeatChar (c, n) {
let out = ''
const max = n || 0
for (let i = 0; i < max; i++) { out += c }
return out
}
/// --- API
function RDN (obj) {
const self = this
this.attrs = {}
if (obj) {
Object.keys(obj).forEach(function (k) {
self.set(k, obj[k])
})
}
}
RDN.prototype.set = function rdnSet (name, value, opts) {
assert.string(name, 'name (string) required')
assert.string(value, 'value (string) required')
const self = this
const lname = name.toLowerCase()
this.attrs[lname] = {
value: value,
name: name
}
if (opts && typeof (opts) === 'object') {
Object.keys(opts).forEach(function (k) {
if (k !== 'value') { self.attrs[lname][k] = opts[k] }
})
}
}
RDN.prototype.equals = function rdnEquals (rdn) {
if (typeof (rdn) !== 'object') { return false }
const ourKeys = Object.keys(this.attrs)
const theirKeys = Object.keys(rdn.attrs)
if (ourKeys.length !== theirKeys.length) { return false }
ourKeys.sort()
theirKeys.sort()
for (let i = 0; i < ourKeys.length; i++) {
if (ourKeys[i] !== theirKeys[i]) { return false }
if (this.attrs[ourKeys[i]].value !== rdn.attrs[ourKeys[i]].value) { return false }
}
return true
}
/**
* Convert RDN to string according to specified formatting options.
* (see: DN.format for option details)
*/
RDN.prototype.format = function rdnFormat (options) {
assert.optionalObject(options, 'options must be an object')
options = options || {}
const self = this
let str = ''
function escapeValue (val, forceQuote) {
let out = ''
let cur = 0
const len = val.length
let quoted = false
/* BEGIN JSSTYLED */
// TODO: figure out what this regex is actually trying to test for and
// fix it to appease the linter.
/* eslint-disable-next-line no-useless-escape */
const escaped = /[\\\"]/
const special = /[,=+<>#;]/
/* END JSSTYLED */
if (len > 0) {
// Wrap strings with trailing or leading spaces in quotes
quoted = forceQuote || (val[0] === ' ' || val[len - 1] === ' ')
}
while (cur < len) {
if (escaped.test(val[cur]) || (!quoted && special.test(val[cur]))) {
out += '\\'
}
out += val[cur++]
}
if (quoted) { out = '"' + out + '"' }
return out
}
function sortParsed (a, b) {
return self.attrs[a].order - self.attrs[b].order
}
function sortStandard (a, b) {
const nameCompare = a.localeCompare(b)
if (nameCompare === 0) {
// TODO: Handle binary values
return self.attrs[a].value.localeCompare(self.attrs[b].value)
} else {
return nameCompare
}
}
const keys = Object.keys(this.attrs)
if (options.keepOrder) {
keys.sort(sortParsed)
} else {
keys.sort(sortStandard)
}
keys.forEach(function (key) {
const attr = self.attrs[key]
if (str.length) { str += '+' }
if (options.keepCase) {
str += attr.name
} else {
if (options.upperName) { str += key.toUpperCase() } else { str += key }
}
str += '=' + escapeValue(attr.value, (options.keepQuote && attr.quoted))
})
return str
}
RDN.prototype.toString = function rdnToString () {
return this.format()
}
// Thank you OpenJDK!
function parse (name) {
if (typeof (name) !== 'string') { throw new TypeError('name (string) required') }
let cur = 0
const len = name.length
function parseRdn () {
const rdn = new RDN()
let order = 0
rdn.spLead = trim()
while (cur < len) {
const opts = {
order: order
}
const attr = parseAttrType()
trim()
if (cur >= len || name[cur++] !== '=') { throw invalidDN(name) }
trim()
// Parameters about RDN value are set in 'opts' by parseAttrValue
const value = parseAttrValue(opts)
rdn.set(attr, value, opts)
rdn.spTrail = trim()
if (cur >= len || name[cur] !== '+') { break }
++cur
++order
}
return rdn
}
function trim () {
let count = 0
while ((cur < len) && isWhitespace(name[cur])) {
++cur
count++
}
return count
}
function parseAttrType () {
const beg = cur
while (cur < len) {
const c = name[cur]
if (isAlphaNumeric(c) ||
c === '.' ||
c === '-' ||
c === ' ') {
++cur
} else {
break
}
}
// Back out any trailing spaces.
while ((cur > beg) && (name[cur - 1] === ' ')) { --cur }
if (beg === cur) { throw invalidDN(name) }
return name.slice(beg, cur)
}
function parseAttrValue (opts) {
if (cur < len && name[cur] === '#') {
opts.binary = true
return parseBinaryAttrValue()
} else if (cur < len && name[cur] === '"') {
opts.quoted = true
return parseQuotedAttrValue()
} else {
return parseStringAttrValue()
}
}
function parseBinaryAttrValue () {
const beg = cur++
while (cur < len && isAlphaNumeric(name[cur])) { ++cur }
return name.slice(beg, cur)
}
function parseQuotedAttrValue () {
let str = ''
++cur // Consume the first quote
while ((cur < len) && name[cur] !== '"') {
if (name[cur] === '\\') { cur++ }
str += name[cur++]
}
if (cur++ >= len) {
// no closing quote
throw invalidDN(name)
}
return str
}
function parseStringAttrValue () {
const beg = cur
let str = ''
let esc = -1
while ((cur < len) && !atTerminator()) {
if (name[cur] === '\\') {
// Consume the backslash and mark its place just in case it's escaping
// whitespace which needs to be preserved.
esc = cur++
}
if (cur === len) {
// backslash followed by nothing
throw invalidDN(name)
}
str += name[cur++]
}
// Trim off (unescaped) trailing whitespace and rewind cursor to the end of
// the AttrValue to record whitespace length.
for (; cur > beg; cur--) {
if (!isWhitespace(name[cur - 1]) || (esc === (cur - 1))) { break }
}
return str.slice(0, cur - beg)
}
function atTerminator () {
return (cur < len &&
(name[cur] === ',' ||
name[cur] === ';' ||
name[cur] === '+'))
}
const rdns = []
// Short-circuit for empty DNs
if (len === 0) { return new DN(rdns) }
rdns.push(parseRdn())
while (cur < len) {
if (name[cur] === ',' || name[cur] === ';') {
++cur
rdns.push(parseRdn())
} else {
throw invalidDN(name)
}
}
return new DN(rdns)
}
function DN (rdns) {
assert.optionalArrayOfObject(rdns, '[object] required')
this.rdns = rdns ? rdns.slice() : []
this._format = {}
}
Object.defineProperties(DN.prototype, {
length: {
get: function getLength () { return this.rdns.length },
configurable: false
}
})
/**
* Convert DN to string according to specified formatting options.
*
* Parameters:
* - options: formatting parameters (optional, details below)
*
* Options are divided into two types:
* - Preservation options: Using data recorded during parsing, details of the
* original DN are preserved when converting back into a string.
* - Modification options: Alter string formatting defaults.
*
* Preservation options _always_ take precedence over modification options.
*
* Preservation Options:
* - keepOrder: Order of multi-value RDNs.
* - keepQuote: RDN values which were quoted will remain so.
* - keepSpace: Leading/trailing spaces will be output.
* - keepCase: Parsed attr name will be output instead of lowercased version.
*
* Modification Options:
* - upperName: RDN names will be uppercased instead of lowercased.
* - skipSpace: Disable trailing space after RDN separators
*/
DN.prototype.format = function dnFormat (options) {
assert.optionalObject(options, 'options must be an object')
options = options || this._format
let str = ''
this.rdns.forEach(function (rdn) {
const rdnString = rdn.format(options)
if (str.length !== 0) {
str += ','
}
if (options.keepSpace) {
str += (repeatChar(' ', rdn.spLead) +
rdnString + repeatChar(' ', rdn.spTrail))
} else if (options.skipSpace === true || str.length === 0) {
str += rdnString
} else {
str += ' ' + rdnString
}
})
return str
}
/**
* Set default string formatting options.
*/
DN.prototype.setFormat = function setFormat (options) {
assert.object(options, 'options must be an object')
this._format = options
}
DN.prototype.toString = function dnToString () {
return this.format()
}
DN.prototype.parentOf = function parentOf (dn) {
if (typeof (dn) !== 'object') { dn = parse(dn) }
if (this.rdns.length >= dn.rdns.length) { return false }
const diff = dn.rdns.length - this.rdns.length
for (let i = this.rdns.length - 1; i >= 0; i--) {
const myRDN = this.rdns[i]
const theirRDN = dn.rdns[i + diff]
if (!myRDN.equals(theirRDN)) { return false }
}
return true
}
DN.prototype.childOf = function childOf (dn) {
if (typeof (dn) !== 'object') { dn = parse(dn) }
return dn.parentOf(this)
}
DN.prototype.isEmpty = function isEmpty () {
return (this.rdns.length === 0)
}
DN.prototype.equals = function dnEquals (dn) {
if (typeof (dn) !== 'object') { dn = parse(dn) }
if (this.rdns.length !== dn.rdns.length) { return false }
for (let i = 0; i < this.rdns.length; i++) {
if (!this.rdns[i].equals(dn.rdns[i])) { return false }
}
return true
}
DN.prototype.parent = function dnParent () {
if (this.rdns.length !== 0) {
const save = this.rdns.shift()
const dn = new DN(this.rdns)
this.rdns.unshift(save)
return dn
}
return null
}
DN.prototype.clone = function dnClone () {
const dn = new DN(this.rdns)
dn._format = this._format
return dn
}
DN.prototype.reverse = function dnReverse () {
this.rdns.reverse()
return this
}
DN.prototype.pop = function dnPop () {
return this.rdns.pop()
}
DN.prototype.push = function dnPush (rdn) {
assert.object(rdn, 'rdn (RDN) required')
return this.rdns.push(rdn)
}
DN.prototype.shift = function dnShift () {
return this.rdns.shift()
}
DN.prototype.unshift = function dnUnshift (rdn) {
assert.object(rdn, 'rdn (RDN) required')
return this.rdns.unshift(rdn)
}
DN.isDN = function isDN (dn) {
if (!dn || typeof (dn) !== 'object') {
return false
}
if (dn instanceof DN) {
return true
}
if (Array.isArray(dn.rdns)) {
// Really simple duck-typing for now
return true
}
return false
}
/// --- Exports
module.exports = {
parse: parse,
DN: DN,
RDN: RDN
}
@@ -0,0 +1,120 @@
// Copyright 2011 Mark Cavage, Inc. All rights reserved.s
/// --- Globals
let SERVER_PROVIDER
let DTRACE_ID = 0
const MAX_INT = 4294967295
/*
* Args:
* server-*-start:
* 0 -> id
* 1 -> remoteIP
* 2 -> bindDN
* 3 -> req.dn
* 4,5 -> op specific
*
* server-*-done:
* 0 -> id
* 1 -> remoteIp
* 2 -> bindDN
* 3 -> requsetDN
* 4 -> status
* 5 -> errorMessage
*
*/
const SERVER_PROBES = {
// 4: attributes.length
'server-add-start': ['int', 'char *', 'char *', 'char *', 'int'],
'server-add-done': ['int', 'char *', 'char *', 'char *', 'int', 'char *'],
'server-bind-start': ['int', 'char *', 'char *', 'char *'],
'server-bind-done': ['int', 'char *', 'char *', 'char *', 'int', 'char *'],
// 4: attribute, 5: value
'server-compare-start': ['int', 'char *', 'char *', 'char *',
'char *', 'char *'],
'server-compare-done': ['int', 'char *', 'char *', 'char *', 'int', 'char *'],
'server-delete-start': ['int', 'char *', 'char *', 'char *'],
'server-delete-done': ['int', 'char *', 'char *', 'char *', 'int', 'char *'],
// 4: requestName, 5: requestValue
'server-exop-start': ['int', 'char *', 'char *', 'char *', 'char *',
'char *'],
'server-exop-done': ['int', 'char *', 'char *', 'char *', 'int', 'char *'],
// 4: changes.length
'server-modify-start': ['int', 'char *', 'char *', 'char *', 'int'],
'server-modify-done': ['int', 'char *', 'char *', 'char *', 'int', 'char *'],
// 4: newRdn, 5: newSuperior
'server-modifydn-start': ['int', 'char *', 'char *', 'char *', 'char *',
'char *'],
'server-modifydn-done': ['int', 'char *', 'char *', 'char *', 'int',
'char *'],
// 4: scope, 5: filter
'server-search-start': ['int', 'char *', 'char *', 'char *', 'char *',
'char *'],
'server-search-done': ['int', 'char *', 'char *', 'char *', 'int', 'char *'],
// Last two are searchEntry.DN and seachEntry.attributes.length
'server-search-entry': ['int', 'char *', 'char *', 'char *', 'char *', 'int'],
'server-unbind-start': ['int', 'char *', 'char *', 'char *'],
'server-unbind-done': ['int', 'char *', 'char *', 'char *', 'int', 'char *'],
'server-abandon-start': ['int', 'char *', 'char *', 'char *'],
'server-abandon-done': ['int', 'char *', 'char *', 'char *', 'int', 'char *'],
// remote IP
'server-connection': ['char *']
}
/// --- API
module.exports = (function () {
if (!SERVER_PROVIDER) {
try {
const dtrace = require('dtrace-provider')
SERVER_PROVIDER = dtrace.createDTraceProvider('ldapjs')
Object.keys(SERVER_PROBES).forEach(function (p) {
const args = SERVER_PROBES[p].splice(0)
args.unshift(p)
dtrace.DTraceProvider.prototype.addProbe.apply(SERVER_PROVIDER, args)
})
} catch (e) {
SERVER_PROVIDER = {
fire: function () {
},
enable: function () {
},
addProbe: function () {
const p = {
fire: function () {
}
}
return (p)
},
removeProbe: function () {
},
disable: function () {
}
}
}
SERVER_PROVIDER.enable()
SERVER_PROVIDER._nextId = function () {
if (DTRACE_ID === MAX_INT) { DTRACE_ID = 0 }
return ++DTRACE_ID
}
}
return SERVER_PROVIDER
}())
@@ -0,0 +1,47 @@
'use strict'
module.exports = {
LDAP_SUCCESS: 0,
LDAP_OPERATIONS_ERROR: 1,
LDAP_PROTOCOL_ERROR: 2,
LDAP_TIME_LIMIT_EXCEEDED: 3,
LDAP_SIZE_LIMIT_EXCEEDED: 4,
LDAP_COMPARE_FALSE: 5,
LDAP_COMPARE_TRUE: 6,
LDAP_AUTH_METHOD_NOT_SUPPORTED: 7,
LDAP_STRONG_AUTH_REQUIRED: 8,
LDAP_REFERRAL: 10,
LDAP_ADMIN_LIMIT_EXCEEDED: 11,
LDAP_UNAVAILABLE_CRITICAL_EXTENSION: 12,
LDAP_CONFIDENTIALITY_REQUIRED: 13,
LDAP_SASL_BIND_IN_PROGRESS: 14,
LDAP_NO_SUCH_ATTRIBUTE: 16,
LDAP_UNDEFINED_ATTRIBUTE_TYPE: 17,
LDAP_INAPPROPRIATE_MATCHING: 18,
LDAP_CONSTRAINT_VIOLATION: 19,
LDAP_ATTRIBUTE_OR_VALUE_EXISTS: 20,
LDAP_INVALID_ATTRIBUTE_SYNTAX: 21,
LDAP_NO_SUCH_OBJECT: 32,
LDAP_ALIAS_PROBLEM: 33,
LDAP_INVALID_DN_SYNTAX: 34,
LDAP_ALIAS_DEREF_PROBLEM: 36,
LDAP_INAPPROPRIATE_AUTHENTICATION: 48,
LDAP_INVALID_CREDENTIALS: 49,
LDAP_INSUFFICIENT_ACCESS_RIGHTS: 50,
LDAP_BUSY: 51,
LDAP_UNAVAILABLE: 52,
LDAP_UNWILLING_TO_PERFORM: 53,
LDAP_LOOP_DETECT: 54,
LDAP_SORT_CONTROL_MISSING: 60,
LDAP_INDEX_RANGE_ERROR: 61,
LDAP_NAMING_VIOLATION: 64,
LDAP_OBJECTCLASS_VIOLATION: 65,
LDAP_NOT_ALLOWED_ON_NON_LEAF: 66,
LDAP_NOT_ALLOWED_ON_RDN: 67,
LDAP_ENTRY_ALREADY_EXISTS: 68,
LDAP_OBJECTCLASS_MODS_PROHIBITED: 69,
LDAP_AFFECTS_MULTIPLE_DSAS: 71,
LDAP_CONTROL_ERROR: 76,
LDAP_OTHER: 80,
LDAP_PROXIED_AUTHORIZATION_DENIED: 123
}
@@ -0,0 +1,147 @@
'use strict'
const util = require('util')
const assert = require('assert-plus')
const LDAPResult = require('../messages').LDAPResult
/// --- Globals
const CODES = require('./codes')
const ERRORS = []
/// --- Error Base class
function LDAPError (message, dn, caller) {
if (Error.captureStackTrace) { Error.captureStackTrace(this, caller || LDAPError) }
this.lde_message = message
this.lde_dn = dn
}
util.inherits(LDAPError, Error)
Object.defineProperties(LDAPError.prototype, {
name: {
get: function getName () { return 'LDAPError' },
configurable: false
},
code: {
get: function getCode () { return CODES.LDAP_OTHER },
configurable: false
},
message: {
get: function getMessage () {
return this.lde_message || this.name
},
set: function setMessage (message) {
this.lde_message = message
},
configurable: false
},
dn: {
get: function getDN () {
return (this.lde_dn ? this.lde_dn.toString() : '')
},
configurable: false
}
})
/// --- Exported API
module.exports = {}
module.exports.LDAPError = LDAPError
// Some whacky games here to make sure all the codes are exported
Object.keys(CODES).forEach(function (code) {
module.exports[code] = CODES[code]
if (code === 'LDAP_SUCCESS') { return }
let err = ''
let msg = ''
const pieces = code.split('_').slice(1)
for (let i = 0; i < pieces.length; i++) {
const lc = pieces[i].toLowerCase()
const key = lc.charAt(0).toUpperCase() + lc.slice(1)
err += key
msg += key + ((i + 1) < pieces.length ? ' ' : '')
}
if (!/\w+Error$/.test(err)) { err += 'Error' }
// At this point LDAP_OPERATIONS_ERROR is now OperationsError in $err
// and 'Operations Error' in $msg
module.exports[err] = function (message, dn, caller) {
LDAPError.call(this, message, dn, caller || module.exports[err])
}
module.exports[err].constructor = module.exports[err]
util.inherits(module.exports[err], LDAPError)
Object.defineProperties(module.exports[err].prototype, {
name: {
get: function getName () { return err },
configurable: false
},
code: {
get: function getCode () { return CODES[code] },
configurable: false
}
})
ERRORS[CODES[code]] = {
err: err,
message: msg
}
})
module.exports.getError = function (res) {
assert.ok(res instanceof LDAPResult, 'res (LDAPResult) required')
const errObj = ERRORS[res.status]
const E = module.exports[errObj.err]
return new E(res.errorMessage || errObj.message,
res.matchedDN || null,
module.exports.getError)
}
module.exports.getMessage = function (code) {
assert.number(code, 'code (number) required')
const errObj = ERRORS[code]
return (errObj && errObj.message ? errObj.message : '')
}
/// --- Custom application errors
function ConnectionError (message) {
LDAPError.call(this, message, null, ConnectionError)
}
util.inherits(ConnectionError, LDAPError)
module.exports.ConnectionError = ConnectionError
Object.defineProperties(ConnectionError.prototype, {
name: {
get: function () { return 'ConnectionError' },
configurable: false
}
})
function AbandonedError (message) {
LDAPError.call(this, message, null, AbandonedError)
}
util.inherits(AbandonedError, LDAPError)
module.exports.AbandonedError = AbandonedError
Object.defineProperties(AbandonedError.prototype, {
name: {
get: function () { return 'AbandonedError' },
configurable: false
}
})
function TimeoutError (message) {
LDAPError.call(this, message, null, TimeoutError)
}
util.inherits(TimeoutError, LDAPError)
module.exports.TimeoutError = TimeoutError
Object.defineProperties(TimeoutError.prototype, {
name: {
get: function () { return 'TimeoutError' },
configurable: false
}
})
@@ -0,0 +1,27 @@
// Copyright 2011 Mark Cavage, Inc. All rights reserved.
const assert = require('assert')
const util = require('util')
const parents = require('ldap-filter')
const Filter = require('./filter')
/// --- API
function AndFilter (options) {
parents.AndFilter.call(this, options)
}
util.inherits(AndFilter, parents.AndFilter)
Filter.mixin(AndFilter)
module.exports = AndFilter
AndFilter.prototype._toBer = function (ber) {
assert.ok(ber)
this.filters.forEach(function (f) {
ber = f.toBer(ber)
})
return ber
}
@@ -0,0 +1,35 @@
// Copyright 2011 Mark Cavage, Inc. All rights reserved.
const assert = require('assert')
const util = require('util')
const parents = require('ldap-filter')
const Filter = require('./filter')
/// --- API
function ApproximateFilter (options) {
parents.ApproximateFilter.call(this, options)
}
util.inherits(ApproximateFilter, parents.ApproximateFilter)
Filter.mixin(ApproximateFilter)
module.exports = ApproximateFilter
ApproximateFilter.prototype.parse = function (ber) {
assert.ok(ber)
this.attribute = ber.readString().toLowerCase()
this.value = ber.readString()
return true
}
ApproximateFilter.prototype._toBer = function (ber) {
assert.ok(ber)
ber.writeString(this.attribute)
ber.writeString(this.value)
return ber
}
@@ -0,0 +1,60 @@
// Copyright 2011 Mark Cavage, Inc. All rights reserved.
const assert = require('assert-plus')
const util = require('util')
const ASN1 = require('asn1').Ber
const parents = require('ldap-filter')
const Filter = require('./filter')
/// --- API
function EqualityFilter (options) {
parents.EqualityFilter.call(this, options)
}
util.inherits(EqualityFilter, parents.EqualityFilter)
Filter.mixin(EqualityFilter)
module.exports = EqualityFilter
EqualityFilter.prototype.matches = function (target, strictAttrCase) {
assert.object(target, 'target')
const tv = parents.getAttrValue(target, this.attribute, strictAttrCase)
let value = this.value
if (this.attribute.toLowerCase() === 'objectclass') {
/*
* Perform case-insensitive match for objectClass since nearly every LDAP
* implementation behaves in this manner.
*/
value = value.toLowerCase()
return parents.testValues(function (v) {
return value === v.toLowerCase()
}, tv)
} else {
return parents.testValues(function (v) {
return value === v
}, tv)
}
}
EqualityFilter.prototype.parse = function (ber) {
assert.ok(ber)
this.attribute = ber.readString().toLowerCase()
this.value = ber.readString(ASN1.OctetString, true)
if (this.attribute === 'objectclass') { this.value = this.value.toLowerCase() }
return true
}
EqualityFilter.prototype._toBer = function (ber) {
assert.ok(ber)
ber.writeString(this.attribute)
ber.writeBuffer(this.raw, ASN1.OctetString)
return ber
}
@@ -0,0 +1,44 @@
// Copyright 2011 Mark Cavage, Inc. All rights reserved.
/**
* RFC 2254 Escaping of filter strings
*
* Raw Escaped
* (o=Parens (R Us)) (o=Parens \28R Us\29)
* (cn=star*) (cn=star\2A)
* (filename=C:\MyFile) (filename=C:\5cMyFile)
*
* Use substr_filter to avoid having * ecsaped.
*
* @author [Austin King](https://github.com/ozten)
*/
exports.escape = function (inp) {
if (typeof (inp) === 'string') {
let esc = ''
for (let i = 0; i < inp.length; i++) {
switch (inp[i]) {
case '*':
esc += '\\2a'
break
case '(':
esc += '\\28'
break
case ')':
esc += '\\29'
break
case '\\':
esc += '\\5c'
break
case '\0':
esc += '\\00'
break
default:
esc += inp[i]
break
}
}
return esc
} else {
return inp
}
}
@@ -0,0 +1,59 @@
// Copyright 2011 Mark Cavage, Inc. All rights reserved.
const assert = require('assert')
const util = require('util')
const parents = require('ldap-filter')
const Filter = require('./filter')
// THIS IS A STUB!
//
// ldapjs does not support server side extensible matching.
// This class exists only for the client to send them.
/// --- API
function ExtensibleFilter (options) {
parents.ExtensibleFilter.call(this, options)
}
util.inherits(ExtensibleFilter, parents.ExtensibleFilter)
Filter.mixin(ExtensibleFilter)
module.exports = ExtensibleFilter
ExtensibleFilter.prototype.parse = function (ber) {
const end = ber.offset + ber.length
while (ber.offset < end) {
const tag = ber.peek()
switch (tag) {
case 0x81:
this.rule = ber.readString(tag)
break
case 0x82:
this.matchType = ber.readString(tag)
break
case 0x83:
this.value = ber.readString(tag)
break
case 0x84:
this.dnAttributes = ber.readBoolean(tag)
break
default:
throw new Error('Invalid ext_match filter type: 0x' + tag.toString(16))
}
}
return true
}
ExtensibleFilter.prototype._toBer = function (ber) {
assert.ok(ber)
if (this.rule) { ber.writeString(this.rule, 0x81) }
if (this.matchType) { ber.writeString(this.matchType, 0x82) }
ber.writeString(this.value, 0x83)
if (this.dnAttributes) { ber.writeBoolean(this.dnAttributes, 0x84) }
return ber
}
@@ -0,0 +1,60 @@
// Copyright 2011 Mark Cavage, Inc. All rights reserved.
// var assert = require('assert')
const Protocol = require('../protocol')
/// --- Globals
const TYPES = {
and: Protocol.FILTER_AND,
or: Protocol.FILTER_OR,
not: Protocol.FILTER_NOT,
equal: Protocol.FILTER_EQUALITY,
substring: Protocol.FILTER_SUBSTRINGS,
ge: Protocol.FILTER_GE,
le: Protocol.FILTER_LE,
present: Protocol.FILTER_PRESENT,
approx: Protocol.FILTER_APPROX,
ext: Protocol.FILTER_EXT
}
/// --- API
function isFilter (filter) {
if (!filter || typeof (filter) !== 'object') {
return false
}
// Do our best to duck-type it
if (typeof (filter.toBer) === 'function' &&
typeof (filter.matches) === 'function' &&
TYPES[filter.type] !== undefined) {
return true
}
return false
}
function isBerWriter (ber) {
return Boolean(
ber &&
typeof (ber) === 'object' &&
typeof (ber.startSequence) === 'function' &&
typeof (ber.endSequence) === 'function'
)
}
function mixin (target) {
target.prototype.toBer = function toBer (ber) {
if (isBerWriter(ber) === false) { throw new TypeError('ber (BerWriter) required') }
ber.startSequence(TYPES[this.type])
ber = this._toBer(ber)
ber.endSequence()
return ber
}
}
module.exports = {
isFilter: isFilter,
mixin: mixin
}
@@ -0,0 +1,35 @@
// Copyright 2011 Mark Cavage, Inc. All rights reserved.
const assert = require('assert')
const util = require('util')
const parents = require('ldap-filter')
const Filter = require('./filter')
/// --- API
function GreaterThanEqualsFilter (options) {
parents.GreaterThanEqualsFilter.call(this, options)
}
util.inherits(GreaterThanEqualsFilter, parents.GreaterThanEqualsFilter)
Filter.mixin(GreaterThanEqualsFilter)
module.exports = GreaterThanEqualsFilter
GreaterThanEqualsFilter.prototype.parse = function (ber) {
assert.ok(ber)
this.attribute = ber.readString().toLowerCase()
this.value = ber.readString()
return true
}
GreaterThanEqualsFilter.prototype._toBer = function (ber) {
assert.ok(ber)
ber.writeString(this.attribute)
ber.writeString(this.value)
return ber
}
@@ -0,0 +1,208 @@
// Copyright 2011 Mark Cavage, Inc. All rights reserved.
const assert = require('assert')
const asn1 = require('asn1')
const parents = require('ldap-filter')
const Protocol = require('../protocol')
const Filter = require('./filter')
const AndFilter = require('./and_filter')
const ApproximateFilter = require('./approx_filter')
const EqualityFilter = require('./equality_filter')
const ExtensibleFilter = require('./ext_filter')
const GreaterThanEqualsFilter = require('./ge_filter')
const LessThanEqualsFilter = require('./le_filter')
const NotFilter = require('./not_filter')
const OrFilter = require('./or_filter')
const PresenceFilter = require('./presence_filter')
const SubstringFilter = require('./substr_filter')
/// --- Globals
const BerReader = asn1.BerReader
/// --- Internal Parsers
/*
* A filter looks like this coming in:
* Filter ::= CHOICE {
* and [0] SET OF Filter,
* or [1] SET OF Filter,
* not [2] Filter,
* equalityMatch [3] AttributeValueAssertion,
* substrings [4] SubstringFilter,
* greaterOrEqual [5] AttributeValueAssertion,
* lessOrEqual [6] AttributeValueAssertion,
* present [7] AttributeType,
* approxMatch [8] AttributeValueAssertion,
* extensibleMatch [9] MatchingRuleAssertion --v3 only
* }
*
* SubstringFilter ::= SEQUENCE {
* type AttributeType,
* SEQUENCE OF CHOICE {
* initial [0] IA5String,
* any [1] IA5String,
* final [2] IA5String
* }
* }
*
* The extensibleMatch was added in LDAPv3:
*
* MatchingRuleAssertion ::= SEQUENCE {
* matchingRule [1] MatchingRuleID OPTIONAL,
* type [2] AttributeDescription OPTIONAL,
* matchValue [3] AssertionValue,
* dnAttributes [4] BOOLEAN DEFAULT FALSE
* }
*/
function _parse (ber) {
assert.ok(ber)
function parseSet (f) {
const end = ber.offset + ber.length
while (ber.offset < end) { f.addFilter(_parse(ber)) }
}
let f
const type = ber.readSequence()
switch (type) {
case Protocol.FILTER_AND:
f = new AndFilter()
parseSet(f)
break
case Protocol.FILTER_APPROX:
f = new ApproximateFilter()
f.parse(ber)
break
case Protocol.FILTER_EQUALITY:
f = new EqualityFilter()
f.parse(ber)
return f
case Protocol.FILTER_EXT:
f = new ExtensibleFilter()
f.parse(ber)
return f
case Protocol.FILTER_GE:
f = new GreaterThanEqualsFilter()
f.parse(ber)
return f
case Protocol.FILTER_LE:
f = new LessThanEqualsFilter()
f.parse(ber)
return f
case Protocol.FILTER_NOT:
f = new NotFilter({
filter: _parse(ber)
})
break
case Protocol.FILTER_OR:
f = new OrFilter()
parseSet(f)
break
case Protocol.FILTER_PRESENT:
f = new PresenceFilter()
f.parse(ber)
break
case Protocol.FILTER_SUBSTRINGS:
f = new SubstringFilter()
f.parse(ber)
break
default:
throw new Error('Invalid search filter type: 0x' + type.toString(16))
}
assert.ok(f)
return f
}
function cloneFilter (input) {
let child
if (input.type === 'and' || input.type === 'or') {
child = input.filters.map(cloneFilter)
} else if (input.type === 'not') {
child = cloneFilter(input.filter)
}
switch (input.type) {
case 'and':
return new AndFilter({ filters: child })
case 'or':
return new OrFilter({ filters: child })
case 'not':
return new NotFilter({ filter: child })
case 'equal':
return new EqualityFilter(input)
case 'substring':
return new SubstringFilter(input)
case 'ge':
return new GreaterThanEqualsFilter(input)
case 'le':
return new LessThanEqualsFilter(input)
case 'present':
return new PresenceFilter(input)
case 'approx':
return new ApproximateFilter(input)
case 'ext':
return new ExtensibleFilter(input)
default:
throw new Error('invalid filter type:' + input.type)
}
}
function escapedToHex (str) {
return str.replace(/\\([0-9a-f](?![0-9a-f])|[^0-9a-f]|$)/gi, function (match, p1) {
if (!p1) {
return '\\5c'
}
const hexCode = p1.charCodeAt(0).toString(16)
return '\\' + hexCode
})
}
function parseString (str) {
const hexStr = escapedToHex(str)
const generic = parents.parse(hexStr)
// The filter object(s) return from ldap-filter.parse lack the toBer/parse
// decoration that native ldapjs filter possess. cloneFilter adds that back.
return cloneFilter(generic)
}
/// --- API
module.exports = {
parse: function (ber) {
if (!ber || !(ber instanceof BerReader)) { throw new TypeError('ber (BerReader) required') }
return _parse(ber)
},
parseString: parseString,
isFilter: Filter.isFilter,
AndFilter: AndFilter,
ApproximateFilter: ApproximateFilter,
EqualityFilter: EqualityFilter,
ExtensibleFilter: ExtensibleFilter,
GreaterThanEqualsFilter: GreaterThanEqualsFilter,
LessThanEqualsFilter: LessThanEqualsFilter,
NotFilter: NotFilter,
OrFilter: OrFilter,
PresenceFilter: PresenceFilter,
SubstringFilter: SubstringFilter
}
@@ -0,0 +1,35 @@
// Copyright 2011 Mark Cavage, Inc. All rights reserved.
const assert = require('assert')
const util = require('util')
const parents = require('ldap-filter')
const Filter = require('./filter')
/// --- API
function LessThanEqualsFilter (options) {
parents.LessThanEqualsFilter.call(this, options)
}
util.inherits(LessThanEqualsFilter, parents.LessThanEqualsFilter)
Filter.mixin(LessThanEqualsFilter)
module.exports = LessThanEqualsFilter
LessThanEqualsFilter.prototype.parse = function (ber) {
assert.ok(ber)
this.attribute = ber.readString().toLowerCase()
this.value = ber.readString()
return true
}
LessThanEqualsFilter.prototype._toBer = function (ber) {
assert.ok(ber)
ber.writeString(this.attribute)
ber.writeString(this.value)
return ber
}
@@ -0,0 +1,23 @@
// Copyright 2011 Mark Cavage, Inc. All rights reserved.
const assert = require('assert')
const util = require('util')
const parents = require('ldap-filter')
const Filter = require('./filter')
/// --- API
function NotFilter (options) {
parents.NotFilter.call(this, options)
}
util.inherits(NotFilter, parents.NotFilter)
Filter.mixin(NotFilter)
module.exports = NotFilter
NotFilter.prototype._toBer = function (ber) {
assert.ok(ber)
return this.filter.toBer(ber)
}
@@ -0,0 +1,27 @@
// Copyright 2011 Mark Cavage, Inc. All rights reserved.
const assert = require('assert')
const util = require('util')
const parents = require('ldap-filter')
const Filter = require('./filter')
/// --- API
function OrFilter (options) {
parents.OrFilter.call(this, options)
}
util.inherits(OrFilter, parents.OrFilter)
Filter.mixin(OrFilter)
module.exports = OrFilter
OrFilter.prototype._toBer = function (ber) {
assert.ok(ber)
this.filters.forEach(function (f) {
ber = f.toBer(ber)
})
return ber
}
@@ -0,0 +1,36 @@
// Copyright 2011 Mark Cavage, Inc. All rights reserved.
const assert = require('assert')
const util = require('util')
const parents = require('ldap-filter')
const Filter = require('./filter')
/// --- API
function PresenceFilter (options) {
parents.PresenceFilter.call(this, options)
}
util.inherits(PresenceFilter, parents.PresenceFilter)
Filter.mixin(PresenceFilter)
module.exports = PresenceFilter
PresenceFilter.prototype.parse = function (ber) {
assert.ok(ber)
this.attribute =
ber.buffer.slice(0, ber.length).toString('utf8').toLowerCase()
ber._offset += ber.length
return true
}
PresenceFilter.prototype._toBer = function (ber) {
assert.ok(ber)
for (let i = 0; i < this.attribute.length; i++) { ber.writeByte(this.attribute.charCodeAt(i)) }
return ber
}
@@ -0,0 +1,70 @@
// Copyright 2011 Mark Cavage, Inc. All rights reserved.
const assert = require('assert')
const util = require('util')
const parents = require('ldap-filter')
const Filter = require('./filter')
/// --- API
function SubstringFilter (options) {
parents.SubstringFilter.call(this, options)
}
util.inherits(SubstringFilter, parents.SubstringFilter)
Filter.mixin(SubstringFilter)
module.exports = SubstringFilter
SubstringFilter.prototype.parse = function (ber) {
assert.ok(ber)
this.attribute = ber.readString().toLowerCase()
ber.readSequence()
const end = ber.offset + ber.length
while (ber.offset < end) {
const tag = ber.peek()
switch (tag) {
case 0x80: // Initial
this.initial = ber.readString(tag)
if (this.attribute === 'objectclass') { this.initial = this.initial.toLowerCase() }
break
case 0x81: { // Any
let anyVal = ber.readString(tag)
if (this.attribute === 'objectclass') { anyVal = anyVal.toLowerCase() }
this.any.push(anyVal)
break
}
case 0x82: // Final
this.final = ber.readString(tag)
if (this.attribute === 'objectclass') { this.final = this.final.toLowerCase() }
break
default:
throw new Error('Invalid substrings filter type: 0x' + tag.toString(16))
}
}
return true
}
SubstringFilter.prototype._toBer = function (ber) {
assert.ok(ber)
ber.writeString(this.attribute)
ber.startSequence()
if (this.initial) { ber.writeString(this.initial, 0x80) }
if (this.any && this.any.length) {
this.any.forEach(function (s) {
ber.writeString(s, 0x81)
})
}
if (this.final) { ber.writeString(this.final, 0x82) }
ber.endSequence()
return ber
}
@@ -0,0 +1,84 @@
// Copyright 2011 Mark Cavage, Inc. All rights reserved.
const logger = require('./logger')
const client = require('./client')
const Attribute = require('./attribute')
const Change = require('./change')
const Protocol = require('./protocol')
const Server = require('./server')
const controls = require('./controls')
const persistentSearch = require('./persistent_search')
const dn = require('./dn')
const errors = require('./errors')
const filters = require('./filters')
const messages = require('./messages')
const url = require('./url')
const hasOwnProperty = (target, val) => Object.prototype.hasOwnProperty.call(target, val)
/// --- API
module.exports = {
Client: client.Client,
createClient: client.createClient,
Server: Server,
createServer: function (options) {
if (options === undefined) { options = {} }
if (typeof (options) !== 'object') { throw new TypeError('options (object) required') }
if (!options.log) {
options.log = logger
}
return new Server(options)
},
Attribute: Attribute,
Change: Change,
dn: dn,
DN: dn.DN,
RDN: dn.RDN,
parseDN: dn.parse,
persistentSearch: persistentSearch,
PersistentSearchCache: persistentSearch.PersistentSearchCache,
filters: filters,
parseFilter: filters.parseString,
url: url,
parseURL: url.parse
}
/// --- Export all the childrenz
let k
for (k in Protocol) {
if (hasOwnProperty(Protocol, k)) { module.exports[k] = Protocol[k] }
}
for (k in messages) {
if (hasOwnProperty(messages, k)) { module.exports[k] = messages[k] }
}
for (k in controls) {
if (hasOwnProperty(controls, k)) { module.exports[k] = controls[k] }
}
for (k in filters) {
if (hasOwnProperty(filters, k)) {
if (k !== 'parse' && k !== 'parseString') { module.exports[k] = filters[k] }
}
}
for (k in errors) {
if (hasOwnProperty(errors, k)) {
module.exports[k] = errors[k]
}
}
@@ -0,0 +1,6 @@
'use strict'
const logger = require('abstract-logging')
logger.child = function () { return logger }
module.exports = logger
@@ -0,0 +1,87 @@
// Copyright 2011 Mark Cavage, Inc. All rights reserved.
const assert = require('assert-plus')
const util = require('util')
const LDAPMessage = require('./message')
const Protocol = require('../protocol')
/// --- API
function AbandonRequest (options) {
options = options || {}
assert.object(options)
assert.optionalNumber(options.abandonID)
options.protocolOp = Protocol.LDAP_REQ_ABANDON
LDAPMessage.call(this, options)
this.abandonID = options.abandonID || 0
}
util.inherits(AbandonRequest, LDAPMessage)
Object.defineProperties(AbandonRequest.prototype, {
type: {
get: function getType () { return 'AbandonRequest' },
configurable: false
}
})
AbandonRequest.prototype._parse = function (ber, length) {
assert.ok(ber)
assert.ok(length)
// What a PITA - have to replicate ASN.1 integer logic to work around the
// way abandon is encoded and the way ldapjs framework handles "normal"
// messages
const buf = ber.buffer
let offset = 0
let value = 0
const fb = buf[offset++]
value = fb & 0x7F
for (let i = 1; i < length; i++) {
value <<= 8
value |= (buf[offset++] & 0xff)
}
if ((fb & 0x80) === 0x80) { value = -value }
ber._offset += length
this.abandonID = value
return true
}
AbandonRequest.prototype._toBer = function (ber) {
assert.ok(ber)
let i = this.abandonID
let sz = 4
while ((((i & 0xff800000) === 0) || ((i & 0xff800000) === 0xff800000)) &&
(sz > 1)) {
sz--
i <<= 8
}
assert.ok(sz <= 4)
while (sz-- > 0) {
ber.writeByte((i & 0xff000000) >> 24)
i <<= 8
}
return ber
}
AbandonRequest.prototype._json = function (j) {
assert.ok(j)
j.abandonID = this.abandonID
return j
}
/// --- Exports
module.exports = AbandonRequest
@@ -0,0 +1,34 @@
// Copyright 2011 Mark Cavage, Inc. All rights reserved.
const assert = require('assert-plus')
const util = require('util')
const LDAPMessage = require('./result')
// var Protocol = require('../protocol')
/// --- API
function AbandonResponse (options) {
options = options || {}
assert.object(options)
options.protocolOp = 0
LDAPMessage.call(this, options)
}
util.inherits(AbandonResponse, LDAPMessage)
Object.defineProperties(AbandonResponse.prototype, {
type: {
get: function getType () { return 'AbandonResponse' },
configurable: false
}
})
AbandonResponse.prototype.end = function (_status) {}
AbandonResponse.prototype._json = function (j) {
return j
}
/// --- Exports
module.exports = AbandonResponse
@@ -0,0 +1,159 @@
// Copyright 2011 Mark Cavage, Inc. All rights reserved.
const assert = require('assert-plus')
const util = require('util')
const LDAPMessage = require('./message')
const Attribute = require('../attribute')
const Protocol = require('../protocol')
const lassert = require('../assert')
/// --- API
function AddRequest (options) {
options = options || {}
assert.object(options)
lassert.optionalStringDN(options.entry)
lassert.optionalArrayOfAttribute(options.attributes)
options.protocolOp = Protocol.LDAP_REQ_ADD
LDAPMessage.call(this, options)
this.entry = options.entry || null
this.attributes = options.attributes ? options.attributes.slice(0) : []
}
util.inherits(AddRequest, LDAPMessage)
Object.defineProperties(AddRequest.prototype, {
type: {
get: function getType () { return 'AddRequest' },
configurable: false
},
_dn: {
get: function getDN () { return this.entry },
configurable: false
}
})
AddRequest.prototype._parse = function (ber) {
assert.ok(ber)
this.entry = ber.readString()
ber.readSequence()
const end = ber.offset + ber.length
while (ber.offset < end) {
const a = new Attribute()
a.parse(ber)
a.type = a.type.toLowerCase()
if (a.type === 'objectclass') {
for (let i = 0; i < a.vals.length; i++) { a.vals[i] = a.vals[i].toLowerCase() }
}
this.attributes.push(a)
}
this.attributes.sort(Attribute.compare)
return true
}
AddRequest.prototype._toBer = function (ber) {
assert.ok(ber)
ber.writeString(this.entry.toString())
ber.startSequence()
this.attributes.forEach(function (a) {
a.toBer(ber)
})
ber.endSequence()
return ber
}
AddRequest.prototype._json = function (j) {
assert.ok(j)
j.entry = this.entry.toString()
j.attributes = []
this.attributes.forEach(function (a) {
j.attributes.push(a.json)
})
return j
}
AddRequest.prototype.indexOf = function (attr) {
if (!attr || typeof (attr) !== 'string') { throw new TypeError('attr (string) required') }
for (let i = 0; i < this.attributes.length; i++) {
if (this.attributes[i].type === attr) { return i }
}
return -1
}
AddRequest.prototype.attributeNames = function () {
const attrs = []
for (let i = 0; i < this.attributes.length; i++) { attrs.push(this.attributes[i].type.toLowerCase()) }
return attrs
}
AddRequest.prototype.getAttribute = function (name) {
if (!name || typeof (name) !== 'string') { throw new TypeError('attribute name (string) required') }
name = name.toLowerCase()
for (let i = 0; i < this.attributes.length; i++) {
if (this.attributes[i].type === name) { return this.attributes[i] }
}
return null
}
AddRequest.prototype.addAttribute = function (attr) {
if (!(attr instanceof Attribute)) { throw new TypeError('attribute (Attribute) required') }
return this.attributes.push(attr)
}
/**
* Returns a "pure" JS representation of this object.
*
* An example object would look like:
*
* {
* "dn": "cn=unit, dc=test",
* "attributes": {
* "cn": ["unit", "foo"],
* "objectclass": ["top", "person"]
* }
* }
*
* @return {Object} that looks like the above.
*/
AddRequest.prototype.toObject = function () {
const self = this
const obj = {
dn: self.entry ? self.entry.toString() : '',
attributes: {}
}
if (!this.attributes || !this.attributes.length) { return obj }
this.attributes.forEach(function (a) {
if (!obj.attributes[a.type]) { obj.attributes[a.type] = [] }
a.vals.forEach(function (v) {
if (obj.attributes[a.type].indexOf(v) === -1) { obj.attributes[a.type].push(v) }
})
})
return obj
}
/// --- Exports
module.exports = AddRequest
@@ -0,0 +1,22 @@
// Copyright 2011 Mark Cavage, Inc. All rights reserved.
const assert = require('assert-plus')
const util = require('util')
const LDAPResult = require('./result')
const Protocol = require('../protocol')
/// --- API
function AddResponse (options) {
options = options || {}
assert.object(options)
options.protocolOp = Protocol.LDAP_REP_ADD
LDAPResult.call(this, options)
}
util.inherits(AddResponse, LDAPResult)
/// --- Exports
module.exports = AddResponse
@@ -0,0 +1,84 @@
// Copyright 2011 Mark Cavage, Inc. All rights reserved.
const assert = require('assert-plus')
const util = require('util')
const asn1 = require('asn1')
const LDAPMessage = require('./message')
const Protocol = require('../protocol')
/// --- Globals
const Ber = asn1.Ber
const LDAP_BIND_SIMPLE = 'simple'
// var LDAP_BIND_SASL = 'sasl'
/// --- API
function BindRequest (options) {
options = options || {}
assert.object(options)
options.protocolOp = Protocol.LDAP_REQ_BIND
LDAPMessage.call(this, options)
this.version = options.version || 0x03
this.name = options.name || null
this.authentication = options.authentication || LDAP_BIND_SIMPLE
this.credentials = options.credentials || ''
}
util.inherits(BindRequest, LDAPMessage)
Object.defineProperties(BindRequest.prototype, {
type: {
get: function getType () { return 'BindRequest' },
configurable: false
},
_dn: {
get: function getDN () { return this.name },
configurable: false
}
})
BindRequest.prototype._parse = function (ber) {
assert.ok(ber)
this.version = ber.readInt()
this.name = ber.readString()
const t = ber.peek()
// TODO add support for SASL et al
if (t !== Ber.Context) { throw new Error('authentication 0x' + t.toString(16) + ' not supported') }
this.authentication = LDAP_BIND_SIMPLE
this.credentials = ber.readString(Ber.Context)
return true
}
BindRequest.prototype._toBer = function (ber) {
assert.ok(ber)
ber.writeInt(this.version)
ber.writeString((this.name || '').toString())
// TODO add support for SASL et al
ber.writeString((this.credentials || ''), Ber.Context)
return ber
}
BindRequest.prototype._json = function (j) {
assert.ok(j)
j.version = this.version
j.name = this.name
j.authenticationType = this.authentication
j.credentials = this.credentials
return j
}
/// --- Exports
module.exports = BindRequest
@@ -0,0 +1,22 @@
// Copyright 2011 Mark Cavage, Inc. All rights reserved.
const assert = require('assert-plus')
const util = require('util')
const LDAPResult = require('./result')
const Protocol = require('../protocol')
/// --- API
function BindResponse (options) {
options = options || {}
assert.object(options)
options.protocolOp = Protocol.LDAP_REP_BIND
LDAPResult.call(this, options)
}
util.inherits(BindResponse, LDAPResult)
/// --- Exports
module.exports = BindResponse
@@ -0,0 +1,74 @@
// Copyright 2011 Mark Cavage, Inc. All rights reserved.
const assert = require('assert-plus')
const util = require('util')
const LDAPMessage = require('./message')
const Protocol = require('../protocol')
const lassert = require('../assert')
/// --- API
function CompareRequest (options) {
options = options || {}
assert.object(options)
assert.optionalString(options.attribute)
assert.optionalString(options.value)
lassert.optionalStringDN(options.entry)
options.protocolOp = Protocol.LDAP_REQ_COMPARE
LDAPMessage.call(this, options)
this.entry = options.entry || null
this.attribute = options.attribute || ''
this.value = options.value || ''
}
util.inherits(CompareRequest, LDAPMessage)
Object.defineProperties(CompareRequest.prototype, {
type: {
get: function getType () { return 'CompareRequest' },
configurable: false
},
_dn: {
get: function getDN () { return this.entry },
configurable: false
}
})
CompareRequest.prototype._parse = function (ber) {
assert.ok(ber)
this.entry = ber.readString()
ber.readSequence()
this.attribute = ber.readString().toLowerCase()
this.value = ber.readString()
return true
}
CompareRequest.prototype._toBer = function (ber) {
assert.ok(ber)
ber.writeString(this.entry.toString())
ber.startSequence()
ber.writeString(this.attribute)
ber.writeString(this.value)
ber.endSequence()
return ber
}
CompareRequest.prototype._json = function (j) {
assert.ok(j)
j.entry = this.entry.toString()
j.attribute = this.attribute
j.value = this.value
return j
}
/// --- Exports
module.exports = CompareRequest
@@ -0,0 +1,33 @@
// Copyright 2011 Mark Cavage, Inc. All rights reserved.
const assert = require('assert-plus')
const util = require('util')
const LDAPResult = require('./result')
const Protocol = require('../protocol')
/// --- API
function CompareResponse (options) {
options = options || {}
assert.object(options)
options.protocolOp = Protocol.LDAP_REP_COMPARE
LDAPResult.call(this, options)
}
util.inherits(CompareResponse, LDAPResult)
CompareResponse.prototype.end = function (matches) {
let status = 0x06
if (typeof (matches) === 'boolean') {
if (!matches) { status = 0x05 } // Compare false
} else {
status = matches
}
return LDAPResult.prototype.end.call(this, status)
}
/// --- Exports
module.exports = CompareResponse
@@ -0,0 +1,62 @@
// Copyright 2011 Mark Cavage, Inc. All rights reserved.
const assert = require('assert-plus')
const util = require('util')
const LDAPMessage = require('./message')
const Protocol = require('../protocol')
const lassert = require('../assert')
/// --- API
function DeleteRequest (options) {
options = options || {}
assert.object(options)
lassert.optionalStringDN(options.entry)
options.protocolOp = Protocol.LDAP_REQ_DELETE
LDAPMessage.call(this, options)
this.entry = options.entry || null
}
util.inherits(DeleteRequest, LDAPMessage)
Object.defineProperties(DeleteRequest.prototype, {
type: {
get: function getType () { return 'DeleteRequest' },
configurable: false
},
_dn: {
get: function getDN () { return this.entry },
configurable: false
}
})
DeleteRequest.prototype._parse = function (ber, length) {
assert.ok(ber)
this.entry = ber.buffer.slice(0, length).toString('utf8')
ber._offset += ber.length
return true
}
DeleteRequest.prototype._toBer = function (ber) {
assert.ok(ber)
const buf = Buffer.from(this.entry.toString())
for (let i = 0; i < buf.length; i++) { ber.writeByte(buf[i]) }
return ber
}
DeleteRequest.prototype._json = function (j) {
assert.ok(j)
j.entry = this.entry
return j
}
/// --- Exports
module.exports = DeleteRequest
@@ -0,0 +1,22 @@
// Copyright 2011 Mark Cavage, Inc. All rights reserved.
const assert = require('assert-plus')
const util = require('util')
const LDAPResult = require('./result')
const Protocol = require('../protocol')
/// --- API
function DeleteResponse (options) {
options = options || {}
assert.object(options)
options.protocolOp = Protocol.LDAP_REP_DELETE
LDAPResult.call(this, options)
}
util.inherits(DeleteResponse, LDAPResult)
/// --- Exports
module.exports = DeleteResponse
@@ -0,0 +1,117 @@
// Copyright 2011 Mark Cavage, Inc. All rights reserved.
const assert = require('assert-plus')
const util = require('util')
const LDAPMessage = require('./message')
const Protocol = require('../protocol')
/// --- API
function ExtendedRequest (options) {
options = options || {}
assert.object(options)
assert.optionalString(options.requestName)
if (options.requestValue &&
!(Buffer.isBuffer(options.requestValue) ||
typeof (options.requestValue) === 'string')) {
throw new TypeError('options.requestValue must be a buffer or a string')
}
options.protocolOp = Protocol.LDAP_REQ_EXTENSION
LDAPMessage.call(this, options)
this.requestName = options.requestName || ''
this.requestValue = options.requestValue
if (Buffer.isBuffer(this.requestValue)) {
this.requestValueBuffer = this.requestValue
} else {
this.requestValueBuffer = Buffer.from(this.requestValue || '', 'utf8')
}
}
util.inherits(ExtendedRequest, LDAPMessage)
Object.defineProperties(ExtendedRequest.prototype, {
type: {
get: function getType () { return 'ExtendedRequest' },
configurable: false
},
_dn: {
get: function getDN () { return this.requestName },
configurable: false
},
name: {
get: function getName () { return this.requestName },
set: function setName (val) {
assert.string(val)
this.requestName = val
},
configurable: false
},
value: {
get: function getValue () { return this.requestValue },
set: function setValue (val) {
if (!(Buffer.isBuffer(val) || typeof (val) === 'string')) { throw new TypeError('value must be a buffer or a string') }
if (Buffer.isBuffer(val)) {
this.requestValueBuffer = val
} else {
this.requestValueBuffer = Buffer.from(val, 'utf8')
}
this.requestValue = val
},
configurable: false
},
valueBuffer: {
get: function getValueBuffer () {
return this.requestValueBuffer
},
set: function setValueBuffer (val) {
if (!Buffer.isBuffer(val)) { throw new TypeError('valueBuffer must be a buffer') }
this.value = val
},
configurable: false
}
})
ExtendedRequest.prototype._parse = function (ber) {
assert.ok(ber)
this.requestName = ber.readString(0x80)
if (ber.peek() === 0x81) {
this.requestValueBuffer = ber.readString(0x81, true)
this.requestValue = this.requestValueBuffer.toString('utf8')
}
return true
}
ExtendedRequest.prototype._toBer = function (ber) {
assert.ok(ber)
ber.writeString(this.requestName, 0x80)
if (Buffer.isBuffer(this.requestValue)) {
ber.writeBuffer(this.requestValue, 0x81)
} else if (typeof (this.requestValue) === 'string') {
ber.writeString(this.requestValue, 0x81)
}
return ber
}
ExtendedRequest.prototype._json = function (j) {
assert.ok(j)
j.requestName = this.requestName
j.requestValue = (Buffer.isBuffer(this.requestValue))
? this.requestValue.toString('hex')
: this.requestValue
return j
}
/// --- Exports
module.exports = ExtendedRequest
@@ -0,0 +1,86 @@
// Copyright 2011 Mark Cavage, Inc. All rights reserved.
const assert = require('assert-plus')
const util = require('util')
const LDAPResult = require('./result')
const Protocol = require('../protocol')
/// --- API
function ExtendedResponse (options) {
options = options || {}
assert.object(options)
assert.optionalString(options.responseName)
assert.optionalString(options.responsevalue)
this.responseName = options.responseName || undefined
this.responseValue = options.responseValue || undefined
options.protocolOp = Protocol.LDAP_REP_EXTENSION
LDAPResult.call(this, options)
}
util.inherits(ExtendedResponse, LDAPResult)
Object.defineProperties(ExtendedResponse.prototype, {
type: {
get: function getType () { return 'ExtendedResponse' },
configurable: false
},
_dn: {
get: function getDN () { return this.responseName },
configurable: false
},
name: {
get: function getName () { return this.responseName },
set: function setName (val) {
assert.string(val)
this.responseName = val
},
configurable: false
},
value: {
get: function getValue () { return this.responseValue },
set: function (val) {
assert.string(val)
this.responseValue = val
},
configurable: false
}
})
ExtendedResponse.prototype._parse = function (ber) {
assert.ok(ber)
if (!LDAPResult.prototype._parse.call(this, ber)) { return false }
if (ber.peek() === 0x8a) { this.responseName = ber.readString(0x8a) }
if (ber.peek() === 0x8b) { this.responseValue = ber.readString(0x8b) }
return true
}
ExtendedResponse.prototype._toBer = function (ber) {
assert.ok(ber)
if (!LDAPResult.prototype._toBer.call(this, ber)) { return false }
if (this.responseName) { ber.writeString(this.responseName, 0x8a) }
if (this.responseValue) { ber.writeString(this.responseValue, 0x8b) }
return ber
}
ExtendedResponse.prototype._json = function (j) {
assert.ok(j)
j = LDAPResult.prototype._json.call(this, j)
j.responseName = this.responseName
j.responseValue = this.responseValue
return j
}
/// --- Exports
module.exports = ExtendedResponse
@@ -0,0 +1,61 @@
// Copyright 2011 Mark Cavage, Inc. All rights reserved.
const LDAPMessage = require('./message')
const LDAPResult = require('./result')
const Parser = require('./parser')
const AbandonRequest = require('./abandon_request')
const AbandonResponse = require('./abandon_response')
const AddRequest = require('./add_request')
const AddResponse = require('./add_response')
const BindRequest = require('./bind_request')
const BindResponse = require('./bind_response')
const CompareRequest = require('./compare_request')
const CompareResponse = require('./compare_response')
const DeleteRequest = require('./del_request')
const DeleteResponse = require('./del_response')
const ExtendedRequest = require('./ext_request')
const ExtendedResponse = require('./ext_response')
const ModifyRequest = require('./modify_request')
const ModifyResponse = require('./modify_response')
const ModifyDNRequest = require('./moddn_request')
const ModifyDNResponse = require('./moddn_response')
const SearchRequest = require('./search_request')
const SearchEntry = require('./search_entry')
const SearchReference = require('./search_reference')
const SearchResponse = require('./search_response')
const UnbindRequest = require('./unbind_request')
const UnbindResponse = require('./unbind_response')
/// --- API
module.exports = {
LDAPMessage: LDAPMessage,
LDAPResult: LDAPResult,
Parser: Parser,
AbandonRequest: AbandonRequest,
AbandonResponse: AbandonResponse,
AddRequest: AddRequest,
AddResponse: AddResponse,
BindRequest: BindRequest,
BindResponse: BindResponse,
CompareRequest: CompareRequest,
CompareResponse: CompareResponse,
DeleteRequest: DeleteRequest,
DeleteResponse: DeleteResponse,
ExtendedRequest: ExtendedRequest,
ExtendedResponse: ExtendedResponse,
ModifyRequest: ModifyRequest,
ModifyResponse: ModifyResponse,
ModifyDNRequest: ModifyDNRequest,
ModifyDNResponse: ModifyDNResponse,
SearchRequest: SearchRequest,
SearchEntry: SearchEntry,
SearchReference: SearchReference,
SearchResponse: SearchResponse,
UnbindRequest: UnbindRequest,
UnbindResponse: UnbindResponse
}
@@ -0,0 +1,110 @@
// Copyright 2011 Mark Cavage, Inc. All rights reserved.
const assert = require('assert-plus')
const util = require('util')
const asn1 = require('asn1')
const logger = require('../logger')
// var Control = require('../controls').Control
// var Protocol = require('../protocol')
/// --- Globals
// var Ber = asn1.Ber
// var BerReader = asn1.BerReader
const BerWriter = asn1.BerWriter
const getControl = require('../controls').getControl
/// --- API
/**
* LDAPMessage structure.
*
* @param {Object} options stuff.
*/
function LDAPMessage (options) {
assert.object(options)
this.messageID = options.messageID || 0
this.protocolOp = options.protocolOp || undefined
this.controls = options.controls ? options.controls.slice(0) : []
this.log = options.log || logger
}
Object.defineProperties(LDAPMessage.prototype, {
id: {
get: function getId () { return this.messageID },
configurable: false
},
dn: {
get: function getDN () { return this._dn || '' },
configurable: false
},
type: {
get: function getType () { return 'LDAPMessage' },
configurable: false
},
json: {
get: function () {
const out = this._json({
messageID: this.messageID,
protocolOp: this.type
})
out.controls = this.controls
return out
},
configurable: false
}
})
LDAPMessage.prototype.toString = function () {
return JSON.stringify(this.json)
}
LDAPMessage.prototype.parse = function (ber) {
assert.ok(ber)
this.log.trace('parse: data=%s', util.inspect(ber.buffer))
// Delegate off to the specific type to parse
this._parse(ber, ber.length)
// Look for controls
if (ber.peek() === 0xa0) {
ber.readSequence()
const end = ber.offset + ber.length
while (ber.offset < end) {
const c = getControl(ber)
if (c) { this.controls.push(c) }
}
}
this.log.trace('Parsing done: %j', this.json)
return true
}
LDAPMessage.prototype.toBer = function () {
let writer = new BerWriter()
writer.startSequence()
writer.writeInt(this.messageID)
writer.startSequence(this.protocolOp)
if (this._toBer) { writer = this._toBer(writer) }
writer.endSequence()
if (this.controls && this.controls.length) {
writer.startSequence(0xa0)
this.controls.forEach(function (c) {
c.toBer(writer)
})
writer.endSequence()
}
writer.endSequence()
return writer.buffer
}
/// --- Exports
module.exports = LDAPMessage
@@ -0,0 +1,85 @@
// Copyright 2011 Mark Cavage, Inc. All rights reserved.
const assert = require('assert-plus')
const util = require('util')
const LDAPMessage = require('./message')
const Protocol = require('../protocol')
const dn = require('../dn')
const lassert = require('../assert')
/// --- API
function ModifyDNRequest (options) {
options = options || {}
assert.object(options)
assert.optionalBool(options.deleteOldRdn)
lassert.optionalStringDN(options.entry)
lassert.optionalDN(options.newRdn)
lassert.optionalDN(options.newSuperior)
options.protocolOp = Protocol.LDAP_REQ_MODRDN
LDAPMessage.call(this, options)
this.entry = options.entry || null
this.newRdn = options.newRdn || null
this.deleteOldRdn = options.deleteOldRdn || true
this.newSuperior = options.newSuperior || null
}
util.inherits(ModifyDNRequest, LDAPMessage)
Object.defineProperties(ModifyDNRequest.prototype, {
type: {
get: function getType () { return 'ModifyDNRequest' },
configurable: false
},
_dn: {
get: function getDN () { return this.entry },
configurable: false
}
})
ModifyDNRequest.prototype._parse = function (ber) {
assert.ok(ber)
this.entry = ber.readString()
this.newRdn = dn.parse(ber.readString())
this.deleteOldRdn = ber.readBoolean()
if (ber.peek() === 0x80) { this.newSuperior = dn.parse(ber.readString(0x80)) }
return true
}
ModifyDNRequest.prototype._toBer = function (ber) {
// assert.ok(ber);
ber.writeString(this.entry.toString())
ber.writeString(this.newRdn.toString())
ber.writeBoolean(this.deleteOldRdn)
if (this.newSuperior) {
const s = this.newSuperior.toString()
const len = Buffer.byteLength(s)
ber.writeByte(0x80) // MODIFY_DN_REQUEST_NEW_SUPERIOR_TAG
ber.writeLength(len)
ber._ensure(len)
ber._buf.write(s, ber._offset)
ber._offset += len
}
return ber
}
ModifyDNRequest.prototype._json = function (j) {
assert.ok(j)
j.entry = this.entry.toString()
j.newRdn = this.newRdn.toString()
j.deleteOldRdn = this.deleteOldRdn
j.newSuperior = this.newSuperior ? this.newSuperior.toString() : ''
return j
}
/// --- Exports
module.exports = ModifyDNRequest
@@ -0,0 +1,22 @@
// Copyright 2011 Mark Cavage, Inc. All rights reserved.
const assert = require('assert-plus')
const util = require('util')
const LDAPResult = require('./result')
const Protocol = require('../protocol')
/// --- API
function ModifyDNResponse (options) {
options = options || {}
assert.object(options)
options.protocolOp = Protocol.LDAP_REP_MODRDN
LDAPResult.call(this, options)
}
util.inherits(ModifyDNResponse, LDAPResult)
/// --- Exports
module.exports = ModifyDNResponse
@@ -0,0 +1,83 @@
// Copyright 2011 Mark Cavage, Inc. All rights reserved.
const assert = require('assert-plus')
const util = require('util')
const LDAPMessage = require('./message')
const Change = require('../change')
const Protocol = require('../protocol')
const lassert = require('../assert')
/// --- API
function ModifyRequest (options) {
options = options || {}
assert.object(options)
lassert.optionalStringDN(options.object)
lassert.optionalArrayOfAttribute(options.attributes)
options.protocolOp = Protocol.LDAP_REQ_MODIFY
LDAPMessage.call(this, options)
this.object = options.object || null
this.changes = options.changes ? options.changes.slice(0) : []
}
util.inherits(ModifyRequest, LDAPMessage)
Object.defineProperties(ModifyRequest.prototype, {
type: {
get: function getType () { return 'ModifyRequest' },
configurable: false
},
_dn: {
get: function getDN () { return this.object },
configurable: false
}
})
ModifyRequest.prototype._parse = function (ber) {
assert.ok(ber)
this.object = ber.readString()
ber.readSequence()
const end = ber.offset + ber.length
while (ber.offset < end) {
const c = new Change()
c.parse(ber)
c.modification.type = c.modification.type.toLowerCase()
this.changes.push(c)
}
this.changes.sort(Change.compare)
return true
}
ModifyRequest.prototype._toBer = function (ber) {
assert.ok(ber)
ber.writeString(this.object.toString())
ber.startSequence()
this.changes.forEach(function (c) {
c.toBer(ber)
})
ber.endSequence()
return ber
}
ModifyRequest.prototype._json = function (j) {
assert.ok(j)
j.object = this.object
j.changes = []
this.changes.forEach(function (c) {
j.changes.push(c.json)
})
return j
}
/// --- Exports
module.exports = ModifyRequest
@@ -0,0 +1,22 @@
// Copyright 2011 Mark Cavage, Inc. All rights reserved.
const assert = require('assert-plus')
const util = require('util')
const LDAPResult = require('./result')
const Protocol = require('../protocol')
/// --- API
function ModifyResponse (options) {
options = options || {}
assert.object(options)
options.protocolOp = Protocol.LDAP_REP_MODIFY
LDAPResult.call(this, options)
}
util.inherits(ModifyResponse, LDAPResult)
/// --- Exports
module.exports = ModifyResponse
@@ -0,0 +1,221 @@
// Copyright 2011 Mark Cavage, Inc. All rights reserved.
const EventEmitter = require('events').EventEmitter
const util = require('util')
const assert = require('assert-plus')
const asn1 = require('asn1')
// var VError = require('verror').VError
const logger = require('../logger')
const AbandonRequest = require('./abandon_request')
const AddRequest = require('./add_request')
const AddResponse = require('./add_response')
const BindRequest = require('./bind_request')
const BindResponse = require('./bind_response')
const CompareRequest = require('./compare_request')
const CompareResponse = require('./compare_response')
const DeleteRequest = require('./del_request')
const DeleteResponse = require('./del_response')
const ExtendedRequest = require('./ext_request')
const ExtendedResponse = require('./ext_response')
const ModifyRequest = require('./modify_request')
const ModifyResponse = require('./modify_response')
const ModifyDNRequest = require('./moddn_request')
const ModifyDNResponse = require('./moddn_response')
const SearchRequest = require('./search_request')
const SearchEntry = require('./search_entry')
const SearchReference = require('./search_reference')
const SearchResponse = require('./search_response')
const UnbindRequest = require('./unbind_request')
// var UnbindResponse = require('./unbind_response')
const LDAPResult = require('./result')
// var Message = require('./message')
const Protocol = require('../protocol')
/// --- Globals
// var Ber = asn1.Ber
const BerReader = asn1.BerReader
/// --- API
function Parser (options = {}) {
assert.object(options)
EventEmitter.call(this)
this.buffer = null
this.log = options.log || logger
}
util.inherits(Parser, EventEmitter)
Parser.prototype.write = function (data) {
if (!data || !Buffer.isBuffer(data)) { throw new TypeError('data (buffer) required') }
let nextMessage = null
const self = this
function end () {
if (nextMessage) { return self.write(nextMessage) }
return true
}
self.buffer = (self.buffer ? Buffer.concat([self.buffer, data]) : data)
const ber = new BerReader(self.buffer)
let foundSeq = false
try {
foundSeq = ber.readSequence()
} catch (e) {
this.emit('error', e)
}
if (!foundSeq || ber.remain < ber.length) {
// ENOTENOUGH
return false
} else if (ber.remain > ber.length) {
// ETOOMUCH
// This is sort of ugly, but allows us to make miminal copies
nextMessage = self.buffer.slice(ber.offset + ber.length)
ber._size = ber.offset + ber.length
assert.equal(ber.remain, ber.length)
}
// If we're here, ber holds the message, and nextMessage is temporarily
// pointing at the next sequence of data (if it exists)
self.buffer = null
let message
try {
// Bail here if peer isn't speaking protocol at all
message = this.getMessage(ber)
if (!message) {
return end()
}
message.parse(ber)
} catch (e) {
this.emit('error', e, message)
return false
}
this.emit('message', message)
return end()
}
Parser.prototype.getMessage = function (ber) {
assert.ok(ber)
const self = this
const messageID = ber.readInt()
const type = ber.readSequence()
let Message
switch (type) {
case Protocol.LDAP_REQ_ABANDON:
Message = AbandonRequest
break
case Protocol.LDAP_REQ_ADD:
Message = AddRequest
break
case Protocol.LDAP_REP_ADD:
Message = AddResponse
break
case Protocol.LDAP_REQ_BIND:
Message = BindRequest
break
case Protocol.LDAP_REP_BIND:
Message = BindResponse
break
case Protocol.LDAP_REQ_COMPARE:
Message = CompareRequest
break
case Protocol.LDAP_REP_COMPARE:
Message = CompareResponse
break
case Protocol.LDAP_REQ_DELETE:
Message = DeleteRequest
break
case Protocol.LDAP_REP_DELETE:
Message = DeleteResponse
break
case Protocol.LDAP_REQ_EXTENSION:
Message = ExtendedRequest
break
case Protocol.LDAP_REP_EXTENSION:
Message = ExtendedResponse
break
case Protocol.LDAP_REQ_MODIFY:
Message = ModifyRequest
break
case Protocol.LDAP_REP_MODIFY:
Message = ModifyResponse
break
case Protocol.LDAP_REQ_MODRDN:
Message = ModifyDNRequest
break
case Protocol.LDAP_REP_MODRDN:
Message = ModifyDNResponse
break
case Protocol.LDAP_REQ_SEARCH:
Message = SearchRequest
break
case Protocol.LDAP_REP_SEARCH_ENTRY:
Message = SearchEntry
break
case Protocol.LDAP_REP_SEARCH_REF:
Message = SearchReference
break
case Protocol.LDAP_REP_SEARCH:
Message = SearchResponse
break
case Protocol.LDAP_REQ_UNBIND:
Message = UnbindRequest
break
default:
this.emit('error',
new Error('Op 0x' + (type ? type.toString(16) : '??') +
' not supported'),
new LDAPResult({
messageID: messageID,
protocolOp: type || Protocol.LDAP_REP_EXTENSION
}))
return false
}
return new Message({
messageID: messageID,
log: self.log
})
}
/// --- Exports
module.exports = Parser

Some files were not shown because too many files have changed in this diff Show More