Workflow funktioniert nun wieder. Es gab Probleme nach Aenderungen.
Build and Publish Site / docker (push) Successful in 23s
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:
+7
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"semi": true,
|
||||
"singleQuote": true,
|
||||
"arrowParens": "always",
|
||||
"printWidth": 100,
|
||||
"trailingComma": "es5"
|
||||
}
|
||||
+4
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"recommendations": ["esbenp.prettier-vscode", "dbaeumer.vscode-eslint"],
|
||||
"unwantedRecommendations": []
|
||||
}
|
||||
+28
@@ -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
@@ -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
@@ -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
@@ -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
@@ -0,0 +1,131 @@
|
||||
# ldapauth-fork
|
||||
|
||||
[](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
@@ -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
@@ -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
@@ -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;
|
||||
+4
@@ -0,0 +1,4 @@
|
||||
node_modules/
|
||||
coverage/
|
||||
.nyc_output/
|
||||
docs/
|
||||
+17
@@ -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: '^_'
|
||||
}]
|
||||
}
|
||||
}
|
||||
Generated
Vendored
+18
@@ -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"
|
||||
Generated
Vendored
+30
@@ -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'
|
||||
logic/node-red-data/node_modules/ldapauth-fork/node_modules/ldapjs/.github/workflows/integration.yml
Generated
Vendored
+32
@@ -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
|
||||
Generated
Vendored
+54
@@ -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
|
||||
+4
@@ -0,0 +1,4 @@
|
||||
check-coverage: false
|
||||
|
||||
files:
|
||||
- 'test/**/*.test.js'
|
||||
+104
@@ -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**
|
||||
+19
@@ -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
|
||||
+54
@@ -0,0 +1,54 @@
|
||||
# LDAPjs
|
||||
|
||||
[](https://github.com/ldapjs/node-ldapjs/actions)
|
||||
[](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>.
|
||||
Generated
Vendored
+8
@@ -0,0 +1,8 @@
|
||||
version: '3'
|
||||
|
||||
services:
|
||||
openldap:
|
||||
image: ghcr.io/ldapjs/docker-test-openldap/openldap:latest
|
||||
ports:
|
||||
- 389:389
|
||||
- 636:636
|
||||
Generated
Vendored
+1
@@ -0,0 +1 @@
|
||||
ldapjs.org
|
||||
Generated
Vendored
+266
@@ -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;
|
||||
}
|
||||
Generated
Vendored
+1
@@ -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 |
Generated
Vendored
+39
@@ -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>
|
||||
Generated
Vendored
+485
@@ -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);
|
||||
});
|
||||
```
|
||||
+127
@@ -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());
|
||||
});
|
||||
```
|
||||
Generated
Vendored
+94
@@ -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
|
||||
Generated
Vendored
+625
@@ -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();
|
||||
});
|
||||
}
|
||||
```
|
||||
Generated
Vendored
+317
@@ -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
|
||||
```
|
||||
+697
@@ -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.
|
||||
+95
@@ -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.
|
||||
Generated
Vendored
+607
@@ -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.
|
||||
Generated
Vendored
+65
@@ -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()
|
||||
})
|
||||
}
|
||||
Generated
Vendored
+65
@@ -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()
|
||||
})
|
||||
}
|
||||
Generated
Vendored
+177
@@ -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)
|
||||
})
|
||||
Generated
Vendored
Executable
+24
@@ -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;
|
||||
}
|
||||
+54
@@ -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
|
||||
}
|
||||
Generated
Vendored
+160
@@ -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'
|
||||
}
|
||||
+213
@@ -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
|
||||
Generated
Vendored
+1297
File diff suppressed because it is too large
Load Diff
Generated
Vendored
+7
@@ -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
|
||||
}
|
||||
Generated
Vendored
+23
@@ -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]'
|
||||
}
|
||||
Generated
Vendored
+25
@@ -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))
|
||||
}
|
||||
}
|
||||
Generated
Vendored
+23
@@ -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
|
||||
}
|
||||
}
|
||||
Generated
Vendored
+151
@@ -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
|
||||
}
|
||||
Generated
Vendored
+34
@@ -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)
|
||||
})
|
||||
}
|
||||
Generated
Vendored
+36
@@ -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()
|
||||
}
|
||||
}
|
||||
logic/node-red-data/node_modules/ldapauth-fork/node_modules/ldapjs/lib/client/request-queue/flush.js
Generated
Vendored
+24
@@ -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)
|
||||
}
|
||||
}
|
||||
logic/node-red-data/node_modules/ldapauth-fork/node_modules/ldapjs/lib/client/request-queue/index.js
Generated
Vendored
+39
@@ -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
|
||||
}
|
||||
logic/node-red-data/node_modules/ldapauth-fork/node_modules/ldapjs/lib/client/request-queue/purge.js
Generated
Vendored
+12
@@ -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'))
|
||||
})
|
||||
}
|
||||
Generated
Vendored
+173
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
Generated
Vendored
+61
@@ -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
|
||||
Generated
Vendored
+83
@@ -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
|
||||
Generated
Vendored
+86
@@ -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
|
||||
}
|
||||
Generated
Vendored
+82
@@ -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
|
||||
Generated
Vendored
+82
@@ -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
|
||||
Generated
Vendored
+108
@@ -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
|
||||
Generated
Vendored
+100
@@ -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
|
||||
Generated
Vendored
+94
@@ -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
|
||||
Generated
Vendored
+112
@@ -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
|
||||
Generated
Vendored
+50
@@ -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
|
||||
+473
@@ -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
|
||||
}
|
||||
+120
@@ -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
|
||||
}())
|
||||
Generated
Vendored
+47
@@ -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
|
||||
}
|
||||
Generated
Vendored
+147
@@ -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
|
||||
}
|
||||
})
|
||||
Generated
Vendored
+27
@@ -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
|
||||
}
|
||||
Generated
Vendored
+35
@@ -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
|
||||
}
|
||||
Generated
Vendored
+60
@@ -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
|
||||
}
|
||||
Generated
Vendored
+44
@@ -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
|
||||
}
|
||||
}
|
||||
Generated
Vendored
+59
@@ -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
|
||||
}
|
||||
Generated
Vendored
+60
@@ -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
|
||||
}
|
||||
Generated
Vendored
+35
@@ -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
|
||||
}
|
||||
Generated
Vendored
+208
@@ -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
|
||||
}
|
||||
Generated
Vendored
+35
@@ -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
|
||||
}
|
||||
Generated
Vendored
+23
@@ -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)
|
||||
}
|
||||
Generated
Vendored
+27
@@ -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
|
||||
}
|
||||
Generated
Vendored
+36
@@ -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
|
||||
}
|
||||
Generated
Vendored
+70
@@ -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
|
||||
}
|
||||
+84
@@ -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]
|
||||
}
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
'use strict'
|
||||
|
||||
const logger = require('abstract-logging')
|
||||
logger.child = function () { return logger }
|
||||
|
||||
module.exports = logger
|
||||
Generated
Vendored
+87
@@ -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
|
||||
Generated
Vendored
+34
@@ -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
|
||||
Generated
Vendored
+159
@@ -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
|
||||
Generated
Vendored
+22
@@ -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
|
||||
Generated
Vendored
+84
@@ -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
|
||||
Generated
Vendored
+22
@@ -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
|
||||
Generated
Vendored
+74
@@ -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
|
||||
Generated
Vendored
+33
@@ -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
|
||||
Generated
Vendored
+62
@@ -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
|
||||
Generated
Vendored
+22
@@ -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
|
||||
Generated
Vendored
+117
@@ -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
|
||||
Generated
Vendored
+86
@@ -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
|
||||
Generated
Vendored
+61
@@ -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
|
||||
|
||||
}
|
||||
Generated
Vendored
+110
@@ -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
|
||||
Generated
Vendored
+85
@@ -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
|
||||
Generated
Vendored
+22
@@ -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
|
||||
Generated
Vendored
+83
@@ -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
|
||||
Generated
Vendored
+22
@@ -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
|
||||
Generated
Vendored
+221
@@ -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
Reference in New Issue
Block a user