├── .eslintrc ├── .github └── workflows │ └── node.js.yml ├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── __test__ └── client.spec.js ├── package-lock.json ├── package.json └── src ├── attribute.js ├── change.js ├── dn.js ├── errors.js ├── filters ├── and_filter.js ├── approx_filter.js ├── equality_filter.js ├── ext_filter.js ├── ge_filter.js ├── index.js ├── le_filter.js ├── not_filter.js ├── or_filter.js ├── presence_filter.js └── substr_filter.js ├── index.js ├── requests ├── add_request.js ├── bind_request.js ├── del_request.js ├── index.js ├── moddn_request.js ├── modify_request.js ├── request.js ├── search_request.js └── unbind_request.js ├── responses ├── index.js ├── parser.js ├── response.js ├── search_entry.js └── search_reference.js └── utils ├── OID.js ├── assert.js ├── error-codes.js ├── parse-url.js └── protocol.js /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "eslint:recommended", 3 | "env": { 4 | "node": true, 5 | "es6": true, 6 | "jest": true 7 | }, 8 | "parserOptions": { 9 | "sourceType": "module", 10 | "ecmaVersion": 8 11 | }, 12 | "rules": { 13 | "semi": [ 14 | "error", 15 | "always" 16 | ], 17 | "linebreak-style": [ 18 | "error", 19 | "unix" 20 | ], 21 | "comma-dangle": [ 22 | "error", 23 | "never" 24 | ], 25 | "prefer-template": "error", 26 | "space-before-function-paren": "off", 27 | "no-console": "off" 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /.github/workflows/node.js.yml: -------------------------------------------------------------------------------- 1 | # This workflow will do a clean installation of node dependencies, cache/restore them, build the source code and run tests across different versions of node 2 | # For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-nodejs 3 | 4 | name: Node.js CI 5 | 6 | on: 7 | push: 8 | branches: [ "master" ] 9 | pull_request: 10 | branches: [ "master" ] 11 | 12 | jobs: 13 | build: 14 | 15 | runs-on: ubuntu-latest 16 | 17 | strategy: 18 | matrix: 19 | node-version: [20.x] 20 | 21 | steps: 22 | - uses: actions/checkout@v3 23 | - name: Use Node.js ${{ matrix.node-version }} 24 | uses: actions/setup-node@v3 25 | with: 26 | node-version: ${{ matrix.node-version }} 27 | cache: 'npm' 28 | - run: npm ci 29 | - run: npm test 30 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .vscode 2 | node_modules 3 | coverage 4 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "9" 4 | script: npm run test -s 5 | before_script: 6 | - npm install 7 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # LDAP js client 2 | 3 | [![Node.js CI](https://github.com/zont/ldapjs-client/actions/workflows/node.js.yml/badge.svg)](https://github.com/zont/ldapjs-client/actions/workflows/node.js.yml) 4 | 5 | > node >= 8.0 6 | 7 | No `ldapjs` (https://www.npmjs.com/package/ldapjs) as dependency. 8 | 9 | **Why**: because `ldapjs` is not maintained for more than two years. 10 | 11 | 12 | ## Installation 13 | 14 | npm install ldapjs-client 15 | 16 | Usage 17 | ----- 18 | 19 | To create a new client: 20 | 21 | ```js 22 | var LdapClient = require('ldapjs-client'); 23 | var client = new LdapClient({ url: 'ldap://127.0.0.1:389' }); 24 | ``` 25 | 26 | Attribute | Type | Description 27 | --- | --- | --- 28 | url | String | A valid LDAP URL (proto/host/port only) 29 | timeout | Number | Milliseconds client should let operations live for before timing out (Default: Infinity) 30 | tlsOptions | Object | Additional options passed to TLS connection layer when connecting via ldaps:// (See: The TLS docs for node.js) 31 | 32 | ### add 33 | ```js 34 | try { 35 | const entry = { 36 | cn: 'foo', 37 | sn: 'bar', 38 | email: ['foo@bar.com', 'foo1@bar.com'], 39 | objectclass: 'fooPerson' 40 | }; 41 | 42 | await client.add('cn=foo, o=example', entry); 43 | } catch (e) { 44 | console.log('Add failed'); 45 | } 46 | ``` 47 | 48 | ### bind 49 | ```js 50 | try { 51 | await client.bind('username', 'password'); 52 | } catch (e) { 53 | console.log('Bind failed'); 54 | } 55 | ``` 56 | 57 | ### del 58 | ```js 59 | try { 60 | await client.del('cn=foo, o=example'); 61 | } catch (e) { 62 | console.log(e); 63 | } 64 | ``` 65 | 66 | ### modify 67 | ```js 68 | try { 69 | const change = { 70 | operation: 'add', // add, delete, replace 71 | modification: { 72 | pets: ['cat', 'dog'] 73 | } 74 | }; 75 | 76 | await client.modify('cn=foo, o=example', change); 77 | } catch (e) { 78 | console.log(e); 79 | } 80 | ``` 81 | 82 | ### modifyDN 83 | ```js 84 | try { 85 | await client.modifyDN('cn=foo, o=example', 'cn=bar'); 86 | } catch (e) { 87 | console.log(e); 88 | } 89 | ``` 90 | 91 | ### search 92 | ```js 93 | try { 94 | const options = { 95 | filter: '(&(l=Seattle)(email=*@foo.com))', 96 | scope: 'sub', 97 | attributes: ['dn', 'sn', 'cn'] 98 | }; 99 | 100 | const entries = await client.search('o=example', options); 101 | } catch (e) { 102 | console.log(e); 103 | } 104 | ``` 105 | 106 | Attribute | Type | Description 107 | --- | --- | --- 108 | scope | String | One of base, one, or sub. Defaults to base 109 | filter | String | A string version of an LDAP filter. Defaults to (objectclass=*) 110 | attributes | Array of String | attributes to select and return. Defaults to the empty set, which means all attributes 111 | sizeLimit | Number | the maximum number of entries to return. Defaults to 0 (unlimited) 112 | pageSize | Number | Page size for paged search. If this attribute is set, search results are retrieved in pages. ActiveDirectory has a default limit of 1000 returned entries. If a larger number of results is expected, this attribute should be set 113 | timeLimit | Number | the maximum amount of time the server should take in responding, in seconds. Defaults to 10. Lots of servers will ignore this 114 | typesOnly | Boolean | on whether you want the server to only return the names of the attributes, and not their values. Borderline useless. Defaults to false 115 | 116 | ### unbind 117 | ```js 118 | try { 119 | await client.unbind(); 120 | } catch (e) { 121 | console.log(e); 122 | } 123 | ``` 124 | 125 | ### destroy 126 | ```js 127 | try { 128 | await client.destroy(); 129 | } catch (e) { 130 | console.log(e); 131 | } 132 | ``` 133 | Close connection if exists and destroy current client 134 | 135 | --- 136 | 137 | Pull requests and suggestions are welcome! 138 | 139 | ## License 140 | 141 | MIT. 142 | -------------------------------------------------------------------------------- /__test__/client.spec.js: -------------------------------------------------------------------------------- 1 | const Client = require('../src'); 2 | 3 | const url = 'ldap://ldap.forumsys.com'; 4 | const user = 'cn=read-only-admin,dc=example,dc=com'; 5 | const password = 'password'; 6 | 7 | describe('Client', () => { 8 | it('defined', () => { 9 | expect(Client).toBeDefined(); 10 | 11 | const client = new Client({ url }); 12 | 13 | expect(client).toBeDefined(); 14 | expect(client.add).toBeDefined(); 15 | expect(client.bind).toBeDefined(); 16 | expect(client.del).toBeDefined(); 17 | expect(client.destroy).toBeDefined(); 18 | expect(client.modify).toBeDefined(); 19 | expect(client.modifyDN).toBeDefined(); 20 | expect(client.search).toBeDefined(); 21 | expect(client.unbind).toBeDefined(); 22 | }); 23 | 24 | it('destroy', async () => { 25 | expect.assertions(1); 26 | 27 | const client = new Client({ url }); 28 | 29 | await client.destroy(); 30 | 31 | expect(true).toBeTruthy(); 32 | }); 33 | 34 | it('bind', async () => { 35 | expect.assertions(1); 36 | 37 | const client = new Client({ url }); 38 | 39 | await client.bind(user, password); 40 | 41 | expect(true).toBeTruthy(); 42 | 43 | await client.destroy(); 44 | }); 45 | 46 | it('parallel bind', async () => { 47 | expect.assertions(1); 48 | 49 | const client = new Client({ url }); 50 | 51 | const p1 = client.bind(user, password); 52 | const p2 = client.bind('uid=einstein,dc=example,dc=com', password); 53 | 54 | await Promise.all([p1, p2]); 55 | 56 | expect(true).toBeTruthy(); 57 | 58 | await client.destroy(); 59 | }); 60 | 61 | it('bind fail', async () => { 62 | expect.assertions(1); 63 | 64 | const client = new Client({ url }); 65 | 66 | try { 67 | await client.bind(user, ''); 68 | 69 | expect(false).toBeTruthy(); 70 | } catch (e) { 71 | expect(true).toBeTruthy(); 72 | } 73 | 74 | await client.destroy(); 75 | }); 76 | 77 | it('connect fail', async () => { 78 | expect.assertions(1); 79 | 80 | const client = new Client({ url: 'ldap://127.0.0.1' }); 81 | 82 | try { 83 | await client.bind(user, password); 84 | 85 | expect(false).toBeTruthy(); 86 | } catch (e) { 87 | expect(true).toBeTruthy(); 88 | } 89 | 90 | await client.destroy(); 91 | }); 92 | 93 | it('SSl fail', async () => { 94 | expect.assertions(1); 95 | 96 | const client = new Client({ url: url.replace('ldap:', 'ldaps:') }); 97 | 98 | try { 99 | await client.bind(user, password); 100 | 101 | expect(false).toBeTruthy(); 102 | } catch (e) { 103 | expect(true).toBeTruthy(); 104 | } 105 | 106 | await client.destroy(); 107 | }); 108 | 109 | it('search', async () => { 110 | expect.assertions(4); 111 | 112 | const client = new Client({ url }); 113 | 114 | await client.bind(user, password); 115 | const response = await client.search('ou=scientists,dc=example,dc=com', { scope: 'sub' }); 116 | 117 | expect(response.length).toBeGreaterThan(0); 118 | expect(response[0].dn).toBeDefined(); 119 | expect(response[0].ou).toBe('scientists'); 120 | expect(response[0].objectClass.length).toBeGreaterThan(0); 121 | 122 | await client.destroy(); 123 | }); 124 | 125 | it ('search w/ base scope', async () => { 126 | const client = new Client({ url }); 127 | 128 | await client.bind(user, password); 129 | 130 | try { 131 | const response = await client.search('ou=scientists,dc=example,dc=com', { scope: 'base' }); 132 | expect(response.length).toBeGreaterThanOrEqual(0); 133 | } catch (e) { 134 | expect(false).toBeTruthy(); 135 | } 136 | 137 | await client.destroy(); 138 | }); 139 | 140 | it('search not found', async () => { 141 | expect.assertions(2); 142 | 143 | const client = new Client({ url }); 144 | 145 | await client.bind(user, password); 146 | const response = await client.search('ou=scientists,dc=example,dc=com', { filter: '(ou=sysadmins)', scope: 'sub' }); 147 | 148 | expect(Array.isArray(response)).toBeTruthy(); 149 | expect(response.length).toBe(0); 150 | 151 | await client.destroy(); 152 | }); 153 | 154 | it('paged search', async () => { 155 | expect.assertions(4); 156 | 157 | const client = new Client({ url }); 158 | 159 | await client.bind(user, password); 160 | const response = await client.search('ou=scientists,dc=example,dc=com', { scope: 'sub', pageSize: 1 }); 161 | 162 | expect(response.length).toBeGreaterThan(0); 163 | expect(response[0].dn).toBeDefined(); 164 | expect(response[0].ou).toBe('scientists'); 165 | expect(response[0].objectClass.length).toBeGreaterThan(0); 166 | 167 | await client.destroy(); 168 | }); 169 | 170 | xit('unbind', async () => { 171 | expect.assertions(4); 172 | 173 | const client = new Client({ url }); 174 | 175 | await client.bind(user, password); 176 | 177 | expect(true).toBeTruthy(); 178 | 179 | await client.unbind(); 180 | 181 | expect(true).toBeTruthy(); 182 | 183 | try { 184 | await client.search('ou=scientists,dc=example,dc=com', { scope: 'sub' }); 185 | 186 | expect(false).toBeTruthy(); 187 | } catch (e) { 188 | expect(true).toBeTruthy(); 189 | } 190 | 191 | await client.bind(user, password); 192 | await client.search('ou=scientists,dc=example,dc=com', { scope: 'sub' }); 193 | await client.unbind(); 194 | 195 | expect(true).toBeTruthy(); 196 | 197 | await client.destroy(); 198 | }); 199 | }); 200 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "author": "Alexander Zonov", 3 | "name": "ldapjs-client", 4 | "description": "LDAP js client", 5 | "keywords": [ 6 | "LDAP", 7 | "ldap", 8 | "client", 9 | "simple", 10 | "promised", 11 | "async" 12 | ], 13 | "version": "0.1.7", 14 | "license": "MIT", 15 | "repository": { 16 | "type": "git", 17 | "url": "git://github.com/zont/ldapjs-client.git" 18 | }, 19 | "main": "src/index.js", 20 | "scripts": { 21 | "lint": "eslint --fix src/**/* __test__/**/*", 22 | "test": "jest --detectOpenHandles" 23 | }, 24 | "dependencies": { 25 | "asn1": "0.2.6", 26 | "assert-plus": "1.0.0", 27 | "ldap-filter": "0.3.3" 28 | }, 29 | "devDependencies": { 30 | "eslint": "8.56.0", 31 | "jest": "29.7.0" 32 | }, 33 | "jest": { 34 | "testEnvironmentOptions": { 35 | "url": "http://localhost/" 36 | }, 37 | "verbose": true, 38 | "collectCoverage": true, 39 | "coverageDirectory": "coverage", 40 | "coverageReporters": [ 41 | "text-summary", 42 | "html" 43 | ] 44 | }, 45 | "engines": { 46 | "node": ">=8.0" 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/attribute.js: -------------------------------------------------------------------------------- 1 | const assert = require('assert-plus'); 2 | const asn1 = require('asn1'); 3 | const Protocol = require('./utils/protocol'); 4 | 5 | const _bufferEncoding = type => type.endsWith(';binary') ? 'base64' : 'utf8'; 6 | 7 | class Attribute { 8 | constructor(options) { 9 | options = options || {}; 10 | 11 | assert.object(options, 'options'); 12 | assert.optionalString(options.type, 'options.type'); 13 | 14 | this.type = options.type || ''; 15 | this._vals = []; 16 | 17 | if (options.vals !== undefined && options.vals !== null) 18 | this.vals = options.vals; 19 | } 20 | 21 | get json() { 22 | return { 23 | type: this.type, 24 | vals: this.vals 25 | }; 26 | } 27 | 28 | get vals() { 29 | const eType = _bufferEncoding(this.type); 30 | return this._vals.map(v => v.toString(eType)); 31 | } 32 | 33 | set vals(vals) { 34 | this._vals = []; 35 | if (Array.isArray(vals)) { 36 | vals.forEach(v => this.addValue(v)); 37 | } else { 38 | this.addValue(vals); 39 | } 40 | } 41 | 42 | addValue(val) { 43 | if (Buffer.isBuffer(val)) { 44 | this._vals.push(val); 45 | } else { 46 | this._vals.push(Buffer.from(String(val), _bufferEncoding(this.type))); 47 | } 48 | } 49 | 50 | parse(ber) { 51 | assert.ok(ber); 52 | 53 | ber.readSequence(); 54 | this.type = ber.readString(); 55 | 56 | if (ber.peek() === Protocol.LBER_SET) { 57 | if (ber.readSequence(Protocol.LBER_SET)) { 58 | const end = ber.offset + ber.length; 59 | while (ber.offset < end) 60 | this._vals.push(ber.readString(asn1.Ber.OctetString, true)); 61 | } 62 | } 63 | 64 | return true; 65 | } 66 | 67 | toBer(ber) { 68 | assert.ok(ber); 69 | 70 | ber.startSequence(); 71 | ber.writeString(this.type); 72 | ber.startSequence(Protocol.LBER_SET); 73 | if (this._vals.length) { 74 | this._vals.forEach(b => { 75 | ber.writeByte(asn1.Ber.OctetString); 76 | ber.writeLength(b.length); 77 | b.forEach(i => ber.writeByte(i)); 78 | }); 79 | } else { 80 | ber.writeStringArray([]); 81 | } 82 | ber.endSequence(); 83 | ber.endSequence(); 84 | 85 | return ber; 86 | } 87 | 88 | toString() { 89 | return JSON.stringify(this.json); 90 | } 91 | 92 | static compare(a, b) { 93 | assert.ok(Attribute.isAttribute(a) && Attribute.isAttribute(b), 'can only compare Attributes'); 94 | 95 | if (a.type < b.type) return -1; 96 | if (a.type > b.type) return 1; 97 | if (a.vals.length < b.vals.length) return -1; 98 | if (a.vals.length > b.vals.length) return 1; 99 | 100 | for (let i = 0; i < a.vals.length; ++i) { 101 | if (a.vals[i] < b.vals[i]) return -1; 102 | if (a.vals[i] > b.vals[i]) return 1; 103 | } 104 | 105 | return 0; 106 | } 107 | 108 | static toBer(attr, ber) { 109 | return Attribute.prototype.toBer.call(attr, ber); 110 | } 111 | 112 | static isAttribute(attr) { 113 | if (!attr || typeof (attr) !== 'object') { 114 | return false; 115 | } 116 | if (attr instanceof Attribute) { 117 | return true; 118 | } 119 | return typeof attr.toBer === 'function' && typeof attr.type === 'string' && Array.isArray(attr.vals) 120 | && attr.vals.filter(item => typeof item === 'string' || Buffer.isBuffer(item)).length === attr.vals.length; 121 | } 122 | 123 | static fromObject(attributes) { 124 | return Object.keys(attributes).map(k => { 125 | const attr = new Attribute({ type: k }); 126 | 127 | if (Array.isArray(attributes[k])) { 128 | attributes[k].forEach(v => attr.addValue(v.toString())); 129 | } else { 130 | attr.addValue(attributes[k].toString()); 131 | } 132 | 133 | return attr; 134 | }); 135 | } 136 | } 137 | 138 | module.exports = Attribute; 139 | -------------------------------------------------------------------------------- /src/change.js: -------------------------------------------------------------------------------- 1 | const assert = require('assert-plus'); 2 | const Attribute = require('./attribute'); 3 | 4 | class Change { 5 | constructor(options) { 6 | if (options) { 7 | assert.object(options); 8 | assert.optionalString(options.operation); 9 | } else { 10 | options = {}; 11 | } 12 | 13 | this._modification = false; 14 | this.operation = options.operation || options.type || 'add'; 15 | this.modification = options.modification || {}; 16 | } 17 | 18 | get operation() { 19 | switch (this._operation) { 20 | case 0x00: return 'add'; 21 | case 0x01: return 'delete'; 22 | case 0x02: return 'replace'; 23 | default: 24 | throw new Error(`0x${this._operation.toString(16)} is invalid`); 25 | } 26 | } 27 | 28 | set operation(val) { 29 | assert.string(val); 30 | switch (val.toLowerCase()) { 31 | case 'add': 32 | this._operation = 0x00; 33 | break; 34 | case 'delete': 35 | this._operation = 0x01; 36 | break; 37 | case 'replace': 38 | this._operation = 0x02; 39 | break; 40 | default: 41 | throw new Error(`Invalid operation type: 0x${val.toString(16)}`); 42 | } 43 | } 44 | 45 | get modification() { 46 | return this._modification; 47 | } 48 | 49 | set modification(val) { 50 | if (Attribute.isAttribute(val)) { 51 | this._modification = val; 52 | return; 53 | } 54 | if (Object.keys(val).length == 2 && typeof val.type === 'string' && Array.isArray(val.vals)) { 55 | this._modification = new Attribute({ 56 | type: val.type, 57 | vals: val.vals 58 | }); 59 | return; 60 | } 61 | 62 | const keys = Object.keys(val); 63 | if (keys.length > 1) { 64 | throw new Error('Only one attribute per Change allowed'); 65 | } else if (keys.length === 0) { 66 | return; 67 | } 68 | 69 | const k = keys[0]; 70 | const _attr = new Attribute({ type: k }); 71 | if (Array.isArray(val[k])) { 72 | val[k].forEach(v => _attr.addValue(v.toString())); 73 | } else { 74 | _attr.addValue(val[k].toString()); 75 | } 76 | this._modification = _attr; 77 | } 78 | 79 | get json() { 80 | return { 81 | operation: this.operation, 82 | modification: this._modification ? this._modification.json : {} 83 | }; 84 | } 85 | 86 | static compare(a, b) { 87 | assert.ok(Change.isChange(a) && Change.isChange(b), 'can only compare Changes'); 88 | 89 | if (a.operation < b.operation) 90 | return -1; 91 | if (a.operation > b.operation) 92 | return 1; 93 | 94 | return Attribute.compare(a.modification, b.modification); 95 | } 96 | 97 | static isChange(change) { 98 | if (!change || typeof change !== 'object') { 99 | return false; 100 | } 101 | return change instanceof Change || (typeof change.toBer === 'function' && change.modification !== undefined && change.operation !== undefined); 102 | } 103 | 104 | static fromObject(change) { 105 | assert.ok(change.operation || change.type, 'change.operation required'); 106 | assert.object(change.modification, 'change.modification'); 107 | 108 | if (Object.keys(change.modification).length == 2 && typeof change.modification.type === 'string' && Array.isArray(change.modification.vals)) { 109 | return [new Change({ 110 | operation: change.operation || change.type, 111 | modification: change.modification 112 | })]; 113 | } else { 114 | return Object.keys(change.modification).map(k => new Change({ 115 | operation: change.operation || change.type, 116 | modification: { 117 | [k]: change.modification[k] 118 | } 119 | })); 120 | } 121 | } 122 | 123 | parse(ber) { 124 | assert.ok(ber); 125 | 126 | ber.readSequence(); 127 | this._operation = ber.readEnumeration(); 128 | this._modification = new Attribute(); 129 | this._modification.parse(ber); 130 | 131 | return true; 132 | } 133 | 134 | toBer(ber) { 135 | assert.ok(ber); 136 | 137 | ber.startSequence(); 138 | ber.writeEnumeration(this._operation); 139 | ber = this._modification.toBer(ber); 140 | ber.endSequence(); 141 | 142 | return ber; 143 | } 144 | } 145 | 146 | module.exports = Change; 147 | -------------------------------------------------------------------------------- /src/dn.js: -------------------------------------------------------------------------------- 1 | const assert = require('assert-plus'); 2 | 3 | ///--- Helpers 4 | 5 | const invalidDN = name => { 6 | const e = new Error(); 7 | e.name = 'InvalidDistinguishedNameError'; 8 | e.message = name; 9 | return e; 10 | }; 11 | 12 | const isAlphaNumeric = c => /[A-Za-z0-9]/.test(c); 13 | const isWhitespace = c => /\s/.test(c); 14 | 15 | const escapeValue = (val, forceQuote) => { 16 | let out = ''; 17 | let cur = 0; 18 | const len = val.length; 19 | let quoted = false; 20 | const escaped = /[\\"]/; 21 | const special = /[,=+<>#;]/; 22 | 23 | if (len > 0) { 24 | quoted = forceQuote || (val[0] == ' ' || val[len-1] == ' '); 25 | } 26 | 27 | while (cur < len) { 28 | if (escaped.test(val[cur]) || (!quoted && special.test(val[cur]))) { 29 | out += '\\'; 30 | } 31 | out += val[cur++]; 32 | } 33 | if (quoted) 34 | out = `"${ out }"`; 35 | return out; 36 | }; 37 | 38 | ///--- API 39 | 40 | class RDN { 41 | constructor(obj) { 42 | this.attrs = {}; 43 | 44 | if (obj) { 45 | Object.keys(obj).forEach(k => this.set(k, obj[k])); 46 | } 47 | } 48 | 49 | set(name, value) { 50 | assert.string(name, 'name (string) required'); 51 | assert.string(value, 'value (string) required'); 52 | 53 | const lname = name.toLowerCase(); 54 | this.attrs[lname] = { name, value }; 55 | } 56 | 57 | toString() { 58 | const keys = Object.keys(this.attrs); 59 | keys.sort((a, b) => a.localeCompare(b) || this.attrs[a].value.localeCompare(this.attrs[b].value)); 60 | 61 | return keys 62 | .map(key => `${key}=${escapeValue(this.attrs[key].value)}`) 63 | .join('+'); 64 | } 65 | } 66 | 67 | // Thank you OpenJDK! 68 | const parse = name => { 69 | assert.string(name, 'name'); 70 | 71 | let cur = 0; 72 | const len = name.length; 73 | 74 | const parseRdn = () => { 75 | const rdn = new RDN(); 76 | let order = 0; 77 | rdn.spLead = trim(); 78 | while (cur < len) { 79 | const opts = { 80 | order: order 81 | }; 82 | const attr = parseAttrType(); 83 | trim(); 84 | if (cur >= len || name[cur++] !== '=') 85 | throw invalidDN(name); 86 | 87 | trim(); 88 | // Parameters about RDN value are set in 'opts' by parseAttrValue 89 | const value = parseAttrValue(opts); 90 | rdn.set(attr, value, opts); 91 | rdn.spTrail = trim(); 92 | if (cur >= len || name[cur] !== '+') 93 | break; 94 | ++cur; 95 | ++order; 96 | } 97 | return rdn; 98 | }; 99 | 100 | const trim = () => { 101 | let count = 0; 102 | while ((cur < len) && isWhitespace(name[cur])) { 103 | ++cur; 104 | ++count; 105 | } 106 | return count; 107 | }; 108 | 109 | const parseAttrType = () => { 110 | const beg = cur; 111 | while (cur < len) { 112 | const c = name[cur]; 113 | if (isAlphaNumeric(c) || 114 | c == '.' || 115 | c == '-' || 116 | c == ' ') { 117 | ++cur; 118 | } else { 119 | break; 120 | } 121 | } 122 | // Back out any trailing spaces. 123 | while ((cur > beg) && (name[cur - 1] == ' ')) 124 | --cur; 125 | 126 | if (beg == cur) 127 | throw invalidDN(name); 128 | 129 | return name.slice(beg, cur); 130 | }; 131 | 132 | const parseAttrValue = opts => { 133 | if (cur < len && name[cur] == '#') { 134 | opts.binary = true; 135 | return parseBinaryAttrValue(); 136 | } else if (cur < len && name[cur] == '"') { 137 | opts.quoted = true; 138 | return parseQuotedAttrValue(); 139 | } else { 140 | return parseStringAttrValue(); 141 | } 142 | }; 143 | 144 | const parseBinaryAttrValue = () => { 145 | const beg = cur++; 146 | while (cur < len && isAlphaNumeric(name[cur])) 147 | ++cur; 148 | 149 | return name.slice(beg, cur); 150 | }; 151 | 152 | const parseQuotedAttrValue = () => { 153 | let str = ''; 154 | ++cur; // Consume the first quote 155 | 156 | while ((cur < len) && name[cur] != '"') { 157 | if (name[cur] === '\\') 158 | cur++; 159 | str += name[cur++]; 160 | } 161 | if (cur++ >= len) // no closing quote 162 | throw invalidDN(name); 163 | 164 | return str; 165 | }; 166 | 167 | const parseStringAttrValue = () => { 168 | const beg = cur; 169 | let str = ''; 170 | let esc = -1; 171 | 172 | while ((cur < len) && !atTerminator()) { 173 | if (name[cur] === '\\') { 174 | // Consume the backslash and mark its place just in case it's escaping 175 | // whitespace which needs to be preserved. 176 | esc = cur++; 177 | } 178 | if (cur === len) // backslash followed by nothing 179 | throw invalidDN(name); 180 | str += name[cur++]; 181 | } 182 | 183 | // Trim off (unescaped) trailing whitespace and rewind cursor to the end of 184 | // the AttrValue to record whitespace length. 185 | for (; cur > beg; cur--) { 186 | if (!isWhitespace(name[cur - 1]) || (esc === (cur - 1))) 187 | break; 188 | } 189 | return str.slice(0, cur - beg); 190 | }; 191 | 192 | const atTerminator = () => cur < len && (name[cur] === ',' || name[cur] === ';' || name[cur] === '+'); 193 | 194 | const rdns = []; 195 | 196 | // Short-circuit for empty DNs 197 | if (len === 0) 198 | return new DN(rdns); 199 | 200 | rdns.push(parseRdn()); 201 | while (cur < len) { 202 | if (name[cur] === ',' || name[cur] === ';') { 203 | ++cur; 204 | rdns.push(parseRdn()); 205 | } else { 206 | throw invalidDN(name); 207 | } 208 | } 209 | 210 | return new DN(rdns); 211 | }; 212 | 213 | class DN { 214 | constructor(rdns) { 215 | assert.optionalArrayOfObject(rdns, 'rdns'); 216 | 217 | this.rdns = rdns ? rdns.slice() : []; 218 | } 219 | 220 | static isDN(dn) { 221 | return dn instanceof DN || (dn && Array.isArray(dn.rdns)); 222 | } 223 | 224 | toString() { 225 | return this.rdns.map(String).join(', '); 226 | } 227 | } 228 | 229 | 230 | module.exports = { parse, DN, RDN }; 231 | -------------------------------------------------------------------------------- /src/errors.js: -------------------------------------------------------------------------------- 1 | const assert = require('assert-plus'); 2 | const { Response } = require('./responses'); 3 | const CODES = require('./utils/error-codes'); 4 | 5 | const ERRORS = {}; 6 | const capitalize = str => str.charAt(0) + str.slice(1).toLowerCase(); 7 | 8 | class LDAPError extends Error { 9 | constructor(message, dn, caller) { 10 | super(message); 11 | 12 | if (Error.captureStackTrace) 13 | Error.captureStackTrace(this, caller || LDAPError); 14 | 15 | this.lde_message = message; 16 | this.lde_dn = dn; 17 | } 18 | 19 | get name() { 20 | return 'LDAPError'; 21 | } 22 | 23 | get code() { 24 | return CODES.LDAP_OTHER; 25 | } 26 | 27 | get message() { 28 | return this.lde_message || this.name; 29 | } 30 | 31 | get dn() { 32 | return this.lde_dn ? this.lde_dn.toString() : ''; 33 | } 34 | } 35 | 36 | class ConnectionError extends LDAPError { 37 | constructor(message) { 38 | super(message, null, ConnectionError); 39 | } 40 | 41 | get name() { 42 | return 'ConnectionError'; 43 | } 44 | } 45 | 46 | class TimeoutError extends LDAPError { 47 | constructor(message) { 48 | super(message, null, TimeoutError); 49 | } 50 | 51 | get name() { 52 | return 'TimeoutError'; 53 | } 54 | } 55 | 56 | Object.keys(CODES) 57 | .filter(key => key !== 'LDAP_SUCCESS') 58 | .forEach(key => { 59 | const pieces = key.split('_').slice(1).map(capitalize); 60 | if (pieces[pieces.length - 1] !== 'Error') { 61 | pieces.push('Error'); 62 | } 63 | 64 | ERRORS[CODES[key]] = class extends LDAPError { 65 | get message() { 66 | return pieces.join(' '); 67 | } 68 | 69 | get name() { 70 | return pieces.join(''); 71 | } 72 | 73 | get code() { 74 | return CODES[key]; 75 | } 76 | }; 77 | }); 78 | 79 | module.exports = { 80 | ConnectionError, 81 | TimeoutError, 82 | ProtocolError: ERRORS[CODES.LDAP_PROTOCOL_ERROR], 83 | 84 | LDAP_SUCCESS: CODES.LDAP_SUCCESS, 85 | 86 | getError(res) { 87 | assert.ok(res instanceof Response, 'res (Response) required'); 88 | 89 | return new (ERRORS[res.status])(null, res.matchedDN || null, module.exports.getError); 90 | } 91 | }; 92 | -------------------------------------------------------------------------------- /src/filters/and_filter.js: -------------------------------------------------------------------------------- 1 | const assert = require('assert-plus'); 2 | const parents = require('ldap-filter'); 3 | const { BerWriter } = require('asn1'); 4 | const { FILTER_AND } = require('../utils/protocol'); 5 | 6 | module.exports = class AndFilter extends parents.AndFilter { 7 | toBer(ber) { 8 | assert.ok(ber instanceof BerWriter, 'ber (BerWriter) required'); 9 | 10 | ber.startSequence(FILTER_AND); 11 | ber = this.filters.reduce((ber, f) => f.toBer(ber), ber); 12 | ber.endSequence(); 13 | 14 | return ber; 15 | } 16 | }; 17 | -------------------------------------------------------------------------------- /src/filters/approx_filter.js: -------------------------------------------------------------------------------- 1 | const assert = require('assert-plus'); 2 | const parents = require('ldap-filter'); 3 | const { BerWriter } = require('asn1'); 4 | const { FILTER_APPROX } = require('../utils/protocol'); 5 | 6 | module.exports = class ApproximateFilter extends parents.ApproximateFilter { 7 | parse(ber) { 8 | assert.ok(ber); 9 | 10 | this.attribute = ber.readString().toLowerCase(); 11 | this.value = ber.readString(); 12 | 13 | return true; 14 | } 15 | 16 | toBer(ber) { 17 | assert.ok(ber instanceof BerWriter, 'ber (BerWriter) required'); 18 | 19 | ber.startSequence(FILTER_APPROX); 20 | ber.writeString(this.attribute); 21 | ber.writeString(this.value); 22 | ber.endSequence(); 23 | 24 | return ber; 25 | } 26 | }; 27 | -------------------------------------------------------------------------------- /src/filters/equality_filter.js: -------------------------------------------------------------------------------- 1 | const assert = require('assert-plus'); 2 | const parents = require('ldap-filter'); 3 | const { Ber: { OctetString }, BerWriter } = require('asn1'); 4 | const { FILTER_EQUALITY } = require('../utils/protocol'); 5 | 6 | module.exports = class EqualityFilter extends parents.EqualityFilter { 7 | matches(target, strictAttrCase) { 8 | assert.object(target, 'target'); 9 | 10 | const tv = parents.getAttrValue(target, this.attribute, strictAttrCase); 11 | const value = this.value; 12 | 13 | if (this.attribute.toLowerCase() === 'objectclass') { 14 | return parents.testValues(v => value.toLowerCase() === v.toLowerCase(), tv); 15 | } else { 16 | return parents.testValues(v => value === v, tv); 17 | } 18 | } 19 | 20 | parse(ber) { 21 | assert.ok(ber); 22 | 23 | this.attribute = ber.readString().toLowerCase(); 24 | this.value = ber.readString(OctetString, true); 25 | 26 | if (this.attribute === 'objectclass') { 27 | this.value = this.value.toLowerCase(); 28 | } 29 | 30 | return true; 31 | } 32 | 33 | toBer(ber) { 34 | assert.ok(ber instanceof BerWriter, 'ber (BerWriter) required'); 35 | if (this.attribute.toLowerCase() === 'objectguid'){ 36 | this.raw = Buffer.from(this.value,'binary'); 37 | } 38 | ber.startSequence(FILTER_EQUALITY); 39 | ber.writeString(this.attribute); 40 | ber.writeBuffer(this.raw, OctetString); 41 | ber.endSequence(); 42 | 43 | return ber; 44 | } 45 | }; 46 | -------------------------------------------------------------------------------- /src/filters/ext_filter.js: -------------------------------------------------------------------------------- 1 | const assert = require('assert-plus'); 2 | const parents = require('ldap-filter'); 3 | const { BerWriter } = require('asn1'); 4 | const { FILTER_EXT } = require('../utils/protocol'); 5 | 6 | module.exports = class ExtensibleFilter extends parents.ExtensibleFilter { 7 | parse(ber) { 8 | const end = ber.offset + ber.length; 9 | while (ber.offset < end) { 10 | const tag = ber.peek(); 11 | switch (tag) { 12 | case 0x81: 13 | this.rule = ber.readString(tag); 14 | break; 15 | case 0x82: 16 | this.matchType = ber.readString(tag); 17 | break; 18 | case 0x83: 19 | this.value = ber.readString(tag); 20 | break; 21 | case 0x84: 22 | this.dnAttributes = ber.readBoolean(tag); 23 | break; 24 | default: 25 | throw new Error(`Invalid ext_match filter type: 0x${tag.toString(16)}`); 26 | } 27 | } 28 | 29 | return true; 30 | } 31 | 32 | toBer(ber) { 33 | assert.ok(ber instanceof BerWriter, 'ber (BerWriter) required'); 34 | 35 | ber.startSequence(FILTER_EXT); 36 | 37 | if (this.rule) { 38 | ber.writeString(this.rule, 0x81); 39 | } 40 | 41 | if (this.matchType) { 42 | ber.writeString(this.matchType, 0x82); 43 | } 44 | 45 | ber.writeString(this.value, 0x83); 46 | 47 | if (this.dnAttributes) { 48 | ber.writeBoolean(this.dnAttributes, 0x84); 49 | } 50 | 51 | ber.endSequence(); 52 | 53 | return ber; 54 | } 55 | }; 56 | -------------------------------------------------------------------------------- /src/filters/ge_filter.js: -------------------------------------------------------------------------------- 1 | const assert = require('assert'); 2 | const parents = require('ldap-filter'); 3 | const { BerWriter } = require('asn1'); 4 | const { FILTER_GE } = require('../utils/protocol'); 5 | 6 | module.exports = class GreaterThanEqualsFilter extends parents.GreaterThanEqualsFilter { 7 | parse(ber) { 8 | assert.ok(ber); 9 | 10 | this.attribute = ber.readString().toLowerCase(); 11 | this.value = ber.readString(); 12 | 13 | return true; 14 | } 15 | 16 | toBer(ber) { 17 | assert.ok(ber instanceof BerWriter, 'ber (BerWriter) required'); 18 | 19 | ber.startSequence(FILTER_GE); 20 | ber.writeString(this.attribute); 21 | ber.writeString(this.value); 22 | ber.endSequence(); 23 | 24 | return ber; 25 | } 26 | }; 27 | -------------------------------------------------------------------------------- /src/filters/index.js: -------------------------------------------------------------------------------- 1 | const ldapFilter = require('ldap-filter'); 2 | const AndFilter = require('./and_filter'); 3 | const ApproximateFilter = require('./approx_filter'); 4 | const EqualityFilter = require('./equality_filter'); 5 | const ExtensibleFilter = require('./ext_filter'); 6 | const GreaterThanEqualsFilter = require('./ge_filter'); 7 | const LessThanEqualsFilter = require('./le_filter'); 8 | const NotFilter = require('./not_filter'); 9 | const OrFilter = require('./or_filter'); 10 | const PresenceFilter = require('./presence_filter'); 11 | const SubstringFilter = require('./substr_filter'); 12 | 13 | const cloneFilter = input => { 14 | switch (input.type) { 15 | case 'and': 16 | return new AndFilter({ filters: input.filters.map(cloneFilter) }); 17 | case 'or': 18 | return new OrFilter({ filters: input.filters.map(cloneFilter) }); 19 | case 'not': 20 | return new NotFilter({ filter: cloneFilter(input.filter) }); 21 | case 'equal': 22 | return new EqualityFilter(input); 23 | case 'substring': 24 | return new SubstringFilter(input); 25 | case 'ge': 26 | return new GreaterThanEqualsFilter(input); 27 | case 'le': 28 | return new LessThanEqualsFilter(input); 29 | case 'present': 30 | return new PresenceFilter(input); 31 | case 'approx': 32 | return new ApproximateFilter(input); 33 | case 'ext': 34 | return new ExtensibleFilter(input); 35 | default: 36 | throw new Error(`invalid filter type: ${input.type}`); 37 | } 38 | }; 39 | 40 | module.exports = { 41 | parseString: str => cloneFilter(ldapFilter.parse(str)) 42 | }; 43 | -------------------------------------------------------------------------------- /src/filters/le_filter.js: -------------------------------------------------------------------------------- 1 | const assert = require('assert'); 2 | const parents = require('ldap-filter'); 3 | const { BerWriter } = require('asn1'); 4 | const { FILTER_LE } = require('../utils/protocol'); 5 | 6 | module.exports = class LessThanEqualsFilter extends parents.LessThanEqualsFilter { 7 | parse(ber) { 8 | assert.ok(ber); 9 | 10 | this.attribute = ber.readString().toLowerCase(); 11 | this.value = ber.readString(); 12 | 13 | return true; 14 | } 15 | 16 | toBer(ber) { 17 | assert.ok(ber instanceof BerWriter, 'ber (BerWriter) required'); 18 | 19 | ber.startSequence(FILTER_LE); 20 | ber.writeString(this.attribute); 21 | ber.writeString(this.value); 22 | ber.endSequence(); 23 | 24 | return ber; 25 | } 26 | }; 27 | -------------------------------------------------------------------------------- /src/filters/not_filter.js: -------------------------------------------------------------------------------- 1 | const assert = require('assert'); 2 | const parents = require('ldap-filter'); 3 | const { BerWriter } = require('asn1'); 4 | const { FILTER_NOT } = require('../utils/protocol'); 5 | 6 | module.exports = class NotFilter extends parents.NotFilter { 7 | toBer(ber) { 8 | assert.ok(ber instanceof BerWriter, 'ber (BerWriter) required'); 9 | 10 | ber.startSequence(FILTER_NOT); 11 | ber = this.filter.toBer(ber); 12 | ber.endSequence(); 13 | 14 | return ber; 15 | } 16 | }; 17 | -------------------------------------------------------------------------------- /src/filters/or_filter.js: -------------------------------------------------------------------------------- 1 | const assert = require('assert'); 2 | const parents = require('ldap-filter'); 3 | const { BerWriter } = require('asn1'); 4 | const { FILTER_OR } = require('../utils/protocol'); 5 | 6 | module.exports = class OrFilter extends parents.OrFilter { 7 | toBer(ber) { 8 | assert.ok(ber instanceof BerWriter, 'ber (BerWriter) required'); 9 | 10 | ber.startSequence(FILTER_OR); 11 | ber = this.filters.reduce((ber, f) => f.toBer(ber), ber); 12 | ber.endSequence(); 13 | 14 | return ber; 15 | } 16 | }; 17 | -------------------------------------------------------------------------------- /src/filters/presence_filter.js: -------------------------------------------------------------------------------- 1 | const assert = require('assert'); 2 | const parents = require('ldap-filter'); 3 | const { BerWriter } = require('asn1'); 4 | const { FILTER_PRESENT } = require('../utils/protocol'); 5 | 6 | module.exports = class PresenceFilter extends parents.PresenceFilter { 7 | parse(ber) { 8 | assert.ok(ber); 9 | 10 | this.attribute = ber.buffer.slice(0, ber.length).toString('utf8').toLowerCase(); 11 | ber._offset += ber.length; 12 | 13 | return true; 14 | } 15 | 16 | toBer(ber) { 17 | assert.ok(ber instanceof BerWriter, 'ber (BerWriter) required'); 18 | 19 | ber.startSequence(FILTER_PRESENT); 20 | Buffer.from(this.attribute).forEach(i => ber.writeByte(i)); 21 | ber.endSequence(); 22 | 23 | return ber; 24 | } 25 | }; 26 | -------------------------------------------------------------------------------- /src/filters/substr_filter.js: -------------------------------------------------------------------------------- 1 | const assert = require('assert'); 2 | const parents = require('ldap-filter'); 3 | const { BerWriter } = require('asn1'); 4 | const { FILTER_SUBSTRINGS } = require('../utils/protocol'); 5 | 6 | module.exports = class SubstringFilter extends parents.SubstringFilter { 7 | parse(ber) { 8 | assert.ok(ber); 9 | 10 | this.attribute = ber.readString().toLowerCase(); 11 | ber.readSequence(); 12 | const end = ber.offset + ber.length; 13 | 14 | while (ber.offset < end) { 15 | const tag = ber.peek(); 16 | switch (tag) { 17 | case 0x80: // Initial 18 | this.initial = this.attribute === 'objectclass' ? ber.readString(tag).toLowerCase() : ber.readString(tag); 19 | break; 20 | case 0x81: // Any 21 | this.any.push(this.attribute === 'objectclass' ? ber.readString(tag).toLowerCase() : ber.readString(tag)); 22 | break; 23 | case 0x82: // Final 24 | this.final = this.attribute === 'objectclass' ? ber.readString(tag).toLowerCase() : ber.readString(tag); 25 | break; 26 | default: 27 | throw new Error(`Invalid substrings filter type: 0x${tag.toString(16)}`); 28 | } 29 | } 30 | 31 | return true; 32 | } 33 | 34 | toBer(ber) { 35 | assert.ok(ber instanceof BerWriter, 'ber (BerWriter) required'); 36 | 37 | ber.startSequence(FILTER_SUBSTRINGS); 38 | ber.writeString(this.attribute); 39 | ber.startSequence(); 40 | 41 | if (this.initial) { 42 | ber.writeString(this.initial, 0x80); 43 | } 44 | 45 | if (this.any && this.any.length) { 46 | this.any.forEach(s => ber.writeString(s, 0x81)); 47 | } 48 | 49 | if (this.final) { 50 | ber.writeString(this.final, 0x82); 51 | } 52 | 53 | ber.endSequence(); 54 | ber.endSequence(); 55 | 56 | return ber; 57 | } 58 | }; 59 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | const net = require('net'); 2 | const tls = require('tls'); 3 | const assert = require('assert-plus'); 4 | const Attribute = require('./attribute'); 5 | const Change = require('./change'); 6 | const { parse } = require('./dn'); 7 | const { getError, ConnectionError, TimeoutError, ProtocolError, LDAP_SUCCESS } = require('./errors'); 8 | const { Add, Bind, Del, Modify, ModifyDN, Search, Unbind } = require('./requests'); 9 | const { Response, SearchEntry, SearchReference, Parser } = require('./responses'); 10 | const parseUrl = require('./utils/parse-url'); 11 | const OID = require('./utils/OID'); 12 | 13 | class Client { 14 | constructor(options) { 15 | assert.object(options, 'options'); 16 | assert.optionalNumber(options.timeout, 'timeout'); 17 | 18 | const url = options.url ? parseUrl(options.url) : null; 19 | delete url.search; 20 | 21 | Object.assign(this, options, url); 22 | 23 | this._queue = new Map(); 24 | 25 | this._parser = new Parser(); 26 | this._parser.on('error', e => console.error(e)); 27 | this._parser.on('message', msg => { 28 | if (msg instanceof SearchEntry || msg instanceof SearchReference) { 29 | this._queue.get(msg.id).result.push(msg.object); 30 | } else { 31 | const qItem = this._queue.get(msg.id); 32 | if (qItem) { 33 | const { resolve, reject, result, request, controls } = qItem; 34 | 35 | if (msg instanceof Response) { 36 | if (msg.status !== LDAP_SUCCESS) { 37 | reject(getError(msg)); 38 | } 39 | 40 | controls.length = 0; 41 | msg.controls.forEach((control) => controls.push(control)); 42 | 43 | resolve(request instanceof Search ? result : msg.object); 44 | } else if (msg instanceof Error) { 45 | reject(msg); 46 | } else { 47 | reject(new ProtocolError(msg.type)); 48 | } 49 | 50 | this._queue.delete(msg.id); 51 | } 52 | } 53 | }); 54 | } 55 | 56 | async add(entry, attributes, controls = []) { 57 | assert.string(entry, 'entry'); 58 | assert.object(attributes, 'attributes'); 59 | 60 | return this._send(new Add({ entry, attributes: Attribute.fromObject(attributes), controls })); 61 | } 62 | 63 | async bind(name, credentials, controls = []) { 64 | assert.string(name, 'name'); 65 | assert.optionalString(credentials, 'credentials'); 66 | 67 | return this._send(new Bind({ name, credentials, controls })); 68 | } 69 | 70 | async del(entry, controls = []) { 71 | assert.string(entry, 'entry'); 72 | 73 | return this._send(new Del({ entry, controls })); 74 | } 75 | 76 | async modify(entry, change, controls = []) { 77 | assert.string(entry, 'entry'); 78 | assert.object(change, 'change'); 79 | 80 | const changes = []; 81 | (Array.isArray(change) ? change : [change]).forEach(c => changes.push(...Change.fromObject(c))); 82 | 83 | return this._send(new Modify({ entry, changes, controls })); 84 | } 85 | 86 | async modifyDN(entry, newName, controls = []) { 87 | assert.string(entry, 'entry'); 88 | assert.string(newName, 'newName'); 89 | 90 | const newRdn = parse(newName); 91 | 92 | if (newRdn.rdns.length !== 1) { 93 | return this._send(new ModifyDN({ entry, newRdn: parse(newRdn.rdns.shift().toString()), newSuperior: newRdn })); 94 | } else { 95 | return this._send(new ModifyDN({ entry, newRdn, controls })); 96 | } 97 | } 98 | 99 | async search(baseObject, options, controls = []) { 100 | assert.string(baseObject, 'baseObject'); 101 | assert.object(options, 'options'); 102 | assert.optionalString(options.scope, 'options.scope'); 103 | assert.optionalString(options.filter, 'options.filter'); 104 | assert.optionalNumber(options.sizeLimit, 'options.sizeLimit'); 105 | assert.optionalNumber(options.pageSize, 'options.pageSize'); 106 | assert.optionalNumber(options.timeLimit, 'options.timeLimit'); 107 | assert.optionalArrayOfString(options.attributes, 'options.attributes'); 108 | 109 | if (options.pageSize) { 110 | let pageSize = options.pageSize; 111 | if (pageSize > options.sizeLimit) pageSize = options.sizeLimit; 112 | 113 | const controls0 = controls.filter((control) => { 114 | return control.OID !== OID.PagedResults; 115 | }); 116 | 117 | const pagedResults = { 118 | OID: OID.PagedResults, 119 | criticality: true, 120 | value: { 121 | size: pageSize, 122 | cookie: '' 123 | } 124 | }; 125 | 126 | let cookie = ''; 127 | let results = []; 128 | let hasNext = true; 129 | while (hasNext) { 130 | pagedResults.value.cookie = cookie; 131 | controls.length = 0; 132 | controls = controls.concat(controls0); 133 | controls.push(pagedResults); 134 | 135 | results = results.concat(await this._send(new Search(Object.assign({ baseObject, controls }, options)))); 136 | 137 | const responsePagedResults = controls.find((control) => { 138 | return control.OID === OID.PagedResults; 139 | }); 140 | 141 | if (responsePagedResults !== undefined && responsePagedResults.value.cookie !== '') { 142 | cookie = responsePagedResults.value.cookie; 143 | } else { 144 | hasNext = false; 145 | } 146 | } 147 | 148 | return results; 149 | 150 | } else { 151 | return this._send(new Search(Object.assign({ baseObject, controls }, options))); 152 | } 153 | } 154 | 155 | async unbind(controls = []) { 156 | return this._send(new Unbind({controls})); 157 | } 158 | 159 | async destroy() { 160 | if (this._socket) { 161 | this._socket.removeAllListeners('error'); 162 | this._socket.removeAllListeners('close'); 163 | this._socket.destroy(); 164 | this._socket = null; 165 | } 166 | 167 | if (this._parser) { 168 | this._parser.removeAllListeners('error'); 169 | this._parser.removeAllListeners('message'); 170 | this._parser = null; 171 | } 172 | 173 | if (this._queue) { 174 | this._queue.clear(); 175 | this._queue = null; 176 | } 177 | } 178 | 179 | async _connect() { 180 | return new Promise((resolve, reject) => { 181 | const destroy = () => { 182 | if (this._socket) { 183 | this._socket.destroy(); 184 | this._socket = null; 185 | } 186 | 187 | if (this._queue) { 188 | for (const { reject } of this._queue.values()) { 189 | reject(new ConnectionError('Connection closed')); 190 | } 191 | 192 | this._queue.clear(); 193 | } 194 | }; 195 | 196 | if (this.secure) { 197 | this._socket = tls.connect(this.port, this.host, this.tlsOptions); 198 | this._socket.once('secureConnect', resolve); 199 | } else { 200 | this._socket = net.connect(this.port, this.host); 201 | this._socket.once('connect', resolve); 202 | } 203 | 204 | this._socket.on('close', destroy); 205 | this._socket.on('error', e => { 206 | destroy(); 207 | reject(e || new Error('client error during setup')); 208 | }); 209 | this._socket.on('data', data => this._parser.parse(data)); 210 | }); 211 | } 212 | 213 | async _send(message) { 214 | if (!this._socket) { 215 | await this._connect(); 216 | } 217 | 218 | return new Promise((resolve, reject) => { 219 | try { 220 | this._queue.set(message.id, { resolve, reject, request: message, result: [], controls: message.controls }); 221 | this._socket.write(message.toBer()); 222 | 223 | if (message instanceof Unbind) { 224 | this._socket.removeAllListeners('close'); 225 | this._socket.on('close', () => resolve(new Response({}))); 226 | } 227 | 228 | if (this.timeout) { 229 | setTimeout(() => { 230 | if (this._queue) { 231 | this._queue.delete(message.id); 232 | } 233 | reject(new TimeoutError('request timeout (client interrupt)')); 234 | }, this.timeout); 235 | } 236 | } catch (e) { 237 | this._queue.delete(message.id); 238 | reject(e); 239 | } 240 | }); 241 | } 242 | } 243 | 244 | module.exports = Client; 245 | -------------------------------------------------------------------------------- /src/requests/add_request.js: -------------------------------------------------------------------------------- 1 | const Request = require('./request'); 2 | const { LDAP_REQ_ADD } = require('../utils/protocol'); 3 | const lassert = require('../utils/assert'); 4 | 5 | module.exports = class extends Request { 6 | constructor(options) { 7 | lassert.optionalStringDN(options.entry); 8 | lassert.optionalArrayOfAttribute(options.attributes); 9 | 10 | super(Object.assign({ protocolOp: LDAP_REQ_ADD, type: 'AddRequest', attributes: [] }, options)); 11 | } 12 | 13 | _toBer(ber) { 14 | ber.writeString(this.entry); 15 | ber.startSequence(); 16 | this.attributes.forEach(a => a.toBer(ber)); 17 | ber.endSequence(); 18 | 19 | return ber; 20 | } 21 | }; 22 | -------------------------------------------------------------------------------- /src/requests/bind_request.js: -------------------------------------------------------------------------------- 1 | const { Ber: { Context } } = require('asn1'); 2 | const Request = require('./request'); 3 | const { LDAP_REQ_BIND, LDAP_VERSION_3 } = require('../utils/protocol'); 4 | 5 | module.exports = class extends Request { 6 | constructor(options) { 7 | super(Object.assign({ protocolOp: LDAP_REQ_BIND, credentials: '', type: 'BindRequest' }, options)); 8 | } 9 | 10 | _toBer(ber) { 11 | ber.writeInt(LDAP_VERSION_3); 12 | ber.writeString(this.name); 13 | ber.writeString(this.credentials, Context); 14 | 15 | return ber; 16 | } 17 | }; 18 | -------------------------------------------------------------------------------- /src/requests/del_request.js: -------------------------------------------------------------------------------- 1 | const Request = require('./request'); 2 | const { LDAP_REQ_DELETE } = require('../utils/protocol'); 3 | const lassert = require('../utils/assert'); 4 | 5 | module.exports = class extends Request { 6 | constructor(options) { 7 | lassert.optionalStringDN(options.entry); 8 | 9 | super(Object.assign({ protocolOp: LDAP_REQ_DELETE, type: 'DeleteRequest' }, options)); 10 | } 11 | 12 | _toBer(ber) { 13 | Buffer.from(this.entry).forEach(i => ber.writeByte(i)); 14 | 15 | return ber; 16 | } 17 | }; 18 | -------------------------------------------------------------------------------- /src/requests/index.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | Add: require('./add_request'), 3 | Bind: require('./bind_request'), 4 | Del: require('./del_request'), 5 | Modify: require('./modify_request'), 6 | ModifyDN: require('./moddn_request'), 7 | Search: require('./search_request'), 8 | Unbind: require('./unbind_request') 9 | }; 10 | -------------------------------------------------------------------------------- /src/requests/moddn_request.js: -------------------------------------------------------------------------------- 1 | const Request = require('./request'); 2 | const { LDAP_REQ_MODRDN } = require('../utils/protocol'); 3 | const lassert = require('../utils/assert'); 4 | 5 | module.exports = class extends Request { 6 | constructor(options) { 7 | lassert.optionalStringDN(options.entry); 8 | lassert.optionalDN(options.newRdn); 9 | lassert.optionalDN(options.newSuperior); 10 | 11 | super(Object.assign({ protocolOp: LDAP_REQ_MODRDN, deleteOldRdn: true, type: 'ModifyDNRequest' }, options)); 12 | } 13 | 14 | _toBer(ber) { 15 | ber.writeString(this.entry); 16 | ber.writeString(this.newRdn.toString()); 17 | ber.writeBoolean(this.deleteOldRdn); 18 | if (this.newSuperior) { 19 | const s = this.newSuperior.toString(); 20 | const len = Buffer.byteLength(s); 21 | 22 | ber.writeByte(0x80); // MODIFY_DN_REQUEST_NEW_SUPERIOR_TAG 23 | ber.writeByte(len); 24 | ber._ensure(len); 25 | ber._buf.write(s, ber._offset); 26 | ber._offset += len; 27 | } 28 | 29 | return ber; 30 | } 31 | }; 32 | -------------------------------------------------------------------------------- /src/requests/modify_request.js: -------------------------------------------------------------------------------- 1 | const Request = require('./request'); 2 | const { LDAP_REQ_MODIFY } = require('../utils/protocol'); 3 | const lassert = require('../utils/assert'); 4 | 5 | module.exports = class extends Request { 6 | constructor(options) { 7 | lassert.optionalStringDN(options.entry); 8 | lassert.optionalArrayOfAttribute(options.attributes); 9 | 10 | super(Object.assign({ protocolOp: LDAP_REQ_MODIFY, type: 'ModifyRequest' }, options)); 11 | } 12 | 13 | _toBer(ber) { 14 | ber.writeString(this.entry); 15 | ber.startSequence(); 16 | this.changes.forEach(c => c.toBer(ber)); 17 | ber.endSequence(); 18 | 19 | return ber; 20 | } 21 | }; 22 | -------------------------------------------------------------------------------- /src/requests/request.js: -------------------------------------------------------------------------------- 1 | const asn1 = require('asn1'); 2 | const BerWriter = asn1.BerWriter; 3 | const { LDAP_CONTROLS } = require('../utils/protocol'); 4 | const OID = require('../utils/OID'); 5 | 6 | let id = 0; 7 | const nextID = () => { 8 | id = Math.max(1, (id + 1) % 2147483647); 9 | return id; 10 | }; 11 | 12 | const controlToBer = (control, writer) => { 13 | writer.startSequence(); 14 | writer.writeString(control.OID); 15 | writer.writeBoolean(control.criticality); 16 | 17 | const ber = new BerWriter(); 18 | ber.startSequence(); 19 | switch (control.OID) { 20 | case OID.PagedResults: 21 | ber.writeInt(control.value.size); 22 | if (control.value.cookie === '') { 23 | ber.writeString(''); 24 | } else { 25 | ber.writeBuffer(control.value.cookie, asn1.Ber.OctetString); 26 | } 27 | break; 28 | // Add New OID controls here 29 | default: 30 | } 31 | 32 | ber.endSequence(); 33 | writer.writeBuffer(ber.buffer, 0x04); 34 | 35 | writer.endSequence(); 36 | }; 37 | 38 | module.exports = class { 39 | constructor(options) { 40 | Object.assign(this, options, { id: nextID() }); 41 | } 42 | 43 | toBer() { 44 | let writer = new BerWriter(); 45 | writer.startSequence(); 46 | writer.writeInt(this.id); 47 | writer.startSequence(this.protocolOp); 48 | writer = this._toBer(writer); 49 | writer.endSequence(); 50 | 51 | if (this.controls.length > 0) { 52 | writer.startSequence(LDAP_CONTROLS); 53 | this.controls.forEach((control) => { 54 | controlToBer(control, writer); 55 | }); 56 | writer.endSequence(); 57 | } 58 | 59 | writer.endSequence(); 60 | return writer.buffer; 61 | } 62 | 63 | _toBer(ber) { 64 | return ber; 65 | } 66 | }; 67 | -------------------------------------------------------------------------------- /src/requests/search_request.js: -------------------------------------------------------------------------------- 1 | const { Ber } = require('asn1'); 2 | const Request = require('./request'); 3 | const { parseString } = require('../filters'); 4 | const { LDAP_REQ_SEARCH, NEVER_DEREF_ALIASES, SCOPE_BASE_OBJECT, SCOPE_ONE_LEVEL, SCOPE_SUBTREE } = require('../utils/protocol'); 5 | 6 | const SCOPES = { 7 | base: SCOPE_BASE_OBJECT, 8 | one: SCOPE_ONE_LEVEL, 9 | sub: SCOPE_SUBTREE 10 | }; 11 | 12 | module.exports = class extends Request { 13 | constructor(options) { 14 | super(Object.assign({ protocolOp: LDAP_REQ_SEARCH, scope: 'base', sizeLimit: 0, timeLimit: 10, typesOnly: false, attributes: [], type: 'SearchRequest' }, options)); 15 | } 16 | 17 | set scope(val) { 18 | if (!(val in SCOPES)) { 19 | throw new Error(`${val} is an invalid search scope`); 20 | } 21 | 22 | this._scope = SCOPES[val]; 23 | } 24 | 25 | _toBer(ber) { 26 | ber.writeString(this.baseObject.toString()); 27 | ber.writeEnumeration(this._scope); 28 | ber.writeEnumeration(NEVER_DEREF_ALIASES); 29 | ber.writeInt(this.sizeLimit); 30 | ber.writeInt(this.timeLimit); 31 | ber.writeBoolean(this.typesOnly); 32 | 33 | ber = parseString(this.filter || '(objectclass=*)').toBer(ber); 34 | 35 | ber.startSequence(Ber.Sequence | Ber.Constructor); 36 | if (this.attributes && this.attributes.length) { 37 | this.attributes.forEach(a => ber.writeString(a)); 38 | } 39 | ber.endSequence(); 40 | 41 | return ber; 42 | } 43 | }; 44 | -------------------------------------------------------------------------------- /src/requests/unbind_request.js: -------------------------------------------------------------------------------- 1 | const Request = require('./request'); 2 | const { LDAP_REQ_UNBIND } = require('../utils/protocol'); 3 | 4 | module.exports = class extends Request { 5 | constructor(options) { 6 | super(Object.assign({ protocolOp: LDAP_REQ_UNBIND, type: 'UnbindRequest' }, options)); 7 | } 8 | }; 9 | -------------------------------------------------------------------------------- /src/responses/index.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | Response: require('./response'), 3 | Parser: require('./parser'), 4 | SearchEntry: require('./search_entry'), 5 | SearchReference: require('./search_reference') 6 | }; 7 | -------------------------------------------------------------------------------- /src/responses/parser.js: -------------------------------------------------------------------------------- 1 | const EventEmitter = require('events').EventEmitter; 2 | const assert = require('assert-plus'); 3 | const { BerReader } = require('asn1'); 4 | const { LDAP_REP_SEARCH_ENTRY, LDAP_REP_SEARCH_REF } = require('../utils/protocol'); 5 | const SearchEntry = require('./search_entry'); 6 | const SearchReference = require('./search_reference'); 7 | const Response = require('./response'); 8 | 9 | const getMessage = ber => { 10 | const id = ber.readInt() || 0; 11 | const type = ber.readSequence(); 12 | const Message = type === LDAP_REP_SEARCH_ENTRY 13 | ? SearchEntry 14 | : type === LDAP_REP_SEARCH_REF 15 | ? SearchReference 16 | : Response; 17 | 18 | return new Message({ id }); 19 | }; 20 | 21 | class Parser extends EventEmitter { 22 | constructor() { 23 | super(); 24 | this.buffer = null; 25 | } 26 | 27 | parse(data) { 28 | assert.buffer(data, 'data'); 29 | 30 | this.buffer = this.buffer ? Buffer.concat([this.buffer, data]) : data; 31 | 32 | const ber = new BerReader(this.buffer); 33 | 34 | try { 35 | ber.readSequence(); 36 | } catch (e) { 37 | this.emit('error', e); 38 | return; 39 | } 40 | 41 | // If ber.length == 0, then we do not have a complete chunk 42 | // and can't proceed with parsing. 43 | // Allowing this function to continue results in an infinite loop 44 | // and due to the recursive nature of this function quickly 45 | // hits the stack call size limit. 46 | // This only happens with very large responses. 47 | if (ber.remain < ber.length || ber.length === 0) { 48 | return; 49 | } 50 | 51 | let nextMessage = null; 52 | if (ber.remain > ber.length) { 53 | nextMessage = this.buffer.slice(ber.offset + ber.length); 54 | ber._size = ber.offset + ber.length; 55 | assert.equal(ber.remain, ber.length); 56 | } 57 | 58 | this.buffer = null; 59 | 60 | try { 61 | const message = getMessage(ber); 62 | message.parse(ber); 63 | this.emit('message', message); 64 | } catch (e) { 65 | this.emit('error', e); 66 | } 67 | 68 | if (nextMessage) { 69 | this.parse(nextMessage); 70 | } 71 | } 72 | } 73 | 74 | module.exports = Parser; 75 | -------------------------------------------------------------------------------- /src/responses/response.js: -------------------------------------------------------------------------------- 1 | const assert = require('assert-plus'); 2 | const asn1 = require('asn1'); 3 | const Ber = asn1.Ber; 4 | const BerReader = asn1.BerReader; 5 | const { LDAP_REP_REFERRAL, LDAP_CONTROLS } = require('../utils/protocol'); 6 | const OID = require('../utils/OID'); 7 | 8 | const getControl = (ber) => { 9 | if (ber.readSequence() === null) { return null; } 10 | 11 | const control = { 12 | OID: '', 13 | criticality: false, 14 | value: null 15 | }; 16 | 17 | if (ber.length) { 18 | const end = ber.offset + ber.length; 19 | 20 | control.OID = ber.readString(); 21 | if (ber.offset < end && ber.peek() === Ber.Boolean) control.criticality = ber.readBoolean(); 22 | 23 | if (ber.offset < end) control.value = ber.readString(Ber.OctetString, true); 24 | 25 | const controlBer = new BerReader(control.value); 26 | switch (control.OID) { 27 | case OID.PagedResults: 28 | controlBer.readSequence(); 29 | control.value = {}; 30 | control.value.size = controlBer.readInt(); 31 | control.value.cookie = controlBer.readString(asn1.Ber.OctetString, true); 32 | if (control.value.cookie.length === 0) { 33 | control.value.cookie = ''; 34 | } 35 | break; 36 | // Add New OID controls here 37 | default: 38 | } 39 | } 40 | 41 | return control; 42 | }; 43 | 44 | module.exports = class { 45 | constructor(options) { 46 | assert.optionalNumber(options.status); 47 | assert.optionalString(options.matchedDN); 48 | assert.optionalString(options.errorMessage); 49 | assert.optionalArrayOfString(options.referrals); 50 | assert.optionalArrayOfObject(options.controls); 51 | 52 | Object.assign(this, { status: 0, matchedDN: '', errorMessage: '', referrals: [], type: 'Response', controls: [] }, options); 53 | } 54 | 55 | get object() { 56 | return this; 57 | } 58 | 59 | parse(ber) { 60 | this.status = ber.readEnumeration(); 61 | this.matchedDN = ber.readString(); 62 | this.errorMessage = ber.readString(); 63 | 64 | if (ber.peek() === LDAP_REP_REFERRAL) { 65 | const end = ber.offset + ber.length; 66 | while (ber.offset < end) { 67 | this.referrals.push(ber.readString()); 68 | } 69 | } 70 | 71 | if (ber.peek() === LDAP_CONTROLS) { 72 | ber.readSequence(); 73 | const end = ber.offset + ber.length; 74 | while (ber.offset < end) { 75 | const c = getControl(ber); 76 | if (c) { this.controls.push(c); } 77 | } 78 | } 79 | 80 | return true; 81 | } 82 | }; 83 | -------------------------------------------------------------------------------- /src/responses/search_entry.js: -------------------------------------------------------------------------------- 1 | const assert = require('assert-plus'); 2 | const Response = require('./response'); 3 | const Attribute = require('../attribute'); 4 | const { LDAP_REP_SEARCH_ENTRY } = require('../utils/protocol'); 5 | 6 | module.exports = class extends Response { 7 | constructor(options) { 8 | super(Object.assign({ protocolOp: LDAP_REP_SEARCH_ENTRY, type: 'SearchEntry', attributes: [] }, options)); 9 | } 10 | 11 | get object() { 12 | return this.attributes.reduce((obj, a) => { 13 | obj[a.type] = a.vals && a.vals.length ? a.vals.length > 1 ? a.vals.slice() : a.vals[0] : []; 14 | return obj; 15 | }, { dn: this.objectName }); 16 | } 17 | 18 | parse(ber) { 19 | this.objectName = ber.readString(); 20 | 21 | assert.ok(ber.readSequence()); 22 | 23 | const end = ber.offset + ber.length; 24 | while (ber.offset < end) { 25 | const a = new Attribute(); 26 | a.parse(ber); 27 | this.attributes.push(a); 28 | } 29 | 30 | return true; 31 | } 32 | }; 33 | -------------------------------------------------------------------------------- /src/responses/search_reference.js: -------------------------------------------------------------------------------- 1 | const Response = require('./response'); 2 | const { LDAP_REP_SEARCH_REF } = require('../utils/protocol'); 3 | const { DN } = require('../dn'); 4 | const parseUrl = require('../utils/parse-url'); 5 | 6 | module.exports = class extends Response { 7 | constructor(options) { 8 | super(Object.assign({ protocolOp: LDAP_REP_SEARCH_REF, uris: [], type: 'SearchReference' }, options)); 9 | } 10 | 11 | get object() { 12 | return { 13 | dn: new DN().toString(), 14 | uris: this.uris 15 | }; 16 | } 17 | 18 | parse(ber) { 19 | const length = ber.length; 20 | 21 | while (ber.offset < length) { 22 | const _url = ber.readString(); 23 | parseUrl(_url); 24 | this.uris.push(_url); 25 | } 26 | 27 | return true; 28 | } 29 | }; 30 | -------------------------------------------------------------------------------- /src/utils/OID.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @see {@link https://ldap.com/ldap-oid-reference-guide/} 3 | */ 4 | const OID = { 5 | PagedResults: '1.2.840.113556.1.4.319' 6 | }; 7 | 8 | module.exports = OID; 9 | -------------------------------------------------------------------------------- /src/utils/assert.js: -------------------------------------------------------------------------------- 1 | const { AssertionError } = require('assert'); 2 | const { DN: { isDN } } = require('../dn'); 3 | const { isAttribute } = require('../attribute'); 4 | 5 | const _assert = (arg, expected, name) => { 6 | throw new AssertionError({ 7 | message: `${name || expected} (${expected}) required`, 8 | actual: typeof (arg), 9 | expected, 10 | operator: '===', 11 | stackStartFunction: _assert.caller 12 | }); 13 | }; 14 | 15 | module.exports = { 16 | optionalArrayOfAttribute(input, name) { 17 | if (typeof input !== 'undefined' && (!Array.isArray(input) || input.some(v => !isAttribute(v)))) { 18 | _assert(input, 'array of Attribute', name); 19 | } 20 | }, 21 | 22 | optionalDN(input, name) { 23 | if (typeof input !== 'undefined' && !isDN(input)) { 24 | _assert(input, 'DN', name); 25 | } 26 | }, 27 | 28 | optionalStringDN(input, name) { 29 | if (!(typeof input === 'undefined' || isDN(input) || typeof input === 'string')) { 30 | _assert(input, 'DN or string', name); 31 | } 32 | } 33 | }; 34 | -------------------------------------------------------------------------------- /src/utils/error-codes.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | LDAP_SUCCESS: 0, 3 | LDAP_OPERATIONS_ERROR: 1, 4 | LDAP_PROTOCOL_ERROR: 2, 5 | LDAP_TIME_LIMIT_EXCEEDED: 3, 6 | LDAP_SIZE_LIMIT_EXCEEDED: 4, 7 | LDAP_COMPARE_FALSE: 5, 8 | LDAP_COMPARE_TRUE: 6, 9 | LDAP_AUTH_METHOD_NOT_SUPPORTED: 7, 10 | LDAP_STRONG_AUTH_REQUIRED: 8, 11 | LDAP_REFERRAL: 10, 12 | LDAP_ADMIN_LIMIT_EXCEEDED: 11, 13 | LDAP_UNAVAILABLE_CRITICAL_EXTENSION: 12, 14 | LDAP_CONFIDENTIALITY_REQUIRED: 13, 15 | LDAP_SASL_BIND_IN_PROGRESS: 14, 16 | LDAP_NO_SUCH_ATTRIBUTE: 16, 17 | LDAP_UNDEFINED_ATTRIBUTE_TYPE: 17, 18 | LDAP_INAPPROPRIATE_MATCHING: 18, 19 | LDAP_CONSTRAINT_VIOLATION: 19, 20 | LDAP_ATTRIBUTE_OR_VALUE_EXISTS: 20, 21 | LDAP_INVALID_ATTRIBUTE_SYNTAX: 21, 22 | LDAP_NO_SUCH_OBJECT: 32, 23 | LDAP_ALIAS_PROBLEM: 33, 24 | LDAP_INVALID_DN_SYNTAX: 34, 25 | LDAP_ALIAS_DEREF_PROBLEM: 36, 26 | LDAP_INAPPROPRIATE_AUTHENTICATION: 48, 27 | LDAP_INVALID_CREDENTIALS: 49, 28 | LDAP_INSUFFICIENT_ACCESS_RIGHTS: 50, 29 | LDAP_BUSY: 51, 30 | LDAP_UNAVAILABLE: 52, 31 | LDAP_UNWILLING_TO_PERFORM: 53, 32 | LDAP_LOOP_DETECT: 54, 33 | LDAP_NAMING_VIOLATION: 64, 34 | LDAP_OBJECTCLASS_VIOLATION: 65, 35 | LDAP_NOT_ALLOWED_ON_NON_LEAF: 66, 36 | LDAP_NOT_ALLOWED_ON_RDN: 67, 37 | LDAP_ENTRY_ALREADY_EXISTS: 68, 38 | LDAP_OBJECTCLASS_MODS_PROHIBITED: 69, 39 | LDAP_AFFECTS_MULTIPLE_DSAS: 71, 40 | LDAP_OTHER: 80, 41 | LDAP_PROXIED_AUTHORIZATION_DENIED: 123 42 | }; 43 | -------------------------------------------------------------------------------- /src/utils/parse-url.js: -------------------------------------------------------------------------------- 1 | const querystring = require('querystring'); 2 | const { parse } = require('url'); 3 | const assert = require('assert-plus'); 4 | 5 | const PROTOCOLS = ['ldap:', 'ldaps:']; 6 | const SCOPES = ['base', 'one', 'sub']; 7 | 8 | module.exports = str => { 9 | const u = parse(str); 10 | 11 | assert.ok(PROTOCOLS.includes(u.protocol), `Unsupported protocol: ${u.protocol}`); 12 | 13 | u.secure = u.protocol === 'ldaps:'; 14 | u.host = u.hostname || 'localhost'; 15 | u.port = u.port ? parseInt(u.port, 10) : u.secure ? 636 : 389; 16 | u.pathname = u.pathname ? querystring.unescape(u.pathname.substr(1)) : u.pathname; 17 | 18 | if (u.search) { 19 | const tmp = u.search.substr(1).split('?'); 20 | if (tmp[0]) { 21 | u.attributes = tmp[0].split(',').map(a => querystring.unescape(a.trim())); 22 | } 23 | if (tmp[1]) { 24 | assert.ok(SCOPES.includes(tmp[1]), `Unsupported scope: ${tmp[1]}`); 25 | u.scope = tmp[1]; 26 | } 27 | if (tmp[2]) { 28 | u.filter = querystring.unescape(tmp[2]); 29 | } 30 | if (tmp[3]) { 31 | u.extensions = querystring.unescape(tmp[3]); 32 | } 33 | 34 | u.attributes = u.attributes || []; 35 | u.scope = u.scope || 'base'; 36 | u.filter = u.filter || '(objectclass=*)'; 37 | } 38 | 39 | return u; 40 | }; 41 | -------------------------------------------------------------------------------- /src/utils/protocol.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | LDAP_VERSION_3: 0x03, 3 | LBER_SET: 0x31, 4 | LDAP_CONTROLS: 0xa0, 5 | 6 | SCOPE_BASE_OBJECT: 0, 7 | SCOPE_ONE_LEVEL: 1, 8 | SCOPE_SUBTREE: 2, 9 | 10 | NEVER_DEREF_ALIASES: 0, 11 | DEREF_IN_SEARCHING: 1, 12 | DEREF_BASE_OBJECT: 2, 13 | DEREF_ALWAYS: 3, 14 | 15 | FILTER_AND: 0xa0, 16 | FILTER_OR: 0xa1, 17 | FILTER_NOT: 0xa2, 18 | FILTER_EQUALITY: 0xa3, 19 | FILTER_SUBSTRINGS: 0xa4, 20 | FILTER_GE: 0xa5, 21 | FILTER_LE: 0xa6, 22 | FILTER_PRESENT: 0x87, 23 | FILTER_APPROX: 0xa8, 24 | FILTER_EXT: 0xa9, 25 | 26 | LDAP_REQ_BIND: 0x60, 27 | LDAP_REQ_UNBIND: 0x42, 28 | LDAP_REQ_SEARCH: 0x63, 29 | LDAP_REQ_MODIFY: 0x66, 30 | LDAP_REQ_ADD: 0x68, 31 | LDAP_REQ_DELETE: 0x4a, 32 | LDAP_REQ_MODRDN: 0x6c, 33 | LDAP_REQ_COMPARE: 0x6e, 34 | LDAP_REQ_ABANDON: 0x50, 35 | LDAP_REQ_EXTENSION: 0x77, 36 | 37 | LDAP_REP_BIND: 0x61, 38 | LDAP_REP_SEARCH_ENTRY: 0x64, 39 | LDAP_REP_SEARCH_REF: 0x73, 40 | LDAP_REP_SEARCH: 0x65, 41 | LDAP_REP_MODIFY: 0x67, 42 | LDAP_REP_ADD: 0x69, 43 | LDAP_REP_DELETE: 0x6b, 44 | LDAP_REP_MODRDN: 0x6d, 45 | LDAP_REP_COMPARE: 0x6f, 46 | LDAP_REP_EXTENSION: 0x78 47 | }; 48 | --------------------------------------------------------------------------------