├── .eslintignore ├── .eslintrc.js ├── .github ├── dependabot.yml └── workflows │ ├── docs.yml │ ├── integration.yml │ └── main.yml ├── .gitignore ├── .npmrc ├── .taprc.yml ├── CHANGES.md ├── LICENSE ├── README.md ├── docker-compose.yml ├── docs ├── branding │ ├── public │ │ ├── CNAME │ │ └── media │ │ │ ├── css │ │ │ └── style.css │ │ │ └── img │ │ │ └── logo.svg │ └── template.html ├── client.md ├── dn.md ├── errors.md ├── examples.md ├── filters.md ├── guide.md ├── index.md └── server.md ├── dt.png ├── examples ├── cluster-threading-net-server.js ├── cluster-threading.js └── inmemory.js ├── lib ├── client │ ├── client.js │ ├── constants.js │ ├── index.js │ ├── message-tracker │ │ ├── ge-window.js │ │ ├── id-generator.js │ │ ├── index.js │ │ └── purge-abandoned.js │ ├── request-queue │ │ ├── enqueue.js │ │ ├── flush.js │ │ ├── index.js │ │ └── purge.js │ └── search_pager.js ├── controls │ └── index.js ├── corked_emitter.js ├── errors │ ├── codes.js │ └── index.js ├── index.js ├── logger.js ├── messages │ ├── index.js │ ├── parser.js │ └── search_response.js ├── persistent_search.js ├── server.js └── url.js ├── package.json ├── scripts └── build-docs.js ├── test-integration ├── .eslintrc.js └── client │ ├── connect.test.js │ ├── issue-860.test.js │ ├── issue-883.test.js │ ├── issue-885.test.js │ ├── issue-923.test.js │ ├── issue-940.test.js │ ├── issue-946.test.js │ └── issues.test.js └── test ├── .eslintrc.js ├── client.test.js ├── controls └── control.test.js ├── corked_emitter.test.js ├── errors.test.js ├── imgs └── test.jpg ├── issue-845.test.js ├── issue-890.test.js ├── laundry.test.js ├── lib └── client │ ├── message-tracker │ ├── ge-window.test.js │ ├── id-generator.test.js │ ├── index.test.js │ └── purge-abandoned.test.js │ └── request-queue │ ├── enqueue.test.js │ ├── flush.test.js │ └── purge.test.js ├── messages └── parser.test.js ├── server.test.js ├── url.test.js └── utils.js /.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | coverage/ 3 | .nyc_output/ 4 | docs/ 5 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { 3 | commonjs: true, 4 | es2021: true, 5 | node: true 6 | }, 7 | parserOptions: { 8 | ecmaVersion: 'latest' 9 | }, 10 | extends: [ 11 | 'standard' 12 | ], 13 | rules: { 14 | 'no-shadow': 'error', 15 | 'no-unused-vars': ['error', { 16 | argsIgnorePattern: '^_', 17 | caughtErrorsIgnorePattern: '^_' 18 | }] 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "github-actions" 4 | # versioning-strategy: increase-if-necessary 5 | directory: "/" 6 | schedule: 7 | interval: "weekly" 8 | day: "saturday" 9 | time: "03:00" 10 | timezone: "America/New_York" 11 | - package-ecosystem: "npm" 12 | versioning-strategy: increase-if-necessary 13 | directory: "/" 14 | schedule: 15 | interval: "weekly" 16 | day: "saturday" 17 | time: "03:00" 18 | timezone: "America/New_York" 19 | -------------------------------------------------------------------------------- /.github/workflows/docs.yml: -------------------------------------------------------------------------------- 1 | name: 'Update Docs' 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | 8 | jobs: 9 | docs: 10 | name: Update Docs 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/checkout@v3 14 | - uses: actions/setup-node@v3 15 | with: 16 | node-version: '18' 17 | - name: Install Packages 18 | run: npm install 19 | - name: Build Docs 20 | run: npm run docs 21 | - name: Deploy 🚢 22 | uses: cpina/github-action-push-to-another-repository@master 23 | env: 24 | API_TOKEN_GITHUB: ${{ secrets.API_TOKEN_GITHUB }} 25 | with: 26 | source-directory: 'public' 27 | destination-github-username: 'ldapjs' 28 | destination-repository-name: 'ldapjs.github.io' 29 | user-email: 'bot@ldapjs.org' 30 | target-branch: 'gh-pages' 31 | -------------------------------------------------------------------------------- /.github/workflows/integration.yml: -------------------------------------------------------------------------------- 1 | name: 'Integration Tests' 2 | 3 | # Notes: 4 | # https://github.community/t5/GitHub-Actions/Github-Actions-services-not-reachable/m-p/30739/highlight/true#M538 5 | 6 | on: 7 | push: 8 | branches: 9 | - master 10 | - next 11 | pull_request: 12 | branches: 13 | - master 14 | - next 15 | 16 | jobs: 17 | baseline: 18 | name: Baseline Tests 19 | runs-on: ubuntu-latest 20 | 21 | services: 22 | openldap: 23 | image: ghcr.io/ldapjs/docker-test-openldap/openldap:2023-10-30 24 | ports: 25 | - 389:389 26 | - 636:636 27 | options: > 28 | --health-cmd "ldapsearch -Y EXTERNAL -Q -H ldapi:// -b ou=people,dc=planetexpress,dc=com -LLL '(cn=Turanga Leela)' cn" 29 | 30 | steps: 31 | - uses: actions/checkout@v3 32 | - uses: actions/setup-node@v3 33 | with: 34 | node-version: 'lts/*' 35 | 36 | - name: Install Packages 37 | run: npm install 38 | - name: Run Tests 39 | run: npm run test:integration 40 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: 'Lint And Test' 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | - next 8 | pull_request: 9 | branches: 10 | - master 11 | - next 12 | 13 | jobs: 14 | lint: 15 | name: Lint Check 16 | runs-on: ubuntu-latest 17 | steps: 18 | - uses: actions/checkout@v3 19 | - uses: actions/setup-node@v3 20 | with: 21 | node-version: 'lts/*' 22 | - name: Install Packages 23 | run: npm install 24 | - name: Lint Code 25 | run: npm run lint:ci 26 | 27 | run_tests: 28 | name: Unit Tests 29 | strategy: 30 | matrix: 31 | os: 32 | - ubuntu-latest 33 | - windows-latest 34 | node: 35 | - 16 36 | - 18 37 | - 20 38 | runs-on: ${{ matrix.os }} 39 | steps: 40 | - uses: actions/checkout@v3 41 | - uses: actions/setup-node@v3 42 | with: 43 | node-version: ${{ matrix.node }} 44 | - name: Install Packages 45 | run: npm install 46 | - name: Run Tests 47 | run: npm run test:ci 48 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.ldif 2 | *.tar.* 3 | *.tgz 4 | 5 | # Lock files 6 | pnpm-lock.yaml 7 | shrinkwrap.yaml 8 | package-lock.json 9 | yarn.lock 10 | 11 | # Logs 12 | logs 13 | *.log 14 | npm-debug.log* 15 | 16 | # Runtime data 17 | pids 18 | *.pid 19 | *.seed 20 | 21 | # Directory for instrumented libs generated by jscoverage/JSCover 22 | lib-cov 23 | 24 | # Coverage directory used by tools like istanbul 25 | coverage 26 | 27 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 28 | .grunt 29 | 30 | # node-waf configuration 31 | .lock-wscript 32 | 33 | # Compiled binary addons (http://nodejs.org/api/addons.html) 34 | build/Release 35 | 36 | # Dependency directory 37 | node_modules 38 | 39 | # Optional npm cache directory 40 | .npm 41 | 42 | # Optional REPL history 43 | .node_repl_history 44 | 45 | # 0x 46 | .__browserify_string_empty.js 47 | profile-* 48 | *.flamegraph 49 | 50 | # tap --cov 51 | .nyc_output/ 52 | 53 | # JetBrains IntelliJ IDEA 54 | .idea/ 55 | *.iml 56 | 57 | # VS Code 58 | .vscode/ 59 | 60 | # xcode 61 | build/* 62 | *.mode1 63 | *.mode1v3 64 | *.mode2v3 65 | *.perspective 66 | *.perspectivev3 67 | *.pbxuser 68 | *.xcworkspace 69 | xcuserdata 70 | 71 | # macOS 72 | .DS_Store 73 | 74 | # keys 75 | *.pem 76 | *.env.json 77 | *.env 78 | 79 | # built docs 80 | /public 81 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | # npm general settings 2 | package-lock=false 3 | legacy-peer-deps=true 4 | -------------------------------------------------------------------------------- /.taprc.yml: -------------------------------------------------------------------------------- 1 | # With PR #834 the code in this code base has been reduced significantly. 2 | # As a result, the coverage percentages changed, and are much lower than 3 | # previously. So we are reducing the requirements accordingly 4 | branches: 50 5 | functions: 50 6 | lines: 50 7 | statements: 50 8 | 9 | files: 10 | - 'test/**/*.test.js' 11 | -------------------------------------------------------------------------------- /CHANGES.md: -------------------------------------------------------------------------------- 1 | # ldapjs Changelog 2 | 3 | ## 2.0.0 4 | 5 | - Going foward, please see https://github.com/ldapjs/node-ldapjs/releases 6 | 7 | ## 1.0.2 8 | 9 | - Update dtrace-provider dependency 10 | 11 | ## 1.0.1 12 | 13 | - Update dependencies 14 | * assert-plus to 1.0.0 15 | * bunyan to 1.8.3 16 | * dashdash to 1.14.0 17 | * backoff to 2.5.0 18 | * once to 1.4.0 19 | * vasync to 1.6.4 20 | * verror to 1.8.1 21 | * dtrace-provider to 0.7.0 22 | - Drop any semblence of support for node 0.8.x 23 | 24 | ## 1.0.0 25 | 26 | - Update dependencies 27 | * asn1 to 0.2.3 28 | * bunyan to 1.5.1 29 | * dtrace-provider to 0.6.0 30 | - Removed pooled client 31 | - Removed custom formatting for GUIDs 32 | - Completely overhaul DN parsing/formatting 33 | - Add options for format preservation 34 | - Removed `spaced()` and `rndSpaced` from DN API 35 | - Fix parent/child rules regarding empty DNs 36 | - Request routing overhaul 37 | * #154 Route lookups do not depend on object property order 38 | * #111 Null ('') DN will act as catch-all 39 | - Add StartTLS support to client (Sponsored by: DoubleCheck Email Manager) 40 | - Improve robustness of client reconnect logic 41 | - Add 'resultError' event to client 42 | - Update paged search automation in client 43 | - Add Change.apply method for modifying objects 44 | - #143 Preserve raw Buffer value in Control objects 45 | - Test code coverage with node-istanbul 46 | - Convert tests to node-tape 47 | - Add controls for server-side sorting 48 | - #201 Replace nopt with dashdash 49 | - #134 Allow configuration of derefAliases client option 50 | - #197 Properly dispatch unbind requests 51 | - #196 Handle string ports properly in server.listen 52 | - Add basic server API tests 53 | - Store EqualityFilter value as Buffer 54 | - Run full test suite during 'make test' 55 | - #190 Add error code 123 from RFC4370 56 | - #178 Perform strict presence testing on attribute vals 57 | - #183 Accept buffers or strings for cert/key in createServer 58 | - #180 Add '-i, --insecure' option and to all ldapjs-\* CLIs 59 | - #254 Allow simple client bind with empty credentials 60 | 61 | ## 0.7.1 62 | 63 | - #169 Update dependencies 64 | * asn1 to 0.2.1 65 | * pooling to 0.4.6 66 | * assert-plus to 0.1.5 67 | * bunyan to 0.22.1 68 | - #173 Make dtrace-provider an optional dependency 69 | - #142 Improve parser error handling 70 | - #161 Properly handle close events on tls sockets 71 | - #163 Remove buffertools dependency 72 | - #162 Fix error event handling for pooled clients 73 | - #159 Allow ext request message to have a buffer value 74 | - #155 Make \*Filter.matches case insensitive for attrs 75 | 76 | ## 0.7.0 77 | 78 | - #87 Minor update to ClientPool event pass-through 79 | - #145 Update pooling to 0.4.5 80 | - #144 Fix unhandled error during client connection 81 | - Output ldapi:// URLs for UNIX domain sockets 82 | - Support extensible matching of caseIgnore and caseIgnoreSubstrings 83 | - Fix some ClientPool event handling 84 | - Improve DN formatting flexibility 85 | * Add 'spaced' function to DN objects allowing toggle of inter-RDN when 86 | rendering to a string. ('dc=test,dc=tld' vs 'dc=test, dc=tld') 87 | * Detect RDN spacing when parsing DN. 88 | - #128 Fix user can't bind with inmemory example 89 | - #139 Bump required tap version to 0.4.1 90 | - Allow binding ldap server on an ephemeral port 91 | 92 | ## 0.6.3 93 | 94 | - Update bunyan to 0.21.1 95 | - Remove listeners on the right object (s/client/res/) 96 | - Replace log4js with bunyan for binaries 97 | - #127 socket is closed issue with pools 98 | - #122 Allow changing TLS connection options in client 99 | - #120 Fix a bug with formatting digits less than 16. 100 | - #118 Fix "failed to instantiate provider" warnings in console on SmartOS 101 | 102 | ## 0.6.2 - 0.1.0 103 | 104 | **See git history** 105 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2019 LDAPjs, All rights reserved. 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Project Decomissioned 2 | 3 | This project has been decomissioned. I, James Sumners, took it on when it was 4 | languishing without any maintenance as it filled a need in the ecosystem and 5 | I had built things at a prior organization that depended upon this project. 6 | I spent a lot of time triaging issues and reworking things toward a path 7 | that could be more easily maintained by a community of volunteers. But I have 8 | not had the time to dedicate to this project in quite a while. There are 9 | outstanding issues that would take me at least a week of dedicated development 10 | time to solve, and I cannot afford to take time off of work to do that. 11 | Particularly considering that the aforementioned organization was two 12 | jobs ago, and it is extremely unlikely that I will transition to a role again 13 | that will need this project. 14 | 15 | So, why am I just now deciding to decomission this project? Because today, 16 | 2024-05-14, I received the following email: 17 | 18 | ![Abusive email](dt.png) 19 | 20 | I will not tolerate abuse, and I especially will not tolerate tacit death 21 | threats, over a hobby. You can thank the author of that email for the 22 | decomissioning on this project. 23 | 24 | My recommendation to you in regard to LDAP operations: write a gateway in a 25 | language that is more suited to these types of operations. I'd suggest 26 | [Go](https://go.dev). 27 | 28 | 👋 29 | 30 | P.S.: if I ever do need this project again, I might revive it. But I'd fight 31 | hard for my suggestion above. Also, I will consider turning it over to an 32 | interested party, but I will require at least one recommendation from a 33 | Node.js core contributor that I can vet with the people that I know on that 34 | team. -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | services: 2 | openldap: 3 | image: ghcr.io/ldapjs/docker-test-openldap/openldap:2023-10-30 4 | ports: 5 | - 389:389 6 | - 636:636 7 | healthcheck: 8 | start_period: 3s 9 | test: > 10 | /usr/bin/ldapsearch -Y EXTERNAL -Q -H ldapi:// -b ou=people,dc=planetexpress,dc=com -LLL '(cn=Turanga Leela)' cn 1>/dev/null 11 | -------------------------------------------------------------------------------- /docs/branding/public/CNAME: -------------------------------------------------------------------------------- 1 | ldapjs.org 2 | -------------------------------------------------------------------------------- /docs/branding/public/media/css/style.css: -------------------------------------------------------------------------------- 1 | 2 | /* ---- general styles */ 3 | 4 | body { 5 | font: 13px "Lucida Grande", "Lucida Sans Unicode", arial, sans-serif; 6 | line-height: 1.53846; /* 20px */ 7 | color: #4a3f2d; 8 | } 9 | 10 | :focus:not(:focus-visible) { 11 | outline: 0; 12 | } 13 | 14 | h1,h2,h3 { 15 | font-weight:normal; 16 | } 17 | 18 | h3{ 19 | margin-bottom:0; 20 | } 21 | 22 | ul, li { 23 | margin:0px; 24 | padding:0px; 25 | } 26 | 27 | ul { 28 | margin-left:40px; 29 | } 30 | 31 | ul > li { 32 | list-style:disc; 33 | list-style-position:inside; 34 | margin:10px 0px; 35 | } 36 | 37 | hr { 38 | border:none; 39 | width:98%; 40 | margin-left:-10px; 41 | border-top:1px solid #CCCCCC; 42 | border-bottom:1px solid #FFFFFF; 43 | } 44 | 45 | code, 46 | pre { 47 | border:1px solid #CCCCCC; 48 | background:#F2F0EE; 49 | -webkit-border-radius:2px; 50 | -moz-border-radius:2px; 51 | border-radius:2px; 52 | white-space:pre-wrap; 53 | } 54 | code { 55 | padding: 0 0.2em; 56 | } 57 | pre { 58 | margin: 1em 0; 59 | padding: .75em; 60 | overflow: auto; 61 | padding:10px 1.2em; 62 | margin-top:0; 63 | margin-bottom:20px; 64 | } 65 | pre code { 66 | border: medium none; 67 | padding: 0; 68 | } 69 | a code { 70 | text-decoration: underline; 71 | } 72 | 73 | a { 74 | color:#FD6512; 75 | text-decoration:none; 76 | } 77 | 78 | h4 { 79 | font-size: 85%; 80 | margin: 0; 81 | padding: 0; 82 | line-height: 1em; 83 | display: inline; 84 | } 85 | 86 | /* ---- header and sidebar */ 87 | 88 | #header { 89 | background:#C3BDB3; 90 | background:#1C313C; 91 | height:66px; 92 | left:0px; 93 | position:absolute; 94 | top:0px; 95 | width:100%; 96 | z-index:1; 97 | font-size:0.7em; 98 | } 99 | 100 | #header h1 { 101 | width: 424px; 102 | height: 35px; 103 | display:block; 104 | background: url(../img/logo.svg) no-repeat; 105 | line-height:2.1em; 106 | padding:0; 107 | padding-left:140px; 108 | margin-top:18px; 109 | margin-left:20px; 110 | color:white; 111 | text-transform: uppercase; 112 | } 113 | 114 | #sidebar { 115 | background-color:#EDEBEA; 116 | bottom:0px; 117 | left:0px; 118 | overflow:auto; 119 | padding:20px 0px 0px 15px; 120 | position:absolute; 121 | top:66px; 122 | width:265px; 123 | z-index:1; 124 | } 125 | 126 | #content { 127 | top:64px; 128 | bottom:0px; 129 | right:0px; 130 | left:290px; 131 | padding:20px 30px 400px; 132 | position:absolute; 133 | overflow:auto; 134 | z-index:0; 135 | } 136 | 137 | #sidebar h1 { 138 | font-size:1.2em; 139 | padding:0px; 140 | margin-top:15px; 141 | margin-bottom:3px; 142 | } 143 | 144 | #sidebar ul { 145 | margin:3px 0 10px 0; 146 | } 147 | 148 | #sidebar ul ul { 149 | margin:3px 0 5px 10px; 150 | } 151 | 152 | #sidebar li { 153 | margin:0; 154 | padding:0; 155 | font-size:0.9em; 156 | } 157 | 158 | #sidebar li, 159 | #sidebar li a { 160 | color:#5C5954; 161 | list-style:none; 162 | padding:1px 0px 1px 2px; 163 | } 164 | 165 | 166 | /* ---- intro */ 167 | 168 | .intro { 169 | color:#29231A; 170 | padding: 22px 25px; 171 | background: #EDEBEA; 172 | -webkit-border-radius: 5px; 173 | -moz-border-radius: 5px; 174 | -o-border-radius: 5px; 175 | border-radius: 5px; 176 | margin-bottom:40px; 177 | } 178 | .intro h1 { 179 | color: #1C313C; 180 | } 181 | .intro h3 { 182 | margin: 5px 0px 3px; 183 | font-size: 100%; 184 | font-weight: bold; 185 | } 186 | .intro ul { 187 | list-style-type:disc; 188 | padding-left:20px; 189 | margin-left:0; 190 | } 191 | .intro ul li{ 192 | margin:0; 193 | } 194 | .intro p { 195 | padding-left:20px; 196 | margin: 5px 0px 3px; 197 | } 198 | 199 | 200 | 201 | h2 { 202 | overflow: auto; 203 | margin-top: 60px; 204 | border-top: 2px solid #979592; 205 | z-index: 3; 206 | } 207 | h1 + h2 { 208 | margin-top: 0px; 209 | } 210 | 211 | h2 span { 212 | background: #979592; 213 | float:right; 214 | color:#fff; 215 | margin:0; 216 | margin-left:3px; 217 | padding:0.3em 0.7em; 218 | font-size: 0.55em; 219 | word-spacing: 0.8em; /* separate verb from path */ 220 | color:#fff; 221 | } 222 | 223 | 224 | 225 | 226 | /*---- print media */ 227 | 228 | @media print { 229 | body { background:white; color:black; margin:0; } 230 | #sidebar { 231 | display: none; 232 | } 233 | #content { 234 | position: relative; 235 | padding: 5px; 236 | left: 0px; 237 | top: 0px; 238 | } 239 | h1, h2, h4 { 240 | page-break-after: avoid; 241 | } 242 | pre { 243 | page-break-inside: avoid; 244 | } 245 | } 246 | 247 | /* tables still need cellspacing="0" in the markup */ 248 | table { 249 | border-collapse:collapse; border-spacing:0; 250 | margin: 20px 0; 251 | } 252 | th, 253 | td { 254 | border: solid #aaa; 255 | border-width: 1px 0; 256 | line-height: 23px; 257 | padding: 0 12px; 258 | text-align: left; 259 | vertical-align: text-bottom; 260 | } 261 | th { 262 | border-collapse: separate; 263 | } 264 | tbody tr:nth-child(odd) { 265 | background-color: #f2f0ee; 266 | } 267 | -------------------------------------------------------------------------------- /docs/branding/public/media/img/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /docs/branding/template.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | %(title)s 5 | 6 | 7 | 8 | 9 | 10 | 13 | 33 | 34 |
35 | %(content)s 36 |
37 | 38 | 39 | 40 | -------------------------------------------------------------------------------- /docs/client.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Client API | ldapjs 3 | --- 4 | 5 | # ldapjs Client API 6 | 7 |
8 | 9 | This document covers the ldapjs client API and assumes that you are familiar 10 | with LDAP. If you're not, read the [guide](guide.html) first. 11 | 12 |
13 | 14 | # Create a client 15 | 16 | The code to create a new client looks like: 17 | 18 | ```js 19 | const ldap = require('ldapjs'); 20 | 21 | const client = ldap.createClient({ 22 | url: ['ldap://127.0.0.1:1389', 'ldap://127.0.0.2:1389'] 23 | }); 24 | 25 | client.on('connectError', (err) => { 26 | // handle connection error 27 | }) 28 | ``` 29 | 30 | You can use `ldap://` or `ldaps://`; the latter would connect over SSL (note 31 | that this will not use the LDAP TLS extended operation, but literally an SSL 32 | connection to port 636, as in LDAP v2). The full set of options to create a 33 | client is: 34 | 35 | |Attribute |Description | 36 | |---------------|-----------------------------------------------------------| 37 | |url |A string or array of valid LDAP URL(s) (proto/host/port) | 38 | |socketPath |Socket path if using AF\_UNIX sockets | 39 | |log |A compatible logger instance (Default: no-op logger) | 40 | |timeout |Milliseconds client should let operations live for before timing out (Default: Infinity)| 41 | |connectTimeout |Milliseconds client should wait before timing out on TCP connections (Default: OS default)| 42 | |tlsOptions |Additional options passed to TLS connection layer when connecting via `ldaps://` (See: The TLS docs for node.js)| 43 | |idleTimeout |Milliseconds after last activity before client emits idle event| 44 | |reconnect |Try to reconnect when the connection gets lost (Default is false)| 45 | 46 | ### url 47 | This parameter takes a single connection string or an array of connection strings 48 | as an input. In case an array is provided, the client tries to connect to the 49 | servers in given order. To achieve random server strategy (e.g. to distribute 50 | the load among the servers), please shuffle the array before passing it as an 51 | argument. 52 | 53 | ### Note On Logger 54 | 55 | A passed in logger is expected to conform to the [Bunyan](https://www.npmjs.com/package/bunyan) 56 | API. Specifically, the logger is expected to have a `child()` method. If a logger 57 | is supplied that does not have such a method, then a shim version is added 58 | that merely returns the passed in logger. 59 | 60 | Known compatible loggers are: 61 | 62 | + [Bunyan](https://www.npmjs.com/package/bunyan) 63 | + [Pino](https://www.npmjs.com/package/pino) 64 | 65 | 66 | ### Note On Error Handling 67 | 68 | The client is an `EventEmitter`. If you don't register an error handler and 69 | e.g. a connection error occurs, Node.js will print a stack trace and exit the 70 | process ([reference](https://nodejs.org/api/events.html#error-events)). 71 | 72 | ## Connection management 73 | 74 | As LDAP is a stateful protocol (as opposed to HTTP), having connections torn 75 | down from underneath you can be difficult to deal with. Several mechanisms 76 | have been provided to mitigate this trouble. 77 | 78 | ### Reconnect 79 | 80 | You can provide a Boolean option indicating if a reconnect should be tried. For 81 | more sophisticated control, you can provide an Object with the properties 82 | `initialDelay` (default: `100`), `maxDelay` (default: `10000`) and 83 | `failAfter` (default: `Infinity`). 84 | After the reconnect you maybe need to [bind](#bind) again. 85 | 86 | ## Client events 87 | 88 | The client is an `EventEmitter` and can emit the following events: 89 | 90 | |Event |Description | 91 | |---------------|----------------------------------------------------------| 92 | |error |General error | 93 | |connectRefused |Server refused connection. Most likely bad authentication | 94 | |connectTimeout |Server timeout | 95 | |connectError |Socket connection error | 96 | |setupError |Setup error after successful connection | 97 | |socketTimeout |Socket timeout | 98 | |resultError |Search result error | 99 | |timeout |Search result timeout | 100 | |destroy |After client is disconnected | 101 | |end |Socket end event | 102 | |close |Socket closed | 103 | |connect |Client connected | 104 | |idle |Idle timeout reached | 105 | 106 | ## Common patterns 107 | 108 | The last two parameters in every API are `controls` and `callback`. `controls` 109 | can be either a single instance of a `Control` or an array of `Control` objects. 110 | You can, and probably will, omit this option. 111 | 112 | Almost every operation has the callback form of `function(err, res)` where err 113 | will be an instance of an `LDAPError` (you can use `instanceof` to switch). 114 | You probably won't need to check the `res` parameter, but it's there if you do. 115 | 116 | # bind 117 | `bind(dn, password, controls, callback)` 118 | 119 | Performs a bind operation against the LDAP server. 120 | 121 | The bind API only allows LDAP 'simple' binds (equivalent to HTTP Basic 122 | Authentication) for now. Note that all client APIs can optionally take an array 123 | of `Control` objects. You probably don't need them though... 124 | 125 | Example: 126 | 127 | ```js 128 | client.bind('cn=root', 'secret', (err) => { 129 | assert.ifError(err); 130 | }); 131 | ``` 132 | 133 | # add 134 | `add(dn, entry, controls, callback)` 135 | 136 | Performs an add operation against the LDAP server. 137 | 138 | Allows you to add an entry (which is just a plain JS object), and as always, 139 | controls are optional. 140 | 141 | Example: 142 | 143 | ```js 144 | const entry = { 145 | cn: 'foo', 146 | sn: 'bar', 147 | email: ['foo@bar.com', 'foo1@bar.com'], 148 | objectclass: 'fooPerson' 149 | }; 150 | client.add('cn=foo, o=example', entry, (err) => { 151 | assert.ifError(err); 152 | }); 153 | ``` 154 | 155 | # compare 156 | `compare(dn, attribute, value, controls, callback)` 157 | 158 | Performs an LDAP compare operation with the given attribute and value against 159 | the entry referenced by dn. 160 | 161 | Example: 162 | 163 | ```js 164 | client.compare('cn=foo, o=example', 'sn', 'bar', (err, matched) => { 165 | assert.ifError(err); 166 | 167 | console.log('matched: ' + matched); 168 | }); 169 | ``` 170 | 171 | # del 172 | `del(dn, controls, callback)` 173 | 174 | 175 | Deletes an entry from the LDAP server. 176 | 177 | Example: 178 | 179 | ```js 180 | client.del('cn=foo, o=example', (err) => { 181 | assert.ifError(err); 182 | }); 183 | ``` 184 | 185 | # exop 186 | `exop(name, value, controls, callback)` 187 | 188 | Performs an LDAP extended operation against an LDAP server. `name` is typically 189 | going to be an OID (well, the RFC says it must be; however, ldapjs has no such 190 | restriction). `value` is completely arbitrary, and is whatever the exop says it 191 | should be. 192 | 193 | Example (performs an LDAP 'whois' extended op): 194 | 195 | ```js 196 | client.exop('1.3.6.1.4.1.4203.1.11.3', (err, value, res) => { 197 | assert.ifError(err); 198 | 199 | console.log('whois: ' + value); 200 | }); 201 | ``` 202 | 203 | # modify 204 | `modify(name, changes, controls, callback)` 205 | 206 | Performs an LDAP modify operation against the LDAP server. This API requires 207 | you to pass in a `Change` object, which is described below. Note that you can 208 | pass in a single `Change` or an array of `Change` objects. 209 | 210 | Example: 211 | 212 | ```js 213 | const change = new ldap.Change({ 214 | operation: 'add', 215 | modification: { 216 | type: 'pets', 217 | values: ['cat', 'dog'] 218 | } 219 | }); 220 | 221 | client.modify('cn=foo, o=example', change, (err) => { 222 | assert.ifError(err); 223 | }); 224 | ``` 225 | 226 | ## Change 227 | 228 | A `Change` object maps to the LDAP protocol of a modify change, and requires you 229 | to set the `operation` and `modification`. The `operation` is a string, and 230 | must be one of: 231 | 232 | | Operation | Description | 233 | |-----------|-------------| 234 | | replace | Replaces the attribute referenced in `modification`. If the modification has no values, it is equivalent to a delete. | 235 | | add | Adds the attribute value(s) referenced in `modification`. The attribute may or may not already exist. | 236 | | delete | Deletes the attribute (and all values) referenced in `modification`. | 237 | 238 | `modification` is just a plain old JS object with the required type and values you want. 239 | 240 | | Operation | Description | 241 | |-----------|-------------| 242 | | type | String that defines the attribute type for the modification. | 243 | | values | Defines the values for modification. | 244 | 245 | 246 | # modifyDN 247 | `modifyDN(dn, newDN, controls, callback)` 248 | 249 | Performs an LDAP modifyDN (rename) operation against an entry in the LDAP 250 | server. A couple points with this client API: 251 | 252 | * There is no ability to set "keep old dn." It's always going to flag the old 253 | dn to be purged. 254 | * The client code will automatically figure out if the request is a "new 255 | superior" request ("new superior" means move to a different part of the tree, 256 | as opposed to just renaming the leaf). 257 | 258 | Example: 259 | 260 | ```js 261 | client.modifyDN('cn=foo, o=example', 'cn=bar', (err) => { 262 | assert.ifError(err); 263 | }); 264 | ``` 265 | 266 | # search 267 | `search(base, options, controls, callback)` 268 | 269 | Performs a search operation against the LDAP server. 270 | 271 | The search operation is more complex than the other operations, so this one 272 | takes an `options` object for all the parameters. However, ldapjs makes some 273 | defaults for you so that if you pass nothing in, it's pretty much equivalent 274 | to an HTTP GET operation (i.e., base search against the DN, filter set to 275 | always match). 276 | 277 | Like every other operation, `base` is a DN string. 278 | 279 | Options can be a string representing a valid LDAP filter or an object 280 | containing the following fields: 281 | 282 | |Attribute |Description | 283 | |-----------|---------------------------------------------------| 284 | |scope |One of `base`, `one`, or `sub`. Defaults to `base`.| 285 | |filter |A string version of an LDAP filter (see below), or a programatically constructed `Filter` object. Defaults to `(objectclass=*)`.| 286 | |attributes |attributes to select and return (if these are set, the server will return *only* these attributes). Defaults to the empty set, which means all attributes. You can provide a string if you want a single attribute or an array of string for one or many.| 287 | |attrsOnly |boolean on whether you want the server to only return the names of the attributes, and not their values. Borderline useless. Defaults to false.| 288 | |sizeLimit |the maximum number of entries to return. Defaults to 0 (unlimited).| 289 | |timeLimit |the maximum amount of time the server should take in responding, in seconds. Defaults to 10. Lots of servers will ignore this.| 290 | |paged |enable and/or configure automatic result paging| 291 | 292 | Responses inside callback of the `search` method are an `EventEmitter` where you will get a notification for 293 | each `searchEntry` that comes back from the server. You will additionally be able to listen for a `searchRequest` 294 | , `searchReference`, `error` and `end` event. 295 | `searchRequest` is emitted immediately after every `SearchRequest` is sent with a `SearchRequest` parameter. You can do operations 296 | like `client.abandon` with `searchRequest.messageId` to abandon this search request. Note that the `error` event will 297 | only be for client/TCP errors, not LDAP error codes like the other APIs. You'll want to check the LDAP status code 298 | (likely for `0`) on the `end` event to assert success. LDAP search results can give you a lot of status codes, such as 299 | time or size exceeded, busy, inappropriate matching, etc., which is why this method doesn't try to wrap up the code 300 | matching. 301 | 302 | Example: 303 | 304 | ```js 305 | const opts = { 306 | filter: '(&(l=Seattle)(email=*@foo.com))', 307 | scope: 'sub', 308 | attributes: ['dn', 'sn', 'cn'] 309 | }; 310 | 311 | client.search('o=example', opts, (err, res) => { 312 | assert.ifError(err); 313 | 314 | res.on('searchRequest', (searchRequest) => { 315 | console.log('searchRequest: ', searchRequest.messageId); 316 | }); 317 | res.on('searchEntry', (entry) => { 318 | console.log('entry: ' + JSON.stringify(entry.pojo)); 319 | }); 320 | res.on('searchReference', (referral) => { 321 | console.log('referral: ' + referral.uris.join()); 322 | }); 323 | res.on('error', (err) => { 324 | console.error('error: ' + err.message); 325 | }); 326 | res.on('end', (result) => { 327 | console.log('status: ' + result.status); 328 | }); 329 | }); 330 | ``` 331 | 332 | ## Filter Strings 333 | 334 | The easiest way to write search filters is to write them compliant with RFC2254, 335 | which is "The string representation of LDAP search filters." Note that 336 | ldapjs doesn't support extensible matching, since it's one of those features 337 | that almost nobody actually uses in practice. 338 | 339 | Assuming you don't really want to read the RFC, search filters in LDAP are 340 | basically are a "tree" of attribute/value assertions, with the tree specified 341 | in prefix notation. For example, let's start simple, and build up a complicated 342 | filter. The most basic filter is equality, so let's assume you want to search 343 | for an attribute `email` with a value of `foo@bar.com`. The syntax would be: 344 | 345 | ``` 346 | (email=foo@bar.com) 347 | ``` 348 | 349 | ldapjs requires all filters to be surrounded by '()' blocks. Ok, that was easy. 350 | Let's now assume that you want to find all records where the email is actually 351 | just anything in the "@bar.com" domain and the location attribute is set to 352 | Seattle: 353 | 354 | ``` 355 | (&(email=*@bar.com)(l=Seattle)) 356 | ``` 357 | 358 | Now our filter is actually three LDAP filters. We have an `and` filter (single 359 | amp `&`), an `equality` filter `(the l=Seattle)`, and a `substring` filter. 360 | Substrings are wildcard filters. They use `*` as the wildcard. You can put more 361 | than one wildcard for a given string. For example you could do `(email=*@*bar.com)` 362 | to match any email of @bar.com or its subdomains like `"example@foo.bar.com"`. 363 | 364 | Now, let's say we also want to set our filter to include a 365 | specification that either the employeeType *not* be a manager nor a secretary: 366 | 367 | ``` 368 | (&(email=*@bar.com)(l=Seattle)(!(|(employeeType=manager)(employeeType=secretary)))) 369 | ``` 370 | 371 | The `not` character is represented as a `!`, the `or` as a single pipe `|`. 372 | It gets a little bit complicated, but it's actually quite powerful, and lets you 373 | find almost anything you're looking for. 374 | 375 | ## Paging 376 | Many LDAP server enforce size limits upon the returned result set (commonly 377 | 1000). In order to retrieve results beyond this limit, a `PagedResultControl` 378 | is passed between the client and server to iterate through the entire dataset. 379 | While callers could choose to do this manually via the `controls` parameter to 380 | `search()`, ldapjs has internal mechanisms to easily automate the process. The 381 | most simple way to use the paging automation is to set the `paged` option to 382 | true when performing a search: 383 | 384 | ```js 385 | const opts = { 386 | filter: '(objectclass=commonobject)', 387 | scope: 'sub', 388 | paged: true, 389 | sizeLimit: 200 390 | }; 391 | client.search('o=largedir', opts, (err, res) => { 392 | assert.ifError(err); 393 | res.on('searchEntry', (entry) => { 394 | // do per-entry processing 395 | }); 396 | res.on('page', (result) => { 397 | console.log('page end'); 398 | }); 399 | res.on('error', (resErr) => { 400 | assert.ifError(resErr); 401 | }); 402 | res.on('end', (result) => { 403 | console.log('done '); 404 | }); 405 | }); 406 | ``` 407 | 408 | This will enable paging with a default page size of 199 (`sizeLimit` - 1) and 409 | will output all of the resulting objects via the `searchEntry` event. At the 410 | end of each result during the operation, a `page` event will be emitted as 411 | well (which includes the intermediate `searchResult` object). 412 | 413 | For those wanting more precise control over the process, an object with several 414 | parameters can be provided for the `paged` option. The `pageSize` parameter 415 | sets the size of result pages requested from the server. If no value is 416 | specified, it will fall back to the default (100 or `sizeLimit` - 1, to obey 417 | the RFC). The `pagePause` parameter allows back-pressure to be exerted on the 418 | paged search operation by pausing at the end of each page. When enabled, a 419 | callback function is passed as an additional parameter to `page` events. The 420 | client will wait to request the next page until that callback is executed. 421 | 422 | Here is an example where both of those parameters are used: 423 | 424 | ```js 425 | const queue = new MyWorkQueue(someSlowWorkFunction); 426 | const opts = { 427 | filter: '(objectclass=commonobject)', 428 | scope: 'sub', 429 | paged: { 430 | pageSize: 250, 431 | pagePause: true 432 | }, 433 | }; 434 | client.search('o=largerdir', opts, (err, res) => { 435 | assert.ifError(err); 436 | res.on('searchEntry', (entry) => { 437 | // Submit incoming objects to queue 438 | queue.push(entry); 439 | }); 440 | res.on('page', (result, cb) => { 441 | // Allow the queue to flush before fetching next page 442 | queue.cbWhenFlushed(cb); 443 | }); 444 | res.on('error', (resErr) => { 445 | assert.ifError(resErr); 446 | }); 447 | res.on('end', (result) => { 448 | console.log('done'); 449 | }); 450 | }); 451 | ``` 452 | 453 | # starttls 454 | `starttls(options, controls, callback)` 455 | 456 | Attempt to secure existing LDAP connection via STARTTLS. 457 | 458 | Example: 459 | 460 | ```js 461 | const opts = { 462 | ca: [fs.readFileSync('mycacert.pem')] 463 | }; 464 | 465 | client.starttls(opts, (err, res) => { 466 | assert.ifError(err); 467 | 468 | // Client communication now TLS protected 469 | }); 470 | ``` 471 | 472 | 473 | # unbind 474 | `unbind(callback)` 475 | 476 | Performs an unbind operation against the LDAP server. 477 | 478 | Note that unbind operation is not an opposite operation 479 | for bind. Unbinding results in disconnecting the client 480 | regardless of whether a bind operation was performed. 481 | 482 | The `callback` argument is optional as unbind does 483 | not have a response. 484 | 485 | Example: 486 | 487 | ```js 488 | client.unbind((err) => { 489 | assert.ifError(err); 490 | }); 491 | ``` 492 | -------------------------------------------------------------------------------- /docs/dn.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: DN API | ldapjs 3 | --- 4 | 5 | # ldapjs DN API 6 | 7 |
8 | 9 | This document covers the ldapjs DN API and assumes that you are familiar 10 | with LDAP. If you're not, read the [guide](guide.html) first. 11 | 12 |
13 | 14 | DNs are LDAP distinguished names, and are composed of a set of RDNs (relative 15 | distinguished names). [RFC2253](http://www.ietf.org/rfc/rfc2253.txt) has the 16 | complete specification, but basically an RDN is an attribute value assertion 17 | with `=` as the seperator, like: `cn=foo` where 'cn' is 'commonName' and 'foo' 18 | is the value. You can have compound RDNs by using the `+` character: 19 | `cn=foo+sn=bar`. As stated above, DNs are a set of RDNs, typically separated 20 | with the `,` character, like: `cn=foo, ou=people, o=example`. This uniquely 21 | identifies an entry in the tree, and is read "bottom up". 22 | 23 | # parseDN(dnString) 24 | 25 | The `parseDN` API converts a string representation of a DN into an ldapjs DN 26 | object; in most cases this will be handled for you under the covers of the 27 | ldapjs framework, but if you need it, it's there. 28 | 29 | ```js 30 | const parseDN = require('ldapjs').parseDN; 31 | 32 | const dn = parseDN('cn=foo+sn=bar, ou=people, o=example'); 33 | console.log(dn.toString()); 34 | ``` 35 | 36 | # DN 37 | 38 | The DN object is largely what you'll be interacting with, since all the server 39 | APIs are setup to give you a DN object. 40 | 41 | ## childOf(dn) 42 | 43 | Returns a boolean indicating whether 'this' is a child of the passed in dn. The 44 | `dn` argument can be either a string or a DN. 45 | 46 | ```js 47 | server.add('o=example', (req, res, next) => { 48 | if (req.dn.childOf('ou=people, o=example')) { 49 | ... 50 | } else { 51 | ... 52 | } 53 | }); 54 | ``` 55 | 56 | ## parentOf(dn) 57 | 58 | The inverse of `childOf`; returns a boolean on whether or not `this` is a parent 59 | of the passed in dn. Like `childOf`, can take either a string or a DN. 60 | 61 | ```js 62 | server.add('o=example', (req, res, next) => { 63 | const dn = parseDN('ou=people, o=example'); 64 | if (dn.parentOf(req.dn)) { 65 | ... 66 | } else { 67 | ... 68 | } 69 | }); 70 | ``` 71 | 72 | ## equals(dn) 73 | 74 | Returns a boolean indicating whether `this` is equivalent to the passed in `dn` 75 | argument. `dn` can be a string or a DN. 76 | 77 | ```js 78 | server.add('o=example', (req, res, next) => { 79 | if (req.dn.equals('cn=foo, ou=people, o=example')) { 80 | ... 81 | } else { 82 | ... 83 | } 84 | }); 85 | ``` 86 | 87 | ## parent() 88 | 89 | Returns a DN object that is the direct parent of `this`. If there is no parent 90 | this can return `null` (e.g. `parseDN('o=example').parent()` will return null). 91 | 92 | 93 | ## format(options) 94 | 95 | Convert a DN object to string according to specified formatting options. These 96 | options are divided into two types. Preservation Options use data recorded 97 | during parsing to preserve details of the original DN. Modification options 98 | alter string formatting defaults. Preservation options _always_ take 99 | precedence over Modification Options. 100 | 101 | Preservation Options: 102 | 103 | - `keepOrder`: Order of multi-value RDNs. 104 | - `keepQuote`: RDN values which were quoted will remain so. 105 | - `keepSpace`: Leading/trailing spaces will be output. 106 | - `keepCase`: Parsed attribute name will be output instead of lowercased version. 107 | 108 | Modification Options: 109 | 110 | - `upperName`: RDN names will be uppercased instead of lowercased. 111 | - `skipSpace`: Disable trailing space after RDN separators 112 | 113 | ## setFormat(options) 114 | 115 | Sets the default `options` for string formatting when `toString` is called. 116 | It accepts the same parameters as `format`. 117 | 118 | 119 | ## toString() 120 | 121 | Returns the string representation of `this`. 122 | 123 | ```js 124 | server.add('o=example', (req, res, next) => { 125 | console.log(req.dn.toString()); 126 | }); 127 | ``` 128 | -------------------------------------------------------------------------------- /docs/errors.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Errors API | ldapjs 3 | --- 4 | 5 | # ldapjs Errors API 6 | 7 |
8 | 9 | This document covers the ldapjs errors API and assumes that you are familiar 10 | with LDAP. If you're not, read the [guide](guide.html) first. 11 | 12 |
13 | 14 | All errors in the ldapjs framework extend from an abstract error type called 15 | `LDAPError`. In addition to the properties listed below, all errors will have 16 | a `stack` property correctly set. 17 | 18 | In general, you'll be using the errors in ldapjs like: 19 | 20 | ```js 21 | const ldap = require('ldapjs'); 22 | 23 | const db = {}; 24 | 25 | server.add('o=example', (req, res, next) => { 26 | const parent = req.dn.parent(); 27 | if (parent) { 28 | if (!db[parent.toString()]) 29 | return next(new ldap.NoSuchObjectError(parent.toString())); 30 | } 31 | if (db[req.dn.toString()]) 32 | return next(new ldap.EntryAlreadyExistsError(req.dn.toString())); 33 | 34 | ... 35 | }); 36 | ``` 37 | 38 | I.e., if you just pass them into the `next()` handler, ldapjs will automatically 39 | return the appropriate LDAP error message, and stop the handler chain. 40 | 41 | All errors will have the following properties: 42 | 43 | ## code 44 | 45 | Returns the LDAP status code associated with this error. 46 | 47 | ## name 48 | 49 | The name of this error. 50 | 51 | ## message 52 | 53 | The message that will be returned to the client. 54 | 55 | # Complete list of LDAPError subclasses 56 | 57 | * OperationsError 58 | * ProtocolError 59 | * TimeLimitExceededError 60 | * SizeLimitExceededError 61 | * CompareFalseError 62 | * CompareTrueError 63 | * AuthMethodNotSupportedError 64 | * StrongAuthRequiredError 65 | * ReferralError 66 | * AdminLimitExceededError 67 | * UnavailableCriticalExtensionError 68 | * ConfidentialityRequiredError 69 | * SaslBindInProgressError 70 | * NoSuchAttributeError 71 | * UndefinedAttributeTypeError 72 | * InappropriateMatchingError 73 | * ConstraintViolationError 74 | * AttributeOrValueExistsError 75 | * InvalidAttriubteSyntaxError 76 | * NoSuchObjectError 77 | * AliasProblemError 78 | * InvalidDnSyntaxError 79 | * AliasDerefProblemError 80 | * InappropriateAuthenticationError 81 | * InvalidCredentialsError 82 | * InsufficientAccessRightsError 83 | * BusyError 84 | * UnavailableError 85 | * UnwillingToPerformError 86 | * LoopDetectError 87 | * NamingViolationError 88 | * ObjectclassViolationError 89 | * NotAllowedOnNonLeafError 90 | * NotAllowedOnRdnError 91 | * EntryAlreadyExistsError 92 | * ObjectclassModsProhibitedError 93 | * AffectsMultipleDsasError 94 | * OtherError 95 | -------------------------------------------------------------------------------- /docs/examples.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Examples | ldapjs 3 | --- 4 | 5 | # ldapjs Examples 6 | 7 |
8 | 9 | This page contains a (hopefully) growing list of sample code to get you started 10 | with ldapjs. 11 | 12 |
13 | 14 | # In-memory server 15 | 16 | ```js 17 | const ldap = require('ldapjs'); 18 | 19 | 20 | ///--- Shared handlers 21 | 22 | function authorize(req, res, next) { 23 | /* Any user may search after bind, only cn=root has full power */ 24 | const isSearch = (req instanceof ldap.SearchRequest); 25 | if (!req.connection.ldap.bindDN.equals('cn=root') && !isSearch) 26 | return next(new ldap.InsufficientAccessRightsError()); 27 | 28 | return next(); 29 | } 30 | 31 | 32 | ///--- Globals 33 | 34 | const SUFFIX = 'o=joyent'; 35 | const db = {}; 36 | const server = ldap.createServer(); 37 | 38 | 39 | 40 | server.bind('cn=root', (req, res, next) => { 41 | if (req.dn.toString() !== 'cn=root' || req.credentials !== 'secret') 42 | return next(new ldap.InvalidCredentialsError()); 43 | 44 | res.end(); 45 | return next(); 46 | }); 47 | 48 | server.add(SUFFIX, authorize, (req, res, next) => { 49 | const dn = req.dn.toString(); 50 | 51 | if (db[dn]) 52 | return next(new ldap.EntryAlreadyExistsError(dn)); 53 | 54 | db[dn] = req.toObject().attributes; 55 | res.end(); 56 | return next(); 57 | }); 58 | 59 | server.bind(SUFFIX, (req, res, next) => { 60 | const dn = req.dn.toString(); 61 | if (!db[dn]) 62 | return next(new ldap.NoSuchObjectError(dn)); 63 | 64 | if (!db[dn].userpassword) 65 | return next(new ldap.NoSuchAttributeError('userPassword')); 66 | 67 | if (db[dn].userpassword.indexOf(req.credentials) === -1) 68 | return next(new ldap.InvalidCredentialsError()); 69 | 70 | res.end(); 71 | return next(); 72 | }); 73 | 74 | server.compare(SUFFIX, authorize, (req, res, next) => { 75 | const dn = req.dn.toString(); 76 | if (!db[dn]) 77 | return next(new ldap.NoSuchObjectError(dn)); 78 | 79 | if (!db[dn][req.attribute]) 80 | return next(new ldap.NoSuchAttributeError(req.attribute)); 81 | 82 | const matches = false; 83 | const vals = db[dn][req.attribute]; 84 | for (const value of vals) { 85 | if (value === req.value) { 86 | matches = true; 87 | break; 88 | } 89 | } 90 | 91 | res.end(matches); 92 | return next(); 93 | }); 94 | 95 | server.del(SUFFIX, authorize, (req, res, next) => { 96 | const dn = req.dn.toString(); 97 | if (!db[dn]) 98 | return next(new ldap.NoSuchObjectError(dn)); 99 | 100 | delete db[dn]; 101 | 102 | res.end(); 103 | return next(); 104 | }); 105 | 106 | server.modify(SUFFIX, authorize, (req, res, next) => { 107 | const dn = req.dn.toString(); 108 | if (!req.changes.length) 109 | return next(new ldap.ProtocolError('changes required')); 110 | if (!db[dn]) 111 | return next(new ldap.NoSuchObjectError(dn)); 112 | 113 | const entry = db[dn]; 114 | 115 | for (const change of req.changes) { 116 | mod = change.modification; 117 | switch (change.operation) { 118 | case 'replace': 119 | if (!entry[mod.type]) 120 | return next(new ldap.NoSuchAttributeError(mod.type)); 121 | 122 | if (!mod.vals || !mod.vals.length) { 123 | delete entry[mod.type]; 124 | } else { 125 | entry[mod.type] = mod.vals; 126 | } 127 | 128 | break; 129 | 130 | case 'add': 131 | if (!entry[mod.type]) { 132 | entry[mod.type] = mod.vals; 133 | } else { 134 | for (const v of mod.vals) { 135 | if (entry[mod.type].indexOf(v) === -1) 136 | entry[mod.type].push(v); 137 | } 138 | } 139 | 140 | break; 141 | 142 | case 'delete': 143 | if (!entry[mod.type]) 144 | return next(new ldap.NoSuchAttributeError(mod.type)); 145 | 146 | delete entry[mod.type]; 147 | 148 | break; 149 | } 150 | } 151 | 152 | res.end(); 153 | return next(); 154 | }); 155 | 156 | server.search(SUFFIX, authorize, (req, res, next) => { 157 | const dn = req.dn.toString(); 158 | if (!db[dn]) 159 | return next(new ldap.NoSuchObjectError(dn)); 160 | 161 | let scopeCheck; 162 | 163 | switch (req.scope) { 164 | case 'base': 165 | if (req.filter.matches(db[dn])) { 166 | res.send({ 167 | dn: dn, 168 | attributes: db[dn] 169 | }); 170 | } 171 | 172 | res.end(); 173 | return next(); 174 | 175 | case 'one': 176 | scopeCheck = (k) => { 177 | if (req.dn.equals(k)) 178 | return true; 179 | 180 | const parent = ldap.parseDN(k).parent(); 181 | return (parent ? parent.equals(req.dn) : false); 182 | }; 183 | break; 184 | 185 | case 'sub': 186 | scopeCheck = (k) => { 187 | return (req.dn.equals(k) || req.dn.parentOf(k)); 188 | }; 189 | 190 | break; 191 | } 192 | 193 | const keys = Object.keys(db); 194 | for (const key of keys) { 195 | if (!scopeCheck(key)) 196 | return; 197 | 198 | if (req.filter.matches(db[key])) { 199 | res.send({ 200 | dn: key, 201 | attributes: db[key] 202 | }); 203 | } 204 | } 205 | 206 | res.end(); 207 | return next(); 208 | }); 209 | 210 | 211 | 212 | ///--- Fire it up 213 | 214 | server.listen(1389, () => { 215 | console.log('LDAP server up at: %s', server.url); 216 | }); 217 | ``` 218 | 219 | # /etc/passwd server 220 | 221 | ```js 222 | const fs = require('fs'); 223 | const ldap = require('ldapjs'); 224 | const { spawn } = require('child_process'); 225 | 226 | 227 | 228 | ///--- Shared handlers 229 | 230 | function authorize(req, res, next) { 231 | if (!req.connection.ldap.bindDN.equals('cn=root')) 232 | return next(new ldap.InsufficientAccessRightsError()); 233 | 234 | return next(); 235 | } 236 | 237 | 238 | function loadPasswdFile(req, res, next) { 239 | fs.readFile('/etc/passwd', 'utf8', (err, data) => { 240 | if (err) 241 | return next(new ldap.OperationsError(err.message)); 242 | 243 | req.users = {}; 244 | 245 | const lines = data.split('\n'); 246 | for (const line of lines) { 247 | if (!line || /^#/.test(line)) 248 | continue; 249 | 250 | const record = line.split(':'); 251 | if (!record || !record.length) 252 | continue; 253 | 254 | req.users[record[0]] = { 255 | dn: 'cn=' + record[0] + ', ou=users, o=myhost', 256 | attributes: { 257 | cn: record[0], 258 | uid: record[2], 259 | gid: record[3], 260 | description: record[4], 261 | homedirectory: record[5], 262 | shell: record[6] || '', 263 | objectclass: 'unixUser' 264 | } 265 | }; 266 | } 267 | 268 | return next(); 269 | }); 270 | } 271 | 272 | 273 | const pre = [authorize, loadPasswdFile]; 274 | 275 | 276 | 277 | ///--- Mainline 278 | 279 | const server = ldap.createServer(); 280 | 281 | server.bind('cn=root', (req, res, next) => { 282 | if (req.dn.toString() !== 'cn=root' || req.credentials !== 'secret') 283 | return next(new ldap.InvalidCredentialsError()); 284 | 285 | res.end(); 286 | return next(); 287 | }); 288 | 289 | 290 | server.add('ou=users, o=myhost', pre, (req, res, next) => { 291 | if (!req.dn.rdns[0].cn) 292 | return next(new ldap.ConstraintViolationError('cn required')); 293 | 294 | if (req.users[req.dn.rdns[0].cn]) 295 | return next(new ldap.EntryAlreadyExistsError(req.dn.toString())); 296 | 297 | const entry = req.toObject().attributes; 298 | 299 | if (entry.objectclass.indexOf('unixUser') === -1) 300 | return next(new ldap.ConstraintViolationError('entry must be a unixUser')); 301 | 302 | const opts = ['-m']; 303 | if (entry.description) { 304 | opts.push('-c'); 305 | opts.push(entry.description[0]); 306 | } 307 | if (entry.homedirectory) { 308 | opts.push('-d'); 309 | opts.push(entry.homedirectory[0]); 310 | } 311 | if (entry.gid) { 312 | opts.push('-g'); 313 | opts.push(entry.gid[0]); 314 | } 315 | if (entry.shell) { 316 | opts.push('-s'); 317 | opts.push(entry.shell[0]); 318 | } 319 | if (entry.uid) { 320 | opts.push('-u'); 321 | opts.push(entry.uid[0]); 322 | } 323 | opts.push(entry.cn[0]); 324 | const useradd = spawn('useradd', opts); 325 | 326 | const messages = []; 327 | 328 | useradd.stdout.on('data', (data) => { 329 | messages.push(data.toString()); 330 | }); 331 | useradd.stderr.on('data', (data) => { 332 | messages.push(data.toString()); 333 | }); 334 | 335 | useradd.on('exit', (code) => { 336 | if (code !== 0) { 337 | let msg = '' + code; 338 | if (messages.length) 339 | msg += ': ' + messages.join(); 340 | return next(new ldap.OperationsError(msg)); 341 | } 342 | 343 | res.end(); 344 | return next(); 345 | }); 346 | }); 347 | 348 | 349 | server.modify('ou=users, o=myhost', pre, (req, res, next) => { 350 | if (!req.dn.rdns[0].cn || !req.users[req.dn.rdns[0].cn]) 351 | return next(new ldap.NoSuchObjectError(req.dn.toString())); 352 | 353 | if (!req.changes.length) 354 | return next(new ldap.ProtocolError('changes required')); 355 | 356 | const user = req.users[req.dn.rdns[0].cn].attributes; 357 | let mod; 358 | 359 | for (const change of req.changes) { 360 | mod = change.modification; 361 | switch (change.operation) { 362 | case 'replace': 363 | if (mod.type !== 'userpassword' || !mod.vals || !mod.vals.length) 364 | return next(new ldap.UnwillingToPerformError('only password updates ' + 365 | 'allowed')); 366 | break; 367 | case 'add': 368 | case 'delete': 369 | return next(new ldap.UnwillingToPerformError('only replace allowed')); 370 | } 371 | } 372 | 373 | const passwd = spawn('chpasswd', ['-c', 'MD5']); 374 | passwd.stdin.end(user.cn + ':' + mod.vals[0], 'utf8'); 375 | 376 | passwd.on('exit', (code) => { 377 | if (code !== 0) 378 | return next(new ldap.OperationsError('' + code)); 379 | 380 | res.end(); 381 | return next(); 382 | }); 383 | }); 384 | 385 | 386 | server.del('ou=users, o=myhost', pre, (req, res, next) => { 387 | if (!req.dn.rdns[0].cn || !req.users[req.dn.rdns[0].cn]) 388 | return next(new ldap.NoSuchObjectError(req.dn.toString())); 389 | 390 | const userdel = spawn('userdel', ['-f', req.dn.rdns[0].cn]); 391 | 392 | const messages = []; 393 | userdel.stdout.on('data', (data) => { 394 | messages.push(data.toString()); 395 | }); 396 | userdel.stderr.on('data', (data) => { 397 | messages.push(data.toString()); 398 | }); 399 | 400 | userdel.on('exit', (code) => { 401 | if (code !== 0) { 402 | let msg = '' + code; 403 | if (messages.length) 404 | msg += ': ' + messages.join(); 405 | return next(new ldap.OperationsError(msg)); 406 | } 407 | 408 | res.end(); 409 | return next(); 410 | }); 411 | }); 412 | 413 | 414 | server.search('o=myhost', pre, (req, res, next) => { 415 | const keys = Object.keys(req.users); 416 | for (const k of keys) { 417 | if (req.filter.matches(req.users[k].attributes)) 418 | res.send(req.users[k]); 419 | } 420 | 421 | res.end(); 422 | return next(); 423 | }); 424 | 425 | 426 | 427 | // LDAP "standard" listens on 389, but whatever. 428 | server.listen(1389, '127.0.0.1', () => { 429 | console.log('/etc/passwd LDAP server up at: %s', server.url); 430 | }); 431 | ``` 432 | 433 | # Address Book 434 | 435 | This example is courtesy of [Diogo Resende](https://github.com/dresende) and 436 | illustrates setting up an address book for typical mail clients such as 437 | Thunderbird or Evolution over a MySQL database. 438 | 439 | ```js 440 | // MySQL test: (create on database 'abook' with username 'abook' and password 'abook') 441 | // 442 | // CREATE TABLE IF NOT EXISTS `users` ( 443 | // `id` int(5) unsigned NOT NULL AUTO_INCREMENT, 444 | // `username` varchar(50) NOT NULL, 445 | // `password` varchar(50) NOT NULL, 446 | // PRIMARY KEY (`id`), 447 | // KEY `username` (`username`) 448 | // ) ENGINE=InnoDB DEFAULT CHARSET=utf8; 449 | // INSERT INTO `users` (`username`, `password`) VALUES 450 | // ('demo', 'demo'); 451 | // CREATE TABLE IF NOT EXISTS `contacts` ( 452 | // `id` int(5) unsigned NOT NULL AUTO_INCREMENT, 453 | // `user_id` int(5) unsigned NOT NULL, 454 | // `name` varchar(100) NOT NULL, 455 | // `email` varchar(255) NOT NULL, 456 | // PRIMARY KEY (`id`), 457 | // KEY `user_id` (`user_id`) 458 | // ) ENGINE=InnoDB DEFAULT CHARSET=utf8; 459 | // INSERT INTO `contacts` (`user_id`, `name`, `email`) VALUES 460 | // (1, 'John Doe', 'john.doe@example.com'), 461 | // (1, 'Jane Doe', 'jane.doe@example.com'); 462 | // 463 | 464 | const ldap = require('ldapjs'); 465 | const mysql = require("mysql"); 466 | const server = ldap.createServer(); 467 | const addrbooks = {}; 468 | const userinfo = {}; 469 | const ldap_port = 389; 470 | const basedn = "dc=example, dc=com"; 471 | const company = "Example"; 472 | const db = mysql.createClient({ 473 | user: "abook", 474 | password: "abook", 475 | database: "abook" 476 | }); 477 | 478 | db.query("SELECT c.*,u.username,u.password " + 479 | "FROM contacts c JOIN users u ON c.user_id=u.id", 480 | (err, contacts) => { 481 | if (err) { 482 | console.log("Error fetching contacts", err); 483 | process.exit(1); 484 | } 485 | 486 | for (const contact of contacts) { 487 | if (!addrbooks.hasOwnProperty(contact.username)) { 488 | addrbooks[contact.username] = []; 489 | userinfo["cn=" + contact.username + ", " + basedn] = { 490 | abook: addrbooks[contact.username], 491 | pwd: contact.password 492 | }; 493 | } 494 | 495 | const p = contact.name.indexOf(" "); 496 | if (p != -1) 497 | contact.firstname = contact.name.substr(0, p); 498 | 499 | p = contact.name.lastIndexOf(" "); 500 | if (p != -1) 501 | contact.surname = contact.name.substr(p + 1); 502 | 503 | addrbooks[contact.username].push({ 504 | dn: "cn=" + contact.name + ", " + basedn, 505 | attributes: { 506 | objectclass: [ "top" ], 507 | cn: contact.name, 508 | mail: contact.email, 509 | givenname: contact.firstname, 510 | sn: contact.surname, 511 | ou: company 512 | } 513 | }); 514 | } 515 | 516 | server.bind(basedn, (req, res, next) => { 517 | const username = req.dn.toString(); 518 | const password = req.credentials; 519 | 520 | if (!userinfo.hasOwnProperty(username) || 521 | userinfo[username].pwd != password) { 522 | return next(new ldap.InvalidCredentialsError()); 523 | } 524 | 525 | res.end(); 526 | return next(); 527 | }); 528 | 529 | server.search(basedn, (req, res, next) => { 530 | const binddn = req.connection.ldap.bindDN.toString(); 531 | 532 | if (userinfo.hasOwnProperty(binddn)) { 533 | for (const abook of userinfo[binddn].abook) { 534 | if (req.filter.matches(abook.attributes)) 535 | res.send(abook); 536 | } 537 | } 538 | res.end(); 539 | }); 540 | 541 | server.listen(ldap_port, () => { 542 | console.log("Addressbook started at %s", server.url); 543 | }); 544 | }); 545 | ``` 546 | 547 | To test out this example, try: 548 | 549 | ```shell 550 | $ ldapsearch -H ldap://localhost:389 -x -D cn=demo,dc=example,dc=com \ 551 | -w demo -b "dc=example,dc=com" objectclass=* 552 | ``` 553 | 554 | # Multi-threaded Server 555 | 556 | 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. 557 | 558 | ```js 559 | const cluster = require('cluster'); 560 | const ldap = require('ldapjs'); 561 | const net = require('net'); 562 | const os = require('os'); 563 | 564 | const threads = []; 565 | threads.getNext = function () { 566 | return (Math.floor(Math.random() * this.length)); 567 | }; 568 | 569 | const serverOptions = { 570 | port: 1389 571 | }; 572 | 573 | if (cluster.isMaster) { 574 | const server = net.createServer(serverOptions, (socket) => { 575 | socket.pause(); 576 | console.log('ldapjs client requesting connection'); 577 | let routeTo = threads.getNext(); 578 | threads[routeTo].send({ type: 'connection' }, socket); 579 | }); 580 | 581 | for (let i = 0; i < os.cpus().length; i++) { 582 | let thread = cluster.fork({ 583 | 'id': i 584 | }); 585 | thread.id = i; 586 | thread.on('message', function (msg) { 587 | 588 | }); 589 | threads.push(thread); 590 | } 591 | 592 | server.listen(serverOptions.port, function () { 593 | console.log('ldapjs listening at ldap://127.0.0.1:' + serverOptions.port); 594 | }); 595 | } else { 596 | const server = ldap.createServer(serverOptions); 597 | 598 | let threadId = process.env.id; 599 | 600 | process.on('message', (msg, socket) => { 601 | switch (msg.type) { 602 | case 'connection': 603 | server.newConnection(socket); 604 | socket.resume(); 605 | console.log('ldapjs client connection accepted on ' + threadId.toString()); 606 | } 607 | }); 608 | 609 | server.search('dc=example', function (req, res, next) { 610 | console.log('ldapjs search initiated on ' + threadId.toString()); 611 | var obj = { 612 | dn: req.dn.toString(), 613 | attributes: { 614 | objectclass: ['organization', 'top'], 615 | o: 'example' 616 | } 617 | }; 618 | 619 | if (req.filter.matches(obj.attributes)) 620 | res.send(obj); 621 | 622 | res.end(); 623 | }); 624 | } 625 | ``` 626 | -------------------------------------------------------------------------------- /docs/filters.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Filters API | ldapjs 3 | --- 4 | 5 | # ldapjs Filters API 6 | 7 |
8 | 9 | This document covers the ldapjs filters API and assumes that you are familiar 10 | with LDAP. If you're not, read the [guide](guide.html) first. 11 | 12 |
13 | 14 | LDAP search filters are really the backbone of LDAP search operations, and 15 | ldapjs tries to get you in "easy" with them if your dataset is small, and also 16 | lets you introspect them if you want to write a "query planner". For reference, 17 | make sure to read over [RFC2254](http://www.ietf.org/rfc/rfc2254.txt), as this 18 | explains the LDAPv3 text filter representation. 19 | 20 | ldapjs gives you a distinct object type mapping to each filter that is 21 | context-sensitive. However, _all_ filters have a `matches()` method on them, if 22 | that's all you need. Most filters will have an `attribute` property on them, 23 | since "simple" filters all operate on an attribute/value assertion. The 24 | "complex" filters are really aggregations of other filters (i.e. 'and'), and so 25 | these don't provide that property. 26 | 27 | All Filters in the ldapjs framework extend from `Filter`, which wil have the 28 | property `type` available; this will return a string name for the filter, and 29 | will be one of: 30 | 31 | # parseFilter(filterString) 32 | 33 | Parses an [RFC2254](http://www.ietf.org/rfc/rfc2254.txt) filter string into an 34 | ldapjs object(s). If the filter is "complex", it will be a "tree" of objects. 35 | For example: 36 | 37 | ```js 38 | const parseFilter = require('ldapjs').parseFilter; 39 | 40 | const f = parseFilter('(objectclass=*)'); 41 | ``` 42 | 43 | Is a "simple" filter, and would just return a `PresenceFilter` object. However, 44 | 45 | ```js 46 | const f = parseFilter('(&(employeeType=manager)(l=Seattle))'); 47 | ``` 48 | 49 | Would return an `AndFilter`, which would have a `filters` array of two 50 | `EqualityFilter` objects. 51 | 52 | `parseFilter` will throw if an invalid string is passed in (that is, a 53 | syntactically invalid string). 54 | 55 | # EqualityFilter 56 | 57 | The equality filter is used to check exact matching of attribute/value 58 | assertions. This object will have an `attribute` and `value` property, and the 59 | `name` property will be `equal`. 60 | 61 | The string syntax for an equality filter is `(attr=value)`. 62 | 63 | The `matches()` method will return true IFF the passed in object has a 64 | key matching `attribute` and a value matching `value`. 65 | 66 | ```js 67 | const f = new EqualityFilter({ 68 | attribute: 'cn', 69 | value: 'foo' 70 | }); 71 | 72 | f.matches({cn: 'foo'}); => true 73 | f.matches({cn: 'bar'}); => false 74 | ``` 75 | 76 | Equality matching uses "strict" type JavaScript comparison, and by default 77 | everything in ldapjs (and LDAP) is a UTF-8 string. If you want comparison 78 | of numbers, or something else, you'll need to use a middleware interceptor 79 | that transforms values of objects. 80 | 81 | # PresenceFilter 82 | 83 | The presence filter is used to check if an object has an attribute at all, with 84 | any value. This object will have an `attribute` property, and the `name` 85 | property will be `present`. 86 | 87 | The string syntax for a presence filter is `(attr=*)`. 88 | 89 | The `matches()` method will return true IFF the passed in object has a 90 | key matching `attribute`. 91 | 92 | ```js 93 | const f = new PresenceFilter({ 94 | attribute: 'cn' 95 | }); 96 | 97 | f.matches({cn: 'foo'}); => true 98 | f.matches({sn: 'foo'}); => false 99 | ``` 100 | 101 | # SubstringFilter 102 | 103 | The substring filter is used to do wildcard matching of a string value. This 104 | object will have an `attribute` property and then it will have an `initial` 105 | property, which is the prefix match, an `any` which will be an array of strings 106 | that are to be found _somewhere_ in the target string, and a `final` property, 107 | which will be the suffix match of the string. `any` and `final` are both 108 | optional. The `name` property will be `substring`. 109 | 110 | The string syntax for a presence filter is `(attr=foo*bar*cat*dog)`, which would 111 | map to: 112 | 113 | ```js 114 | { 115 | initial: 'foo', 116 | any: ['bar', 'cat'], 117 | final: 'dog' 118 | } 119 | ``` 120 | 121 | The `matches()` method will return true IFF the passed in object has a 122 | key matching `attribute` and the "regex" matches the value 123 | 124 | ```js 125 | const f = new SubstringFilter({ 126 | attribute: 'cn', 127 | initial: 'foo', 128 | any: ['bar'], 129 | final: 'baz' 130 | }); 131 | 132 | f.matches({cn: 'foobigbardogbaz'}); => true 133 | f.matches({sn: 'fobigbardogbaz'}); => false 134 | ``` 135 | 136 | # GreaterThanEqualsFilter 137 | 138 | The ge filter is used to do comparisons and ordering based on the value type. As 139 | mentioned elsewhere, by default everything in LDAP and ldapjs is a string, so 140 | this filter's `matches()` would be using lexicographical ordering of strings. 141 | If you wanted `>=` semantics over numeric values, you would need to add some 142 | middleware to convert values before comparison (and the value of the filter). 143 | Note that the ldapjs schema middleware will do this. 144 | 145 | The GreaterThanEqualsFilter will have an `attribute` property, a `value` 146 | property and the `name` property will be `ge`. 147 | 148 | The string syntax for a ge filter is: 149 | 150 | ``` 151 | (cn>=foo) 152 | ``` 153 | 154 | The `matches()` method will return true IFF the passed in object has a 155 | key matching `attribute` and the value is `>=` this filter's `value`. 156 | 157 | ```js 158 | const f = new GreaterThanEqualsFilter({ 159 | attribute: 'cn', 160 | value: 'foo', 161 | }); 162 | 163 | f.matches({cn: 'foobar'}); => true 164 | f.matches({cn: 'abc'}); => false 165 | ``` 166 | 167 | # LessThanEqualsFilter 168 | 169 | The le filter is used to do comparisons and ordering based on the value type. As 170 | mentioned elsewhere, by default everything in LDAP and ldapjs is a string, so 171 | this filter's `matches()` would be using lexicographical ordering of strings. 172 | If you wanted `<=` semantics over numeric values, you would need to add some 173 | middleware to convert values before comparison (and the value of the filter). 174 | Note that the ldapjs schema middleware will do this. 175 | 176 | The string syntax for a le filter is: 177 | 178 | ``` 179 | (cn<=foo) 180 | ``` 181 | 182 | The LessThanEqualsFilter will have an `attribute` property, a `value` 183 | property and the `name` property will be `le`. 184 | 185 | The `matches()` method will return true IFF the passed in object has a 186 | key matching `attribute` and the value is `<=` this filter's `value`. 187 | 188 | ```js 189 | const f = new LessThanEqualsFilter({ 190 | attribute: 'cn', 191 | value: 'foo', 192 | }); 193 | 194 | f.matches({cn: 'abc'}); => true 195 | f.matches({cn: 'foobar'}); => false 196 | ``` 197 | 198 | # AndFilter 199 | 200 | The and filter is a complex filter that simply contains "child" filters. The 201 | object will have a `filters` property which is an array of `Filter` objects. The 202 | `name` property will be `and`. 203 | 204 | The string syntax for an and filter is (assuming below we're and'ing two 205 | equality filters): 206 | 207 | ``` 208 | (&(cn=foo)(sn=bar)) 209 | ``` 210 | 211 | The `matches()` method will return true IFF the passed in object matches all 212 | the filters in the `filters` array. 213 | 214 | ```js 215 | const f = new AndFilter({ 216 | filters: [ 217 | new EqualityFilter({ 218 | attribute: 'cn', 219 | value: 'foo' 220 | }), 221 | new EqualityFilter({ 222 | attribute: 'sn', 223 | value: 'bar' 224 | }) 225 | ] 226 | }); 227 | 228 | f.matches({cn: 'foo', sn: 'bar'}); => true 229 | f.matches({cn: 'foo', sn: 'baz'}); => false 230 | ``` 231 | 232 | # OrFilter 233 | 234 | The or filter is a complex filter that simply contains "child" filters. The 235 | object will have a `filters` property which is an array of `Filter` objects. The 236 | `name` property will be `or`. 237 | 238 | The string syntax for an or filter is (assuming below we're or'ing two 239 | equality filters): 240 | 241 | ``` 242 | (|(cn=foo)(sn=bar)) 243 | ``` 244 | 245 | The `matches()` method will return true IFF the passed in object matches *any* 246 | of the filters in the `filters` array. 247 | 248 | ```js 249 | const f = new OrFilter({ 250 | filters: [ 251 | new EqualityFilter({ 252 | attribute: 'cn', 253 | value: 'foo' 254 | }), 255 | new EqualityFilter({ 256 | attribute: 'sn', 257 | value: 'bar' 258 | }) 259 | ] 260 | }); 261 | 262 | f.matches({cn: 'foo', sn: 'baz'}); => true 263 | f.matches({cn: 'bar', sn: 'baz'}); => false 264 | ``` 265 | 266 | # NotFilter 267 | 268 | The not filter is a complex filter that contains a single "child" filter. The 269 | object will have a `filter` property which is an instance of a `Filter` object. 270 | The `name` property will be `not`. 271 | 272 | The string syntax for a not filter is (assuming below we're not'ing an 273 | equality filter): 274 | 275 | ``` 276 | (!(cn=foo)) 277 | ``` 278 | 279 | The `matches()` method will return true IFF the passed in object does not match 280 | the filter in the `filter` property. 281 | 282 | ```js 283 | const f = new NotFilter({ 284 | filter: new EqualityFilter({ 285 | attribute: 'cn', 286 | value: 'foo' 287 | }) 288 | }); 289 | 290 | f.matches({cn: 'bar'}); => true 291 | f.matches({cn: 'foo'}); => false 292 | ``` 293 | 294 | # ApproximateFilter 295 | 296 | The approximate filter is used to check "approximate" matching of 297 | attribute/value assertions. This object will have an `attribute` and 298 | `value` property, and the `name` property will be `approx`. 299 | 300 | As a side point, this is a useless filter. It's really only here if you have 301 | some whacky client that's sending this. It just does an exact match (which 302 | is what ActiveDirectory does too). 303 | 304 | The string syntax for an equality filter is `(attr~=value)`. 305 | 306 | The `matches()` method will return true IFF the passed in object has a 307 | key matching `attribute` and a value exactly matching `value`. 308 | 309 | ```js 310 | const f = new ApproximateFilter({ 311 | attribute: 'cn', 312 | value: 'foo' 313 | }); 314 | 315 | f.matches({cn: 'foo'}); => true 316 | f.matches({cn: 'bar'}); => false 317 | ``` 318 | -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: ldapjs 3 | --- 4 | 5 |
6 | Reimagining LDAP for Node.js 7 |
8 | 9 | # Overview 10 | 11 |
12 | 13 | ldapjs is a pure JavaScript, from-scratch framework for implementing 14 | [LDAP](http://tools.ietf.org/html/rfc4510) clients and servers in 15 | [Node.js](http://nodejs.org). It is intended for developers used to interacting 16 | with HTTP services in node and [restify](http://restify.com). 17 | 18 |
19 | 20 | ```js 21 | const ldap = require('ldapjs'); 22 | 23 | const server = ldap.createServer(); 24 | 25 | server.search('o=example', (req, res, next) => { 26 | const obj = { 27 | dn: req.dn.toString(), 28 | attributes: { 29 | objectclass: ['organization', 'top'], 30 | o: 'example' 31 | } 32 | }; 33 | 34 | if (req.filter.matches(obj.attributes)) 35 | res.send(obj); 36 | 37 | res.end(); 38 | }); 39 | 40 | server.listen(1389, () => { 41 | console.log('LDAP server listening at %s', server.url); 42 | }); 43 | ``` 44 | 45 | Try hitting that with: 46 | 47 | ```shell 48 | $ ldapsearch -H ldap://localhost:1389 -x -b o=example objectclass=* 49 | ``` 50 | 51 | # Features 52 | 53 | ldapjs implements most of the common operations in the LDAP v3 RFC(s), for 54 | both client and server. It is 100% wire-compatible with the LDAP protocol 55 | itself, and is interoperable with [OpenLDAP](http://openldap.org) and any other 56 | LDAPv3-compliant implementation. ldapjs gives you a powerful routing and 57 | "intercepting filter" pattern for implementing server(s). It is intended 58 | that you can build LDAP over anything you want, not just traditional databases. 59 | 60 | # Getting started 61 | 62 | ```shell 63 | $ npm install ldapjs 64 | ``` 65 | 66 | If you're new to LDAP, check out the [guide](guide.html). Otherwise, the 67 | API documentation is: 68 | 69 | 70 | |Section |Content | 71 | |---------------------------|-------------------------------------------| 72 | |[Server API](server.html) |Reference for implementing LDAP servers. | 73 | |[Client API](client.html) |Reference for implementing LDAP clients. | 74 | |[DN API](dn.html) |API reference for the DN class. | 75 | |[Filter API](filters.html) |API reference for LDAP search filters. | 76 | |[Error API](errors.html) |Listing of all ldapjs Error objects. | 77 | |[Examples](examples.html) |Collection of sample/getting started code. | 78 | 79 | # More information 80 | 81 | - License:[MIT](http://opensource.org/licenses/mit-license.php) 82 | - Code: [ldapjs/node-ldapjs](https://github.com/ldapjs/node-ldapjs) 83 | 84 | # What's not in the box? 85 | 86 | Since most developers and system(s) adminstrators struggle with some of the 87 | esoteric features of LDAP, not all features in LDAP are implemented here. 88 | Specifically: 89 | 90 | * LDIF 91 | * Aliases 92 | * Attributes by OID 93 | * Extensible matching 94 | 95 | There are a few others, but those are the "big" ones. 96 | -------------------------------------------------------------------------------- /dt.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ldapjs/node-ldapjs/8ffd0bc9c149088a10ec4c1ec6a18450f76ad05d/dt.png -------------------------------------------------------------------------------- /examples/cluster-threading-net-server.js: -------------------------------------------------------------------------------- 1 | const cluster = require('cluster') 2 | const ldap = require('ldapjs') 3 | const net = require('net') 4 | const os = require('os') 5 | 6 | const threads = [] 7 | threads.getNext = function () { 8 | return (Math.floor(Math.random() * this.length)) 9 | } 10 | 11 | const serverOptions = { 12 | port: 1389 13 | } 14 | 15 | if (cluster.isMaster) { 16 | const server = net.createServer(serverOptions, (socket) => { 17 | socket.pause() 18 | console.log('ldapjs client requesting connection') 19 | const routeTo = threads.getNext() 20 | threads[routeTo].send({ type: 'connection' }, socket) 21 | }) 22 | 23 | for (let i = 0; i < os.cpus().length; i++) { 24 | const thread = cluster.fork({ 25 | id: i 26 | }) 27 | thread.id = i 28 | thread.on('message', function () { 29 | 30 | }) 31 | threads.push(thread) 32 | } 33 | 34 | server.listen(serverOptions.port, function () { 35 | console.log('ldapjs listening at ldap://127.0.0.1:' + serverOptions.port) 36 | }) 37 | } else { 38 | const server = ldap.createServer(serverOptions) 39 | 40 | const threadId = process.env.id 41 | 42 | process.on('message', (msg, socket) => { 43 | switch (msg.type) { 44 | case 'connection': 45 | server.newConnection(socket) 46 | socket.resume() 47 | console.log('ldapjs client connection accepted on ' + threadId.toString()) 48 | } 49 | }) 50 | 51 | server.search('dc=example', function (req, res) { 52 | console.log('ldapjs search initiated on ' + threadId.toString()) 53 | const obj = { 54 | dn: req.dn.toString(), 55 | attributes: { 56 | objectclass: ['organization', 'top'], 57 | o: 'example' 58 | } 59 | } 60 | 61 | if (req.filter.matches(obj.attributes)) { res.send(obj) } 62 | 63 | res.end() 64 | }) 65 | } 66 | -------------------------------------------------------------------------------- /examples/cluster-threading.js: -------------------------------------------------------------------------------- 1 | const cluster = require('cluster') 2 | const ldap = require('ldapjs') 3 | const os = require('os') 4 | 5 | const threads = [] 6 | threads.getNext = function () { 7 | return (Math.floor(Math.random() * this.length)) 8 | } 9 | 10 | const serverOptions = { 11 | connectionRouter: (socket) => { 12 | socket.pause() 13 | console.log('ldapjs client requesting connection') 14 | const routeTo = threads.getNext() 15 | threads[routeTo].send({ type: 'connection' }, socket) 16 | } 17 | } 18 | 19 | const server = ldap.createServer(serverOptions) 20 | 21 | if (cluster.isMaster) { 22 | for (let i = 0; i < os.cpus().length; i++) { 23 | const thread = cluster.fork({ 24 | id: i 25 | }) 26 | thread.id = i 27 | thread.on('message', function () { 28 | 29 | }) 30 | threads.push(thread) 31 | } 32 | 33 | server.listen(1389, function () { 34 | console.log('ldapjs listening at ' + server.url) 35 | }) 36 | } else { 37 | const threadId = process.env.id 38 | serverOptions.connectionRouter = () => { 39 | console.log('should not be hit') 40 | } 41 | 42 | process.on('message', (msg, socket) => { 43 | switch (msg.type) { 44 | case 'connection': 45 | server.newConnection(socket) 46 | socket.resume() 47 | console.log('ldapjs client connection accepted on ' + threadId.toString()) 48 | } 49 | }) 50 | 51 | server.search('dc=example', function (req, res) { 52 | console.log('ldapjs search initiated on ' + threadId.toString()) 53 | const obj = { 54 | dn: req.dn.toString(), 55 | attributes: { 56 | objectclass: ['organization', 'top'], 57 | o: 'example' 58 | } 59 | } 60 | 61 | if (req.filter.matches(obj.attributes)) { res.send(obj) } 62 | 63 | res.end() 64 | }) 65 | } 66 | -------------------------------------------------------------------------------- /examples/inmemory.js: -------------------------------------------------------------------------------- 1 | const ldap = require('../lib/index') 2 | 3 | /// --- Shared handlers 4 | 5 | function authorize (req, res, next) { 6 | /* Any user may search after bind, only cn=root has full power */ 7 | const isSearch = (req instanceof ldap.SearchRequest) 8 | if (!req.connection.ldap.bindDN.equals('cn=root') && !isSearch) { return next(new ldap.InsufficientAccessRightsError()) } 9 | 10 | return next() 11 | } 12 | 13 | /// --- Globals 14 | 15 | const SUFFIX = 'o=smartdc' 16 | const db = {} 17 | const server = ldap.createServer() 18 | 19 | server.bind('cn=root', function (req, res, next) { 20 | if (req.dn.toString() !== 'cn=root' || req.credentials !== 'secret') { return next(new ldap.InvalidCredentialsError()) } 21 | 22 | res.end() 23 | return next() 24 | }) 25 | 26 | server.add(SUFFIX, authorize, function (req, res, next) { 27 | const dn = req.dn.toString() 28 | 29 | if (db[dn]) { return next(new ldap.EntryAlreadyExistsError(dn)) } 30 | 31 | db[dn] = req.toObject().attributes 32 | res.end() 33 | return next() 34 | }) 35 | 36 | server.bind(SUFFIX, function (req, res, next) { 37 | const dn = req.dn.toString() 38 | if (!db[dn]) { return next(new ldap.NoSuchObjectError(dn)) } 39 | 40 | if (!db[dn].userpassword) { return next(new ldap.NoSuchAttributeError('userPassword')) } 41 | 42 | if (db[dn].userpassword.indexOf(req.credentials) === -1) { return next(new ldap.InvalidCredentialsError()) } 43 | 44 | res.end() 45 | return next() 46 | }) 47 | 48 | server.compare(SUFFIX, authorize, function (req, res, next) { 49 | const dn = req.dn.toString() 50 | if (!db[dn]) { return next(new ldap.NoSuchObjectError(dn)) } 51 | 52 | if (!db[dn][req.attribute]) { return next(new ldap.NoSuchAttributeError(req.attribute)) } 53 | 54 | let matches = false 55 | const vals = db[dn][req.attribute] 56 | for (let i = 0; i < vals.length; i++) { 57 | if (vals[i] === req.value) { 58 | matches = true 59 | break 60 | } 61 | } 62 | 63 | res.end(matches) 64 | return next() 65 | }) 66 | 67 | server.del(SUFFIX, authorize, function (req, res, next) { 68 | const dn = req.dn.toString() 69 | if (!db[dn]) { return next(new ldap.NoSuchObjectError(dn)) } 70 | 71 | delete db[dn] 72 | 73 | res.end() 74 | return next() 75 | }) 76 | 77 | server.modify(SUFFIX, authorize, function (req, res, next) { 78 | const dn = req.dn.toString() 79 | if (!req.changes.length) { return next(new ldap.ProtocolError('changes required')) } 80 | if (!db[dn]) { return next(new ldap.NoSuchObjectError(dn)) } 81 | 82 | const entry = db[dn] 83 | 84 | let mod 85 | for (let i = 0; i < req.changes.length; i++) { 86 | mod = req.changes[i].modification 87 | switch (req.changes[i].operation) { 88 | case 'replace': 89 | if (!entry[mod.type]) { return next(new ldap.NoSuchAttributeError(mod.type)) } 90 | 91 | if (!mod.vals || !mod.vals.length) { 92 | delete entry[mod.type] 93 | } else { 94 | entry[mod.type] = mod.vals 95 | } 96 | 97 | break 98 | 99 | case 'add': 100 | if (!entry[mod.type]) { 101 | entry[mod.type] = mod.vals 102 | } else { 103 | mod.vals.forEach(function (v) { 104 | if (entry[mod.type].indexOf(v) === -1) { entry[mod.type].push(v) } 105 | }) 106 | } 107 | 108 | break 109 | 110 | case 'delete': 111 | if (!entry[mod.type]) { return next(new ldap.NoSuchAttributeError(mod.type)) } 112 | 113 | delete entry[mod.type] 114 | 115 | break 116 | } 117 | } 118 | 119 | res.end() 120 | return next() 121 | }) 122 | 123 | server.search(SUFFIX, authorize, function (req, res, next) { 124 | const dn = req.dn.toString() 125 | if (!db[dn]) { return next(new ldap.NoSuchObjectError(dn)) } 126 | 127 | let scopeCheck 128 | 129 | switch (req.scope) { 130 | case 'base': 131 | if (req.filter.matches(db[dn])) { 132 | res.send({ 133 | dn, 134 | attributes: db[dn] 135 | }) 136 | } 137 | 138 | res.end() 139 | return next() 140 | 141 | case 'one': 142 | scopeCheck = function (k) { 143 | if (req.dn.equals(k)) { return true } 144 | 145 | const parent = ldap.parseDN(k).parent() 146 | return (parent ? parent.equals(req.dn) : false) 147 | } 148 | break 149 | 150 | case 'sub': 151 | scopeCheck = function (k) { 152 | return (req.dn.equals(k) || req.dn.parentOf(k)) 153 | } 154 | 155 | break 156 | } 157 | 158 | Object.keys(db).forEach(function (key) { 159 | if (!scopeCheck(key)) { return } 160 | 161 | if (req.filter.matches(db[key])) { 162 | res.send({ 163 | dn: key, 164 | attributes: db[key] 165 | }) 166 | } 167 | }) 168 | 169 | res.end() 170 | return next() 171 | }) 172 | 173 | /// --- Fire it up 174 | 175 | server.listen(1389, function () { 176 | console.log('LDAP server up at: %s', server.url) 177 | }) 178 | -------------------------------------------------------------------------------- /lib/client/constants.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | module.exports = { 4 | // https://tools.ietf.org/html/rfc4511#section-4.1.1 5 | // Message identifiers are an integer between (0, maxint). 6 | MAX_MSGID: Math.pow(2, 31) - 1 7 | } 8 | -------------------------------------------------------------------------------- /lib/client/index.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const logger = require('../logger') 4 | const Client = require('./client') 5 | 6 | module.exports = { 7 | Client, 8 | createClient: function createClient (options) { 9 | if (isObject(options) === false) throw TypeError('options (object) required') 10 | if (options.url && typeof options.url !== 'string' && !Array.isArray(options.url)) throw TypeError('options.url (string|array) required') 11 | if (options.socketPath && typeof options.socketPath !== 'string') throw TypeError('options.socketPath must be a string') 12 | if ((options.url && options.socketPath) || !(options.url || options.socketPath)) throw TypeError('options.url ^ options.socketPath (String) required') 13 | if (!options.log) options.log = logger 14 | if (isObject(options.log) !== true) throw TypeError('options.log must be an object') 15 | if (!options.log.child) options.log.child = function () { return options.log } 16 | 17 | return new Client(options) 18 | } 19 | } 20 | 21 | function isObject (input) { 22 | return Object.prototype.toString.apply(input) === '[object Object]' 23 | } 24 | -------------------------------------------------------------------------------- /lib/client/message-tracker/ge-window.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const { MAX_MSGID } = require('../constants') 4 | 5 | /** 6 | * Compare a reference id with another id to determine "greater than or equal" 7 | * between the two values according to a sliding window. 8 | * 9 | * @param {integer} ref 10 | * @param {integer} comp 11 | * 12 | * @returns {boolean} `true` if the `comp` value is >= to the `ref` value 13 | * within the computed window, otherwise `false`. 14 | */ 15 | module.exports = function geWindow (ref, comp) { 16 | let max = ref + Math.floor(MAX_MSGID / 2) 17 | const min = ref 18 | if (max >= MAX_MSGID) { 19 | // Handle roll-over 20 | max = max - MAX_MSGID - 1 21 | return ((comp <= max) || (comp >= min)) 22 | } else { 23 | return ((comp <= max) && (comp >= min)) 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /lib/client/message-tracker/id-generator.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const { MAX_MSGID } = require('../constants') 4 | 5 | /** 6 | * Returns a function that generates message identifiers. According to RFC 4511 7 | * the identifers should be `(0, MAX_MSGID)`. The returned function handles 8 | * this and wraps around when the maximum has been reached. 9 | * 10 | * @param {integer} [start=0] Starting number in the identifier sequence. 11 | * 12 | * @returns {function} This function accepts no parameters and returns an 13 | * increasing sequence identifier each invocation until it reaches the maximum 14 | * identifier. At this point the sequence starts over. 15 | */ 16 | module.exports = function idGeneratorFactory (start = 0) { 17 | let currentID = start 18 | return function nextID () { 19 | const id = currentID + 1 20 | currentID = (id >= MAX_MSGID) ? 1 : id 21 | return currentID 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /lib/client/message-tracker/index.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const idGeneratorFactory = require('./id-generator') 4 | const purgeAbandoned = require('./purge-abandoned') 5 | 6 | /** 7 | * Returns a message tracker object that keeps track of which message 8 | * identifiers correspond to which message handlers. Also handles keeping track 9 | * of abandoned messages. 10 | * 11 | * @param {object} options 12 | * @param {string} options.id An identifier for the tracker. 13 | * @param {object} options.parser An object that will be used to parse messages. 14 | * 15 | * @returns {MessageTracker} 16 | */ 17 | module.exports = function messageTrackerFactory (options) { 18 | if (Object.prototype.toString.call(options) !== '[object Object]') { 19 | throw Error('options object is required') 20 | } 21 | if (!options.id || typeof options.id !== 'string') { 22 | throw Error('options.id string is required') 23 | } 24 | if (!options.parser || Object.prototype.toString.call(options.parser) !== '[object Object]') { 25 | throw Error('options.parser object is required') 26 | } 27 | 28 | let currentID = 0 29 | const nextID = idGeneratorFactory() 30 | const messages = new Map() 31 | const abandoned = new Map() 32 | 33 | /** 34 | * @typedef {object} MessageTracker 35 | * @property {string} id The identifier of the tracker as supplied via the options. 36 | * @property {object} parser The parser object given by the the options. 37 | */ 38 | const tracker = { 39 | id: options.id, 40 | parser: options.parser 41 | } 42 | 43 | /** 44 | * Count of messages awaiting response. 45 | * 46 | * @alias pending 47 | * @memberof! MessageTracker# 48 | */ 49 | Object.defineProperty(tracker, 'pending', { 50 | get () { 51 | return messages.size 52 | } 53 | }) 54 | 55 | /** 56 | * Move a specific message to the abanded track. 57 | * 58 | * @param {integer} msgID The identifier for the message to move. 59 | * 60 | * @memberof MessageTracker 61 | * @method abandon 62 | */ 63 | tracker.abandon = function abandonMessage (msgID) { 64 | if (messages.has(msgID) === false) return false 65 | const toAbandon = messages.get(msgID) 66 | abandoned.set(msgID, { 67 | age: currentID, 68 | message: toAbandon.message, 69 | cb: toAbandon.callback 70 | }) 71 | return messages.delete(msgID) 72 | } 73 | 74 | /** 75 | * @typedef {object} Tracked 76 | * @property {object} message The tracked message. Usually the outgoing 77 | * request object. 78 | * @property {Function} callback The handler to use when receiving a 79 | * response to the tracked message. 80 | */ 81 | 82 | /** 83 | * Retrieves the message handler for a message. Removes abandoned messages 84 | * that have been given time to be resolved. 85 | * 86 | * @param {integer} msgID The identifier for the message to get the handler for. 87 | * 88 | * @memberof MessageTracker 89 | * @method fetch 90 | */ 91 | tracker.fetch = function fetchMessage (msgID) { 92 | const tracked = messages.get(msgID) 93 | if (tracked) { 94 | purgeAbandoned(msgID, abandoned) 95 | return tracked 96 | } 97 | 98 | // We sent an abandon request but the server either wasn't able to process 99 | // it or has not received it yet. Therefore, we received a response for the 100 | // abandoned message. So we must return the abandoned message's callback 101 | // to be processed normally. 102 | const abandonedMsg = abandoned.get(msgID) 103 | if (abandonedMsg) { 104 | return { message: abandonedMsg, callback: abandonedMsg.cb } 105 | } 106 | 107 | return null 108 | } 109 | 110 | /** 111 | * Removes all message tracks, cleans up the abandoned track, and invokes 112 | * a callback for each message purged. 113 | * 114 | * @param {function} cb A function with the signature `(msgID, handler)`. 115 | * 116 | * @memberof MessageTracker 117 | * @method purge 118 | */ 119 | tracker.purge = function purgeMessages (cb) { 120 | messages.forEach((val, key) => { 121 | purgeAbandoned(key, abandoned) 122 | tracker.remove(key) 123 | cb(key, val.callback) 124 | }) 125 | } 126 | 127 | /** 128 | * Removes a message from all tracking. 129 | * 130 | * @param {integer} msgID The identifier for the message to remove from tracking. 131 | * 132 | * @memberof MessageTracker 133 | * @method remove 134 | */ 135 | tracker.remove = function removeMessage (msgID) { 136 | if (messages.delete(msgID) === false) { 137 | abandoned.delete(msgID) 138 | } 139 | } 140 | 141 | /** 142 | * Add a message handler to be tracked. 143 | * 144 | * @param {object} message The message object to be tracked. This object will 145 | * have a new property added to it: `messageId`. 146 | * @param {function} callback The handler for the message. 147 | * 148 | * @memberof MessageTracker 149 | * @method track 150 | */ 151 | tracker.track = function trackMessage (message, callback) { 152 | currentID = nextID() 153 | // This side effect is not ideal but the client doesn't attach the tracker 154 | // to itself until after the `.connect` method has fired. If this can be 155 | // refactored later, then we can possibly get rid of this side effect. 156 | message.messageId = currentID 157 | messages.set(currentID, { callback, message }) 158 | } 159 | 160 | return tracker 161 | } 162 | -------------------------------------------------------------------------------- /lib/client/message-tracker/purge-abandoned.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const { AbandonedError } = require('../../errors') 4 | const geWindow = require('./ge-window') 5 | 6 | /** 7 | * Given a `msgID` and a set of `abandoned` messages, remove any abandoned 8 | * messages that existed _prior_ to the specified `msgID`. For example, let's 9 | * assume the server has sent 3 messages: 10 | * 11 | * 1. A search message. 12 | * 2. An abandon message for the search message. 13 | * 3. A new search message. 14 | * 15 | * When the response for message #1 comes in, if it does, it will be processed 16 | * normally due to the specification. Message #2 will not receive a response, or 17 | * if the server does send one since the spec sort of allows it, we won't do 18 | * anything with it because we just discard that listener. Now the response 19 | * for message #3 comes in. At this point, we will issue a purge of responses 20 | * by passing in `msgID = 3`. This result is that we will remove the tracking 21 | * for message #1. 22 | * 23 | * @param {integer} msgID An upper bound for the messages to be purged. 24 | * @param {Map} abandoned A set of abandoned messages. Each message is an object 25 | * `{ age: , cb: }` where `age` was the current message id when the 26 | * abandon message was sent. 27 | */ 28 | module.exports = function purgeAbandoned (msgID, abandoned) { 29 | abandoned.forEach((val, key) => { 30 | if (geWindow(val.age, msgID) === false) return 31 | val.cb(new AbandonedError('client request abandoned')) 32 | abandoned.delete(key) 33 | }) 34 | } 35 | -------------------------------------------------------------------------------- /lib/client/request-queue/enqueue.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | /** 4 | * Adds requests to the queue. If a timeout has been added to the queue then 5 | * this will freeze the queue with the newly added item, flush it, and then 6 | * unfreeze it when the queue has been cleared. 7 | * 8 | * @param {object} message An LDAP message object. 9 | * @param {object} expect An expectation object. 10 | * @param {object} emitter An event emitter or `null`. 11 | * @param {function} cb A callback to invoke when the request is finished. 12 | * 13 | * @returns {boolean} `true` if the requested was queued. `false` if the queue 14 | * is not accepting any requests. 15 | */ 16 | module.exports = function enqueue (message, expect, emitter, cb) { 17 | if (this._queue.size >= this.size || this._frozen) { 18 | return false 19 | } 20 | 21 | this._queue.add({ message, expect, emitter, cb }) 22 | 23 | if (this.timeout === 0) return true 24 | if (this._timer === null) return true 25 | 26 | // A queue can have a specified time allotted for it to be cleared. If that 27 | // time has been reached, reject new entries until the queue has been cleared. 28 | this._timer = setTimeout(queueTimeout.bind(this), this.timeout) 29 | 30 | return true 31 | 32 | function queueTimeout () { 33 | this.freeze() 34 | this.purge() 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /lib/client/request-queue/flush.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | /** 4 | * Invokes all requests in the queue by passing them to the supplied callback 5 | * function and then clears all items from the queue. 6 | * 7 | * @param {function} cb A function used to handle the requests. 8 | */ 9 | module.exports = function flush (cb) { 10 | if (this._timer) { 11 | clearTimeout(this._timer) 12 | this._timer = null 13 | } 14 | 15 | // We must get a local copy of the queue and clear it before iterating it. 16 | // The client will invoke this flush function _many_ times. If we try to 17 | // iterate it without a local copy and clearing first then we will overflow 18 | // the stack. 19 | const requests = Array.from(this._queue.values()) 20 | this._queue.clear() 21 | for (const req of requests) { 22 | cb(req.message, req.expect, req.emitter, req.cb) 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /lib/client/request-queue/index.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const enqueue = require('./enqueue') 4 | const flush = require('./flush') 5 | const purge = require('./purge') 6 | 7 | /** 8 | * Builds a request queue object and returns it. 9 | * 10 | * @param {object} [options] 11 | * @param {integer} [options.size] Maximum size of the request queue. Must be 12 | * a number greater than `0` if supplied. Default: `Infinity`. 13 | * @param {integer} [options.timeout] Time in milliseconds a queue has to 14 | * complete the requests it contains. 15 | * 16 | * @returns {object} A queue instance. 17 | */ 18 | module.exports = function requestQueueFactory (options) { 19 | const opts = Object.assign({}, options) 20 | const q = { 21 | size: (opts.size > 0) ? opts.size : Infinity, 22 | timeout: (opts.timeout > 0) ? opts.timeout : 0, 23 | _queue: new Set(), 24 | _timer: null, 25 | _frozen: false 26 | } 27 | 28 | q.enqueue = enqueue.bind(q) 29 | q.flush = flush.bind(q) 30 | q.purge = purge.bind(q) 31 | q.freeze = function freeze () { 32 | this._frozen = true 33 | } 34 | q.thaw = function thaw () { 35 | this._frozen = false 36 | } 37 | 38 | return q 39 | } 40 | -------------------------------------------------------------------------------- /lib/client/request-queue/purge.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const { TimeoutError } = require('../../errors') 4 | 5 | /** 6 | * Flushes the queue by rejecting all pending requests with a timeout error. 7 | */ 8 | module.exports = function purge () { 9 | this.flush(function flushCB (a, b, c, cb) { 10 | cb(new TimeoutError('request queue timeout')) 11 | }) 12 | } 13 | -------------------------------------------------------------------------------- /lib/client/search_pager.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const EventEmitter = require('events').EventEmitter 4 | const util = require('util') 5 | const assert = require('assert-plus') 6 | const { PagedResultsControl } = require('@ldapjs/controls') 7 | const CorkedEmitter = require('../corked_emitter.js') 8 | 9 | /// --- API 10 | 11 | /** 12 | * Handler object for paged search operations. 13 | * 14 | * Provided to consumers in place of the normal search EventEmitter it adds the 15 | * following new events: 16 | * 1. page - Emitted whenever the end of a result page is encountered. 17 | * If this is the last page, 'end' will also be emitted. 18 | * The event passes two arguments: 19 | * 1. The result object (similar to 'end') 20 | * 2. A callback function optionally used to continue the search 21 | * operation if the pagePause option was specified during 22 | * initialization. 23 | * 2. pageError - Emitted if the server does not support paged search results 24 | * If there are no listeners for this event, the 'error' event 25 | * will be emitted (and 'end' will not be). By listening to 26 | * 'pageError', a successful search that lacks paging will be 27 | * able to emit 'end'. 28 | */ 29 | function SearchPager (opts) { 30 | assert.object(opts) 31 | assert.func(opts.callback) 32 | assert.number(opts.pageSize) 33 | assert.func(opts.sendRequest) 34 | 35 | CorkedEmitter.call(this, {}) 36 | 37 | this.callback = opts.callback 38 | this.controls = opts.controls 39 | this.pageSize = opts.pageSize 40 | this.pagePause = opts.pagePause 41 | this.sendRequest = opts.sendRequest 42 | 43 | this.controls.forEach(function (control) { 44 | if (control.type === PagedResultsControl.OID) { 45 | // The point of using SearchPager is not having to do this. 46 | // Toss an error if the pagedResultsControl is present 47 | throw new Error('redundant pagedResultControl') 48 | } 49 | }) 50 | 51 | this.finished = false 52 | this.started = false 53 | 54 | const emitter = new EventEmitter() 55 | emitter.on('searchRequest', this.emit.bind(this, 'searchRequest')) 56 | emitter.on('searchEntry', this.emit.bind(this, 'searchEntry')) 57 | emitter.on('end', this._onEnd.bind(this)) 58 | emitter.on('error', this._onError.bind(this)) 59 | this.childEmitter = emitter 60 | } 61 | util.inherits(SearchPager, CorkedEmitter) 62 | module.exports = SearchPager 63 | 64 | /** 65 | * Start the paged search. 66 | */ 67 | SearchPager.prototype.begin = function begin () { 68 | // Starting first page 69 | this._nextPage(null) 70 | } 71 | 72 | SearchPager.prototype._onEnd = function _onEnd (res) { 73 | const self = this 74 | let cookie = null 75 | res.controls.forEach(function (control) { 76 | if (control.type === PagedResultsControl.OID) { 77 | cookie = control.value.cookie 78 | } 79 | }) 80 | // Pass a noop callback by default for page events 81 | const nullCb = function () { } 82 | 83 | if (cookie === null) { 84 | // paged search not supported 85 | this.finished = true 86 | this.emit('page', res, nullCb) 87 | const err = new Error('missing paged control') 88 | err.name = 'PagedError' 89 | if (this.listeners('pageError').length > 0) { 90 | this.emit('pageError', err) 91 | // If the consumer as subscribed to pageError, SearchPager is absolved 92 | // from delivering the fault via the 'error' event. Emitting an 'end' 93 | // event after 'error' breaks the contract that the standard client 94 | // provides, so it's only a possibility if 'pageError' is used instead. 95 | this.emit('end', res) 96 | } else { 97 | this.emit('error', err) 98 | // No end event possible per explanation above. 99 | } 100 | return 101 | } 102 | 103 | if (cookie.length === 0) { 104 | // end of paged results 105 | this.finished = true 106 | this.emit('page', nullCb) 107 | this.emit('end', res) 108 | } else { 109 | if (this.pagePause) { 110 | // Wait to fetch next page until callback is invoked 111 | // Halt page fetching if called with error 112 | this.emit('page', res, function (err) { 113 | if (!err) { 114 | self._nextPage(cookie) 115 | } else { 116 | // the paged search has been canceled so emit an end 117 | self.emit('end', res) 118 | } 119 | }) 120 | } else { 121 | this.emit('page', res, nullCb) 122 | this._nextPage(cookie) 123 | } 124 | } 125 | } 126 | 127 | SearchPager.prototype._onError = function _onError (err) { 128 | this.finished = true 129 | this.emit('error', err) 130 | } 131 | 132 | /** 133 | * Initiate a search for the next page using the returned cookie value. 134 | */ 135 | SearchPager.prototype._nextPage = function _nextPage (cookie) { 136 | const controls = this.controls.slice(0) 137 | controls.push(new PagedResultsControl({ 138 | value: { 139 | size: this.pageSize, 140 | cookie 141 | } 142 | })) 143 | 144 | this.sendRequest(controls, this.childEmitter, this._sendCallback.bind(this)) 145 | } 146 | 147 | /** 148 | * Callback provided to the client API for successful transmission. 149 | */ 150 | SearchPager.prototype._sendCallback = function _sendCallback (err) { 151 | if (err) { 152 | this.finished = true 153 | if (!this.started) { 154 | // EmitSend error during the first page, bail via callback 155 | this.callback(err, null) 156 | } else { 157 | this.emit('error', err) 158 | } 159 | } else { 160 | // search successfully send 161 | if (!this.started) { 162 | this.started = true 163 | // send self as emitter as the client would 164 | this.callback(null, this) 165 | } 166 | } 167 | } 168 | -------------------------------------------------------------------------------- /lib/controls/index.js: -------------------------------------------------------------------------------- 1 | // Copyright 2011 Mark Cavage, Inc. All rights reserved. 2 | 3 | const controls = require('@ldapjs/controls') 4 | module.exports = controls 5 | -------------------------------------------------------------------------------- /lib/corked_emitter.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const EventEmitter = require('events').EventEmitter 4 | 5 | /** 6 | * A CorkedEmitter is a variant of an EventEmitter where events emitted 7 | * wait for the appearance of the first listener of any kind. That is, 8 | * a CorkedEmitter will store all .emit()s it receives, to be replayed 9 | * later when an .on() is applied. 10 | * It is meant for situations where the consumers of the emitter are 11 | * unable to register listeners right away, and cannot afford to miss 12 | * any events emitted from the start. 13 | * Note that, whenever the first emitter (for any event) appears, 14 | * the emitter becomes uncorked and works as usual for ALL events, and 15 | * will not cache anything anymore. This is necessary to avoid 16 | * re-ordering emits - either everything is being buffered, or nothing. 17 | */ 18 | function CorkedEmitter () { 19 | const self = this 20 | EventEmitter.call(self) 21 | /** 22 | * An array of arguments objects (array-likes) to emit on open. 23 | */ 24 | self._outstandingEmits = [] 25 | /** 26 | * Whether the normal flow of emits is restored yet. 27 | */ 28 | self._opened = false 29 | // When the first listener appears, we enqueue an opening. 30 | // It is not done immediately, so that other listeners can be 31 | // registered in the same critical section. 32 | self.once('newListener', function () { 33 | setImmediate(function releaseStoredEvents () { 34 | self._opened = true 35 | self._outstandingEmits.forEach(function (args) { 36 | self.emit.apply(self, args) 37 | }) 38 | }) 39 | }) 40 | } 41 | CorkedEmitter.prototype = Object.create(EventEmitter.prototype) 42 | CorkedEmitter.prototype.emit = function emit (eventName) { 43 | if (this._opened || eventName === 'newListener') { 44 | EventEmitter.prototype.emit.apply(this, arguments) 45 | } else { 46 | this._outstandingEmits.push(arguments) 47 | } 48 | } 49 | 50 | module.exports = CorkedEmitter 51 | -------------------------------------------------------------------------------- /lib/errors/codes.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | module.exports = { 4 | LDAP_SUCCESS: 0, 5 | LDAP_OPERATIONS_ERROR: 1, 6 | LDAP_PROTOCOL_ERROR: 2, 7 | LDAP_TIME_LIMIT_EXCEEDED: 3, 8 | LDAP_SIZE_LIMIT_EXCEEDED: 4, 9 | LDAP_COMPARE_FALSE: 5, 10 | LDAP_COMPARE_TRUE: 6, 11 | LDAP_AUTH_METHOD_NOT_SUPPORTED: 7, 12 | LDAP_STRONG_AUTH_REQUIRED: 8, 13 | LDAP_REFERRAL: 10, 14 | LDAP_ADMIN_LIMIT_EXCEEDED: 11, 15 | LDAP_UNAVAILABLE_CRITICAL_EXTENSION: 12, 16 | LDAP_CONFIDENTIALITY_REQUIRED: 13, 17 | LDAP_SASL_BIND_IN_PROGRESS: 14, 18 | LDAP_NO_SUCH_ATTRIBUTE: 16, 19 | LDAP_UNDEFINED_ATTRIBUTE_TYPE: 17, 20 | LDAP_INAPPROPRIATE_MATCHING: 18, 21 | LDAP_CONSTRAINT_VIOLATION: 19, 22 | LDAP_ATTRIBUTE_OR_VALUE_EXISTS: 20, 23 | LDAP_INVALID_ATTRIBUTE_SYNTAX: 21, 24 | LDAP_NO_SUCH_OBJECT: 32, 25 | LDAP_ALIAS_PROBLEM: 33, 26 | LDAP_INVALID_DN_SYNTAX: 34, 27 | LDAP_ALIAS_DEREF_PROBLEM: 36, 28 | LDAP_INAPPROPRIATE_AUTHENTICATION: 48, 29 | LDAP_INVALID_CREDENTIALS: 49, 30 | LDAP_INSUFFICIENT_ACCESS_RIGHTS: 50, 31 | LDAP_BUSY: 51, 32 | LDAP_UNAVAILABLE: 52, 33 | LDAP_UNWILLING_TO_PERFORM: 53, 34 | LDAP_LOOP_DETECT: 54, 35 | LDAP_SORT_CONTROL_MISSING: 60, 36 | LDAP_INDEX_RANGE_ERROR: 61, 37 | LDAP_NAMING_VIOLATION: 64, 38 | LDAP_OBJECTCLASS_VIOLATION: 65, 39 | LDAP_NOT_ALLOWED_ON_NON_LEAF: 66, 40 | LDAP_NOT_ALLOWED_ON_RDN: 67, 41 | LDAP_ENTRY_ALREADY_EXISTS: 68, 42 | LDAP_OBJECTCLASS_MODS_PROHIBITED: 69, 43 | LDAP_AFFECTS_MULTIPLE_DSAS: 71, 44 | LDAP_CONTROL_ERROR: 76, 45 | LDAP_OTHER: 80, 46 | LDAP_PROXIED_AUTHORIZATION_DENIED: 123 47 | } 48 | -------------------------------------------------------------------------------- /lib/errors/index.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const util = require('util') 4 | const assert = require('assert-plus') 5 | 6 | const LDAPResult = require('../messages').LDAPResult 7 | 8 | /// --- Globals 9 | 10 | const CODES = require('./codes') 11 | const ERRORS = [] 12 | 13 | /// --- Error Base class 14 | 15 | function LDAPError (message, dn, caller) { 16 | if (Error.captureStackTrace) { Error.captureStackTrace(this, caller || LDAPError) } 17 | 18 | this.lde_message = message 19 | this.lde_dn = dn 20 | } 21 | util.inherits(LDAPError, Error) 22 | Object.defineProperties(LDAPError.prototype, { 23 | name: { 24 | get: function getName () { return 'LDAPError' }, 25 | configurable: false 26 | }, 27 | code: { 28 | get: function getCode () { return CODES.LDAP_OTHER }, 29 | configurable: false 30 | }, 31 | message: { 32 | get: function getMessage () { 33 | return this.lde_message || this.name 34 | }, 35 | set: function setMessage (message) { 36 | this.lde_message = message 37 | }, 38 | configurable: false 39 | }, 40 | dn: { 41 | get: function getDN () { 42 | return (this.lde_dn ? this.lde_dn.toString() : '') 43 | }, 44 | configurable: false 45 | } 46 | }) 47 | 48 | /// --- Exported API 49 | 50 | module.exports = {} 51 | module.exports.LDAPError = LDAPError 52 | 53 | // Some whacky games here to make sure all the codes are exported 54 | Object.keys(CODES).forEach(function (code) { 55 | module.exports[code] = CODES[code] 56 | if (code === 'LDAP_SUCCESS') { return } 57 | 58 | let err = '' 59 | let msg = '' 60 | const pieces = code.split('_').slice(1) 61 | for (let i = 0; i < pieces.length; i++) { 62 | const lc = pieces[i].toLowerCase() 63 | const key = lc.charAt(0).toUpperCase() + lc.slice(1) 64 | err += key 65 | msg += key + ((i + 1) < pieces.length ? ' ' : '') 66 | } 67 | 68 | if (!/\w+Error$/.test(err)) { err += 'Error' } 69 | 70 | // At this point LDAP_OPERATIONS_ERROR is now OperationsError in $err 71 | // and 'Operations Error' in $msg 72 | module.exports[err] = function (message, dn, caller) { 73 | LDAPError.call(this, message, dn, caller || module.exports[err]) 74 | } 75 | module.exports[err].constructor = module.exports[err] 76 | util.inherits(module.exports[err], LDAPError) 77 | Object.defineProperties(module.exports[err].prototype, { 78 | name: { 79 | get: function getName () { return err }, 80 | configurable: false 81 | }, 82 | code: { 83 | get: function getCode () { return CODES[code] }, 84 | configurable: false 85 | } 86 | }) 87 | 88 | ERRORS[CODES[code]] = { 89 | err, 90 | message: msg 91 | } 92 | }) 93 | 94 | module.exports.getError = function (res) { 95 | assert.ok(res instanceof LDAPResult, 'res (LDAPResult) required') 96 | 97 | const errObj = ERRORS[res.status] 98 | const E = module.exports[errObj.err] 99 | return new E(res.errorMessage || errObj.message, 100 | res.matchedDN || null, 101 | module.exports.getError) 102 | } 103 | 104 | module.exports.getMessage = function (code) { 105 | assert.number(code, 'code (number) required') 106 | 107 | const errObj = ERRORS[code] 108 | return (errObj && errObj.message ? errObj.message : '') 109 | } 110 | 111 | /// --- Custom application errors 112 | 113 | function ConnectionError (message) { 114 | LDAPError.call(this, message, null, ConnectionError) 115 | } 116 | util.inherits(ConnectionError, LDAPError) 117 | module.exports.ConnectionError = ConnectionError 118 | Object.defineProperties(ConnectionError.prototype, { 119 | name: { 120 | get: function () { return 'ConnectionError' }, 121 | configurable: false 122 | } 123 | }) 124 | 125 | function AbandonedError (message) { 126 | LDAPError.call(this, message, null, AbandonedError) 127 | } 128 | util.inherits(AbandonedError, LDAPError) 129 | module.exports.AbandonedError = AbandonedError 130 | Object.defineProperties(AbandonedError.prototype, { 131 | name: { 132 | get: function () { return 'AbandonedError' }, 133 | configurable: false 134 | } 135 | }) 136 | 137 | function TimeoutError (message) { 138 | LDAPError.call(this, message, null, TimeoutError) 139 | } 140 | util.inherits(TimeoutError, LDAPError) 141 | module.exports.TimeoutError = TimeoutError 142 | Object.defineProperties(TimeoutError.prototype, { 143 | name: { 144 | get: function () { return 'TimeoutError' }, 145 | configurable: false 146 | } 147 | }) 148 | -------------------------------------------------------------------------------- /lib/index.js: -------------------------------------------------------------------------------- 1 | // Copyright 2011 Mark Cavage, Inc. All rights reserved. 2 | 3 | const logger = require('./logger') 4 | 5 | const client = require('./client') 6 | const Attribute = require('@ldapjs/attribute') 7 | const Change = require('@ldapjs/change') 8 | const Protocol = require('@ldapjs/protocol') 9 | const Server = require('./server') 10 | 11 | const controls = require('./controls') 12 | const persistentSearch = require('./persistent_search') 13 | const dn = require('@ldapjs/dn') 14 | const errors = require('./errors') 15 | const filters = require('@ldapjs/filter') 16 | const messages = require('./messages') 17 | const url = require('./url') 18 | 19 | const hasOwnProperty = (target, val) => Object.prototype.hasOwnProperty.call(target, val) 20 | 21 | /// --- API 22 | 23 | module.exports = { 24 | Client: client.Client, 25 | createClient: client.createClient, 26 | 27 | Server, 28 | createServer: function (options) { 29 | if (options === undefined) { options = {} } 30 | 31 | if (typeof (options) !== 'object') { throw new TypeError('options (object) required') } 32 | 33 | if (!options.log) { 34 | options.log = logger 35 | } 36 | 37 | return new Server(options) 38 | }, 39 | 40 | Attribute, 41 | Change, 42 | 43 | dn, 44 | DN: dn.DN, 45 | RDN: dn.RDN, 46 | parseDN: dn.DN.fromString, 47 | 48 | persistentSearch, 49 | PersistentSearchCache: persistentSearch.PersistentSearchCache, 50 | 51 | filters, 52 | parseFilter: filters.parseString, 53 | 54 | url, 55 | parseURL: url.parse 56 | } 57 | 58 | /// --- Export all the childrenz 59 | 60 | let k 61 | 62 | for (k in Protocol) { 63 | if (hasOwnProperty(Protocol, k)) { module.exports[k] = Protocol[k] } 64 | } 65 | 66 | for (k in messages) { 67 | if (hasOwnProperty(messages, k)) { module.exports[k] = messages[k] } 68 | } 69 | 70 | for (k in controls) { 71 | if (hasOwnProperty(controls, k)) { module.exports[k] = controls[k] } 72 | } 73 | 74 | for (k in filters) { 75 | if (hasOwnProperty(filters, k)) { 76 | if (k !== 'parse' && k !== 'parseString') { module.exports[k] = filters[k] } 77 | } 78 | } 79 | 80 | for (k in errors) { 81 | if (hasOwnProperty(errors, k)) { 82 | module.exports[k] = errors[k] 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /lib/logger.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const logger = require('abstract-logging') 4 | logger.child = function () { return logger } 5 | 6 | module.exports = logger 7 | -------------------------------------------------------------------------------- /lib/messages/index.js: -------------------------------------------------------------------------------- 1 | // Copyright 2011 Mark Cavage, Inc. All rights reserved. 2 | 3 | const messages = require('@ldapjs/messages') 4 | 5 | const Parser = require('./parser') 6 | 7 | const SearchResponse = require('./search_response') 8 | 9 | /// --- API 10 | 11 | module.exports = { 12 | 13 | LDAPMessage: messages.LdapMessage, 14 | LDAPResult: messages.LdapResult, 15 | Parser, 16 | 17 | AbandonRequest: messages.AbandonRequest, 18 | AbandonResponse: messages.AbandonResponse, 19 | AddRequest: messages.AddRequest, 20 | AddResponse: messages.AddResponse, 21 | BindRequest: messages.BindRequest, 22 | BindResponse: messages.BindResponse, 23 | CompareRequest: messages.CompareRequest, 24 | CompareResponse: messages.CompareResponse, 25 | DeleteRequest: messages.DeleteRequest, 26 | DeleteResponse: messages.DeleteResponse, 27 | ExtendedRequest: messages.ExtensionRequest, 28 | ExtendedResponse: messages.ExtensionResponse, 29 | ModifyRequest: messages.ModifyRequest, 30 | ModifyResponse: messages.ModifyResponse, 31 | ModifyDNRequest: messages.ModifyDnRequest, 32 | ModifyDNResponse: messages.ModifyDnResponse, 33 | SearchRequest: messages.SearchRequest, 34 | SearchEntry: messages.SearchResultEntry, 35 | SearchReference: messages.SearchResultReference, 36 | SearchResponse, 37 | UnbindRequest: messages.UnbindRequest 38 | 39 | } 40 | -------------------------------------------------------------------------------- /lib/messages/parser.js: -------------------------------------------------------------------------------- 1 | // Copyright 2011 Mark Cavage, Inc. All rights reserved. 2 | 3 | const EventEmitter = require('events').EventEmitter 4 | const util = require('util') 5 | 6 | const assert = require('assert-plus') 7 | const asn1 = require('@ldapjs/asn1') 8 | const logger = require('../logger') 9 | 10 | const messages = require('@ldapjs/messages') 11 | const AbandonRequest = messages.AbandonRequest 12 | const AddRequest = messages.AddRequest 13 | const AddResponse = messages.AddResponse 14 | const BindRequest = messages.BindRequest 15 | const BindResponse = messages.BindResponse 16 | const CompareRequest = messages.CompareRequest 17 | const CompareResponse = messages.CompareResponse 18 | const DeleteRequest = messages.DeleteRequest 19 | const DeleteResponse = messages.DeleteResponse 20 | const ExtendedRequest = messages.ExtensionRequest 21 | const ExtendedResponse = messages.ExtensionResponse 22 | const ModifyRequest = messages.ModifyRequest 23 | const ModifyResponse = messages.ModifyResponse 24 | const ModifyDNRequest = messages.ModifyDnRequest 25 | const ModifyDNResponse = messages.ModifyDnResponse 26 | const SearchRequest = messages.SearchRequest 27 | const SearchEntry = messages.SearchResultEntry 28 | const SearchReference = messages.SearchResultReference 29 | const SearchResponse = require('./search_response') 30 | const UnbindRequest = messages.UnbindRequest 31 | const LDAPResult = messages.LdapResult 32 | 33 | const Protocol = require('@ldapjs/protocol') 34 | 35 | /// --- Globals 36 | 37 | const BerReader = asn1.BerReader 38 | 39 | /// --- API 40 | 41 | function Parser (options = {}) { 42 | assert.object(options) 43 | 44 | EventEmitter.call(this) 45 | 46 | this.buffer = null 47 | this.log = options.log || logger 48 | } 49 | util.inherits(Parser, EventEmitter) 50 | 51 | /** 52 | * The LDAP server/client implementations will receive data from a stream and feed 53 | * it into this method. This method will collect that data into an internal 54 | * growing buffer. As that buffer fills with enough data to constitute a valid 55 | * LDAP message, the data will be parsed, emitted as a message object, and 56 | * reset the buffer to account for any next message in the stream. 57 | */ 58 | Parser.prototype.write = function (data) { 59 | if (!data || !Buffer.isBuffer(data)) { throw new TypeError('data (buffer) required') } 60 | 61 | let nextMessage = null 62 | const self = this 63 | 64 | function end () { 65 | if (nextMessage) { return self.write(nextMessage) } 66 | 67 | return true 68 | } 69 | 70 | self.buffer = self.buffer ? Buffer.concat([self.buffer, data]) : data 71 | 72 | let ber = new BerReader(self.buffer) 73 | 74 | let foundSeq = false 75 | try { 76 | foundSeq = ber.readSequence() 77 | } catch (e) { 78 | this.emit('error', e) 79 | } 80 | 81 | if (!foundSeq || ber.remain < ber.length) { 82 | // ENOTENOUGH 83 | return false 84 | } else if (ber.remain > ber.length) { 85 | // ETOOMUCH 86 | 87 | // This is an odd branch. Basically, it is setting `nextMessage` to 88 | // a buffer that represents data part of a message subsequent to the one 89 | // being processed. It then re-creates `ber` as a representation of 90 | // the message being processed and advances its offset to the value 91 | // position of the TLV. 92 | 93 | // Set `nextMessage` to the bytes subsequent to the current message's 94 | // value bytes. That is, slice from the byte immediately following the 95 | // current message's value bytes until the end of the buffer. 96 | nextMessage = self.buffer.slice(ber.offset + ber.length) 97 | 98 | const currOffset = ber.offset 99 | ber = new BerReader(ber.buffer.subarray(0, currOffset + ber.length)) 100 | ber.readSequence() 101 | 102 | assert.equal(ber.remain, ber.length) 103 | } 104 | 105 | // If we're here, ber holds the message, and nextMessage is temporarily 106 | // pointing at the next sequence of data (if it exists) 107 | self.buffer = null 108 | 109 | let message 110 | try { 111 | if (Object.prototype.toString.call(ber) === '[object BerReader]') { 112 | // Parse the BER into a JavaScript object representation. The message 113 | // objects require the full sequence in order to construct the object. 114 | // At this point, we have already read the sequence tag and length, so 115 | // we need to rewind the buffer a bit. The `.sequenceToReader` method 116 | // does this for us. 117 | message = messages.LdapMessage.parse(ber.sequenceToReader()) 118 | } else { 119 | // Bail here if peer isn't speaking protocol at all 120 | message = this.getMessage(ber) 121 | } 122 | 123 | if (!message) { 124 | return end() 125 | } 126 | 127 | // TODO: find a better way to handle logging now that messages and the 128 | // server are decoupled. ~ jsumners 2023-02-17 129 | message.log = this.log 130 | } catch (e) { 131 | this.emit('error', e, message) 132 | return false 133 | } 134 | 135 | this.emit('message', message) 136 | return end() 137 | } 138 | 139 | Parser.prototype.getMessage = function (ber) { 140 | assert.ok(ber) 141 | 142 | const self = this 143 | 144 | const messageId = ber.readInt() 145 | const type = ber.readSequence() 146 | 147 | let Message 148 | switch (type) { 149 | case Protocol.operations.LDAP_REQ_ABANDON: 150 | Message = AbandonRequest 151 | break 152 | 153 | case Protocol.operations.LDAP_REQ_ADD: 154 | Message = AddRequest 155 | break 156 | 157 | case Protocol.operations.LDAP_RES_ADD: 158 | Message = AddResponse 159 | break 160 | 161 | case Protocol.operations.LDAP_REQ_BIND: 162 | Message = BindRequest 163 | break 164 | 165 | case Protocol.operations.LDAP_RES_BIND: 166 | Message = BindResponse 167 | break 168 | 169 | case Protocol.operations.LDAP_REQ_COMPARE: 170 | Message = CompareRequest 171 | break 172 | 173 | case Protocol.operations.LDAP_RES_COMPARE: 174 | Message = CompareResponse 175 | break 176 | 177 | case Protocol.operations.LDAP_REQ_DELETE: 178 | Message = DeleteRequest 179 | break 180 | 181 | case Protocol.operations.LDAP_RES_DELETE: 182 | Message = DeleteResponse 183 | break 184 | 185 | case Protocol.operations.LDAP_REQ_EXTENSION: 186 | Message = ExtendedRequest 187 | break 188 | 189 | case Protocol.operations.LDAP_RES_EXTENSION: 190 | Message = ExtendedResponse 191 | break 192 | 193 | case Protocol.operations.LDAP_REQ_MODIFY: 194 | Message = ModifyRequest 195 | break 196 | 197 | case Protocol.operations.LDAP_RES_MODIFY: 198 | Message = ModifyResponse 199 | break 200 | 201 | case Protocol.operations.LDAP_REQ_MODRDN: 202 | Message = ModifyDNRequest 203 | break 204 | 205 | case Protocol.operations.LDAP_RES_MODRDN: 206 | Message = ModifyDNResponse 207 | break 208 | 209 | case Protocol.operations.LDAP_REQ_SEARCH: 210 | Message = SearchRequest 211 | break 212 | 213 | case Protocol.operations.LDAP_RES_SEARCH_ENTRY: 214 | Message = SearchEntry 215 | break 216 | 217 | case Protocol.operations.LDAP_RES_SEARCH_REF: 218 | Message = SearchReference 219 | break 220 | 221 | case Protocol.operations.LDAP_RES_SEARCH: 222 | Message = SearchResponse 223 | break 224 | 225 | case Protocol.operations.LDAP_REQ_UNBIND: 226 | Message = UnbindRequest 227 | break 228 | 229 | default: 230 | this.emit('error', 231 | new Error('Op 0x' + (type ? type.toString(16) : '??') + 232 | ' not supported'), 233 | new LDAPResult({ 234 | messageId, 235 | protocolOp: type || Protocol.operations.LDAP_RES_EXTENSION 236 | })) 237 | 238 | return false 239 | } 240 | 241 | return new Message({ 242 | messageId, 243 | log: self.log 244 | }) 245 | } 246 | 247 | /// --- Exports 248 | 249 | module.exports = Parser 250 | -------------------------------------------------------------------------------- /lib/messages/search_response.js: -------------------------------------------------------------------------------- 1 | // Copyright 2011 Mark Cavage, Inc. All rights reserved. 2 | 3 | const assert = require('assert-plus') 4 | 5 | const Attribute = require('@ldapjs/attribute') 6 | const { 7 | SearchResultEntry: SearchEntry, 8 | SearchResultReference: SearchReference, 9 | SearchResultDone 10 | } = require('@ldapjs/messages') 11 | 12 | const parseDN = require('@ldapjs/dn').DN.fromString 13 | 14 | /// --- API 15 | 16 | class SearchResponse extends SearchResultDone { 17 | attributes 18 | notAttributes 19 | sentEntries 20 | 21 | constructor (options = {}) { 22 | super(options) 23 | 24 | this.attributes = options.attributes ? options.attributes.slice() : [] 25 | this.notAttributes = [] 26 | this.sentEntries = 0 27 | } 28 | } 29 | 30 | /** 31 | * Allows you to send a SearchEntry back to the client. 32 | * 33 | * @param {Object} entry an instance of SearchEntry. 34 | * @param {Boolean} nofiltering skip filtering notAttributes and '_' attributes. 35 | * Defaults to 'false'. 36 | */ 37 | SearchResponse.prototype.send = function (entry, nofiltering) { 38 | if (!entry || typeof (entry) !== 'object') { throw new TypeError('entry (SearchEntry) required') } 39 | if (nofiltering === undefined) { nofiltering = false } 40 | if (typeof (nofiltering) !== 'boolean') { throw new TypeError('noFiltering must be a boolean') } 41 | 42 | const self = this 43 | 44 | const savedAttrs = {} 45 | let save = null 46 | if (entry instanceof SearchEntry || entry instanceof SearchReference) { 47 | if (!entry.messageId) { entry.messageId = this.messageId } 48 | if (entry.messageId !== this.messageId) { 49 | throw new Error('SearchEntry messageId mismatch') 50 | } 51 | } else { 52 | if (!entry.attributes) { throw new Error('entry.attributes required') } 53 | 54 | const all = (self.attributes.indexOf('*') !== -1) 55 | // Filter attributes in a plain object according to the magic `_` prefix 56 | // and presence in `notAttributes`. 57 | Object.keys(entry.attributes).forEach(function (a) { 58 | const _a = a.toLowerCase() 59 | if (!nofiltering && _a.length && _a[0] === '_') { 60 | savedAttrs[a] = entry.attributes[a] 61 | delete entry.attributes[a] 62 | } else if (!nofiltering && self.notAttributes.indexOf(_a) !== -1) { 63 | savedAttrs[a] = entry.attributes[a] 64 | delete entry.attributes[a] 65 | } else if (all) { 66 | // do nothing 67 | } else if (self.attributes.length && self.attributes.indexOf(_a) === -1) { 68 | savedAttrs[a] = entry.attributes[a] 69 | delete entry.attributes[a] 70 | } 71 | }) 72 | 73 | save = entry 74 | entry = new SearchEntry({ 75 | objectName: typeof (save.dn) === 'string' ? parseDN(save.dn) : save.dn, 76 | messageId: self.messageId, 77 | attributes: Attribute.fromObject(entry.attributes) 78 | }) 79 | } 80 | 81 | try { 82 | this.log.debug('%s: sending: %j', this.connection.ldap.id, entry.pojo) 83 | 84 | this.connection.write(entry.toBer().buffer) 85 | this.sentEntries++ 86 | 87 | // Restore attributes 88 | Object.keys(savedAttrs).forEach(function (k) { 89 | save.attributes[k] = savedAttrs[k] 90 | }) 91 | } catch (e) { 92 | this.log.warn(e, '%s failure to write message %j', 93 | this.connection.ldap.id, this.pojo) 94 | } 95 | } 96 | 97 | SearchResponse.prototype.createSearchEntry = function (object) { 98 | assert.object(object) 99 | 100 | const entry = new SearchEntry({ 101 | messageId: this.messageId, 102 | objectName: object.objectName || object.dn, 103 | attributes: object.attributes ?? [] 104 | }) 105 | return entry 106 | } 107 | 108 | SearchResponse.prototype.createSearchReference = function (uris) { 109 | if (!uris) { throw new TypeError('uris ([string]) required') } 110 | 111 | if (!Array.isArray(uris)) { uris = [uris] } 112 | 113 | const self = this 114 | return new SearchReference({ 115 | messageId: self.messageId, 116 | uri: uris 117 | }) 118 | } 119 | 120 | /// --- Exports 121 | 122 | module.exports = SearchResponse 123 | -------------------------------------------------------------------------------- /lib/persistent_search.js: -------------------------------------------------------------------------------- 1 | /// --- Globals 2 | 3 | // var parseDN = require('./dn').parse 4 | 5 | const EntryChangeNotificationControl = 6 | require('./controls').EntryChangeNotificationControl 7 | 8 | /// --- API 9 | 10 | // Cache used to store connected persistent search clients 11 | function PersistentSearch () { 12 | this.clientList = [] 13 | } 14 | 15 | PersistentSearch.prototype.addClient = function (req, res, callback) { 16 | if (typeof (req) !== 'object') { throw new TypeError('req must be an object') } 17 | if (typeof (res) !== 'object') { throw new TypeError('res must be an object') } 18 | if (callback && typeof (callback) !== 'function') { throw new TypeError('callback must be a function') } 19 | 20 | const log = req.log 21 | 22 | const client = {} 23 | client.req = req 24 | client.res = res 25 | 26 | log.debug('%s storing client', req.logId) 27 | 28 | this.clientList.push(client) 29 | 30 | log.debug('%s stored client', req.logId) 31 | log.debug('%s total number of clients %s', 32 | req.logId, this.clientList.length) 33 | if (callback) { callback(client) } 34 | } 35 | 36 | PersistentSearch.prototype.removeClient = function (req, res, callback) { 37 | if (typeof (req) !== 'object') { throw new TypeError('req must be an object') } 38 | if (typeof (res) !== 'object') { throw new TypeError('res must be an object') } 39 | if (callback && typeof (callback) !== 'function') { throw new TypeError('callback must be a function') } 40 | 41 | const log = req.log 42 | log.debug('%s removing client', req.logId) 43 | const client = {} 44 | client.req = req 45 | client.res = res 46 | 47 | // remove the client if it exists 48 | this.clientList.forEach(function (element, index, array) { 49 | if (element.req === client.req) { 50 | log.debug('%s removing client from list', req.logId) 51 | array.splice(index, 1) 52 | } 53 | }) 54 | 55 | log.debug('%s number of persistent search clients %s', 56 | req.logId, this.clientList.length) 57 | if (callback) { callback(client) } 58 | } 59 | 60 | function getOperationType (requestType) { 61 | switch (requestType) { 62 | case 'AddRequest': 63 | case 'add': 64 | return 1 65 | case 'DeleteRequest': 66 | case 'delete': 67 | return 2 68 | case 'ModifyRequest': 69 | case 'modify': 70 | return 4 71 | case 'ModifyDNRequest': 72 | case 'modrdn': 73 | return 8 74 | default: 75 | throw new TypeError('requestType %s, is an invalid request type', 76 | requestType) 77 | } 78 | } 79 | 80 | function getEntryChangeNotificationControl (req, obj) { 81 | // if we want to return a ECNC 82 | if (req.persistentSearch.value.returnECs) { 83 | const attrs = obj.attributes 84 | const value = {} 85 | value.changeType = getOperationType(attrs.changetype) 86 | // if it's a modDN request, fill in the previous DN 87 | if (value.changeType === 8 && attrs.previousDN) { 88 | value.previousDN = attrs.previousDN 89 | } 90 | 91 | value.changeNumber = attrs.changenumber 92 | return new EntryChangeNotificationControl({ value }) 93 | } else { 94 | return false 95 | } 96 | } 97 | 98 | function checkChangeType (req, requestType) { 99 | return (req.persistentSearch.value.changeTypes & 100 | getOperationType(requestType)) 101 | } 102 | 103 | /// --- Exports 104 | 105 | module.exports = { 106 | PersistentSearchCache: PersistentSearch, 107 | checkChangeType, 108 | getEntryChangeNotificationControl 109 | } 110 | -------------------------------------------------------------------------------- /lib/url.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const querystring = require('querystring') 4 | const url = require('url') 5 | const { DN } = require('@ldapjs/dn') 6 | const filter = require('@ldapjs/filter') 7 | 8 | module.exports = { 9 | 10 | parse: function (urlStr, parseDN) { 11 | let parsedURL 12 | try { 13 | parsedURL = new url.URL(urlStr) 14 | } catch (error) { 15 | throw new TypeError(urlStr + ' is an invalid LDAP url (scope)') 16 | } 17 | 18 | if (!parsedURL.protocol || !(parsedURL.protocol === 'ldap:' || parsedURL.protocol === 'ldaps:')) { throw new TypeError(urlStr + ' is an invalid LDAP url (protocol)') } 19 | 20 | const u = { 21 | protocol: parsedURL.protocol, 22 | hostname: parsedURL.hostname, 23 | port: parsedURL.port, 24 | pathname: parsedURL.pathname, 25 | search: parsedURL.search, 26 | href: parsedURL.href 27 | } 28 | 29 | u.secure = (u.protocol === 'ldaps:') 30 | 31 | if (!u.hostname) { u.hostname = 'localhost' } 32 | 33 | if (!u.port) { 34 | u.port = (u.secure ? 636 : 389) 35 | } else { 36 | u.port = parseInt(u.port, 10) 37 | } 38 | 39 | if (u.pathname) { 40 | u.pathname = querystring.unescape(u.pathname.substr(1)) 41 | u.DN = parseDN ? DN.fromString(u.pathname) : u.pathname 42 | } 43 | 44 | if (u.search) { 45 | u.attributes = [] 46 | const tmp = u.search.substr(1).split('?') 47 | if (tmp && tmp.length) { 48 | if (tmp[0]) { 49 | tmp[0].split(',').forEach(function (a) { 50 | u.attributes.push(querystring.unescape(a.trim())) 51 | }) 52 | } 53 | } 54 | if (tmp[1]) { 55 | if (tmp[1] !== 'base' && tmp[1] !== 'one' && tmp[1] !== 'sub') { throw new TypeError(urlStr + ' is an invalid LDAP url (scope)') } 56 | u.scope = tmp[1] 57 | } 58 | if (tmp[2]) { 59 | u.filter = querystring.unescape(tmp[2]) 60 | } 61 | if (tmp[3]) { 62 | u.extensions = querystring.unescape(tmp[3]) 63 | } 64 | 65 | if (!u.scope) { u.scope = 'base' } 66 | if (!u.filter) { u.filter = filter.parseString('(objectclass=*)') } else { u.filter = filter.parseString(u.filter) } 67 | } 68 | 69 | return u 70 | } 71 | 72 | } 73 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "originalAuthor": "Mark Cavage ", 3 | "name": "ldapjs", 4 | "homepage": "http://ldapjs.org", 5 | "description": "LDAP client and server APIs", 6 | "version": "3.0.7", 7 | "license": "MIT", 8 | "repository": { 9 | "type": "git", 10 | "url": "git://github.com/ldapjs/node-ldapjs.git" 11 | }, 12 | "main": "lib/index.js", 13 | "dependencies": { 14 | "@ldapjs/asn1": "^2.0.0", 15 | "@ldapjs/attribute": "^1.0.0", 16 | "@ldapjs/change": "^1.0.0", 17 | "@ldapjs/controls": "^2.1.0", 18 | "@ldapjs/dn": "^1.1.0", 19 | "@ldapjs/filter": "^2.1.1", 20 | "@ldapjs/messages": "^1.3.0", 21 | "@ldapjs/protocol": "^1.2.1", 22 | "abstract-logging": "^2.0.1", 23 | "assert-plus": "^1.0.0", 24 | "backoff": "^2.5.0", 25 | "once": "^1.4.0", 26 | "vasync": "^2.2.1", 27 | "verror": "^1.10.1" 28 | }, 29 | "devDependencies": { 30 | "@fastify/pre-commit": "^2.0.2", 31 | "eslint": "^8.44.0", 32 | "eslint-config-standard": "^17.0.0", 33 | "eslint-plugin-import": "^2.27.5", 34 | "eslint-plugin-n": "^16.0.0", 35 | "eslint-plugin-node": "^11.1.0", 36 | "eslint-plugin-promise": "6.1.1", 37 | "front-matter": "^4.0.2", 38 | "get-port": "^5.1.1", 39 | "highlight.js": "^11.7.0", 40 | "marked": "^4.2.12", 41 | "tap": "^16.3.7" 42 | }, 43 | "scripts": { 44 | "test": "tap --no-cov -R terse", 45 | "test:ci": "tap --coverage-report=lcovonly -R terse", 46 | "test:cov": "tap -R terse", 47 | "test:cov:html": "tap --coverage-report=html -R terse", 48 | "test:watch": "tap -n -w --no-coverage-report -R terse", 49 | "test:integration": "tap --no-cov -R terse 'test-integration/**/*.test.js'", 50 | "test:integration:local": "docker-compose up -d --wait && npm run test:integration ; docker-compose down", 51 | "lint": "eslint . --fix", 52 | "lint:ci": "eslint .", 53 | "docs": "node scripts/build-docs.js" 54 | }, 55 | "pre-commit": [ 56 | "lint:ci", 57 | "test" 58 | ] 59 | } 60 | -------------------------------------------------------------------------------- /scripts/build-docs.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs/promises') 2 | const path = require('path') 3 | const { marked } = require('marked') 4 | const fm = require('front-matter') 5 | const { highlight } = require('highlight.js') 6 | 7 | marked.use({ 8 | highlight: (code, lang) => { 9 | if (lang) { 10 | return highlight(code, { language: lang }).value 11 | } 12 | 13 | return code 14 | } 15 | }) 16 | 17 | function tocHTML (toc) { 18 | let html = '
    \n' 19 | for (const li of toc) { 20 | html += '
  • \n' 21 | html += `\n` 22 | if (li.children && li.children.length > 0) { 23 | html += tocHTML(li.children) 24 | } 25 | html += '
  • \n' 26 | } 27 | html += '
\n' 28 | 29 | return html 30 | } 31 | 32 | function markdownTOC (markdown) { 33 | const tokens = marked.lexer(markdown) 34 | const slugger = new marked.Slugger() 35 | const toc = [] 36 | let currentHeading 37 | let ignoreFirst = true 38 | for (const token of tokens) { 39 | if (token.type === 'heading') { 40 | if (token.depth === 1) { 41 | if (ignoreFirst) { 42 | ignoreFirst = false 43 | continue 44 | } 45 | currentHeading = { 46 | text: token.text, 47 | slug: slugger.slug(token.text), 48 | children: [] 49 | } 50 | toc.push(currentHeading) 51 | } else if (token.depth === 2) { 52 | if (!currentHeading) { 53 | continue 54 | } 55 | currentHeading.children.push({ 56 | text: token.text, 57 | slug: slugger.slug(token.text) 58 | }) 59 | } 60 | } 61 | } 62 | 63 | return { 64 | toc: tocHTML(toc), 65 | html: marked.parser(tokens) 66 | } 67 | } 68 | 69 | function createHTML (template, text) { 70 | const { attributes, body } = fm(text) 71 | 72 | const { toc, html } = markdownTOC(body) 73 | attributes.toc_html = toc 74 | attributes.content = html 75 | 76 | for (const prop in attributes) { 77 | template = template.replace(new RegExp(`%\\(${prop}\\)s`, 'ig'), attributes[prop]) 78 | } 79 | 80 | return template 81 | } 82 | 83 | async function copyRecursive (src, dest) { 84 | const stats = await fs.stat(src) 85 | const isDirectory = stats.isDirectory() 86 | if (isDirectory) { 87 | await fs.mkdir(dest) 88 | const files = await fs.readdir(src) 89 | for (const file of files) { 90 | await copyRecursive(path.join(src, file), path.join(dest, file)) 91 | } 92 | } else { 93 | await fs.copyFile(src, dest) 94 | } 95 | } 96 | 97 | async function createDocs () { 98 | const docs = path.resolve(__dirname, '..', 'docs') 99 | const dist = path.resolve(__dirname, '..', 'public') 100 | const branding = path.join(docs, 'branding') 101 | const src = path.join(branding, 'public') 102 | 103 | try { 104 | await fs.rm(dist, { recursive: true }) 105 | } catch (ex) { 106 | if (ex.code !== 'ENOENT') { 107 | throw ex 108 | } 109 | } 110 | await copyRecursive(src, dist) 111 | 112 | const highlightjsStyles = path.resolve(__dirname, '..', 'node_modules', 'highlight.js', 'styles') 113 | await fs.copyFile(path.join(highlightjsStyles, 'default.css'), path.join(dist, 'media', 'css', 'highlight.css')) 114 | 115 | const template = await fs.readFile(path.join(branding, 'template.html'), { encoding: 'utf8' }) 116 | const files = await fs.readdir(docs) 117 | for (const file of files) { 118 | if (!file.endsWith('.md')) { 119 | continue 120 | } 121 | const text = await fs.readFile(path.join(docs, file), { encoding: 'utf8' }) 122 | const html = createHTML(template, text) 123 | 124 | await fs.writeFile(path.join(dist, file.replace(/md$/, 'html')), html) 125 | } 126 | } 127 | 128 | createDocs().catch(ex => { 129 | console.error(ex) 130 | process.exitCode = 1 131 | }) 132 | -------------------------------------------------------------------------------- /test-integration/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | rules: { 3 | 'no-shadow': 'off' 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /test-integration/client/connect.test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const tap = require('tap') 4 | const ldapjs = require('../../lib') 5 | 6 | const SCHEME = process.env.SCHEME || 'ldap' 7 | const HOST = process.env.HOST || '127.0.0.1' 8 | const PORT = process.env.PORT || 389 9 | 10 | const baseURL = `${SCHEME}://${HOST}:${PORT}` 11 | 12 | tap.test('connects to a server', t => { 13 | t.plan(2) 14 | 15 | const client = ldapjs.createClient({ url: baseURL }) 16 | client.bind('cn=Philip J. Fry,ou=people,dc=planetexpress,dc=com', 'fry', (err) => { 17 | t.error(err) 18 | t.pass() 19 | client.unbind() 20 | }) 21 | }) 22 | -------------------------------------------------------------------------------- /test-integration/client/issue-860.test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const tap = require('tap') 4 | const ldapjs = require('../../lib') 5 | const parseDN = ldapjs.parseDN 6 | 7 | const SCHEME = process.env.SCHEME || 'ldap' 8 | const HOST = process.env.HOST || '127.0.0.1' 9 | const PORT = process.env.PORT || 389 10 | const baseURL = `${SCHEME}://${HOST}:${PORT}` 11 | 12 | const client = ldapjs.createClient({ url: baseURL }) 13 | 14 | tap.before(() => { 15 | return new Promise((resolve, reject) => { 16 | client.bind('cn=admin,dc=planetexpress,dc=com', 'GoodNewsEveryone', (err) => { 17 | if (err) { 18 | return reject(err) 19 | } 20 | resolve() 21 | }) 22 | }) 23 | }) 24 | 25 | tap.teardown(() => { 26 | client.unbind() 27 | }) 28 | 29 | tap.test('can search OUs with Japanese characters', t => { 30 | t.plan(2) 31 | 32 | const opts = { 33 | filter: '(&(objectClass=person))', 34 | scope: 'sub', 35 | paged: true, 36 | sizeLimit: 100, 37 | attributes: ['cn', 'employeeID'] 38 | } 39 | 40 | const baseDN = parseDN('ou=テスト,dc=planetexpress,dc=com') 41 | 42 | client.search(baseDN.toString(), opts, (err, res) => { 43 | t.error(err, 'search error') 44 | res.on('searchEntry', (entry) => { 45 | t.match(entry.pojo, { 46 | type: 'SearchResultEntry', 47 | objectName: 'cn=jdoe,ou=\\e3\\83\\86\\e3\\82\\b9\\e3\\83\\88,dc=planetexpress,dc=com', 48 | attributes: [{ 49 | type: 'cn', 50 | values: ['John', 'jdoe'] 51 | }] 52 | }) 53 | }) 54 | res.on('error', (err) => { 55 | t.error(err, 'search entry error') 56 | }) 57 | res.on('end', () => { 58 | t.end() 59 | }) 60 | }) 61 | }) 62 | 63 | tap.test('can search with non-ascii chars in filter', t => { 64 | t.plan(3) 65 | 66 | const opts = { 67 | filter: '(&(sn=Rodríguez))', 68 | scope: 'sub', 69 | attributes: ['dn', 'sn', 'cn'], 70 | type: 'user' 71 | } 72 | 73 | let searchEntryCount = 0 74 | client.search('dc=planetexpress,dc=com', opts, (err, res) => { 75 | t.error(err, 'search error') 76 | res.on('searchEntry', (entry) => { 77 | searchEntryCount += 1 78 | t.match(entry.pojo, { 79 | type: 'SearchResultEntry', 80 | objectName: 'cn=Bender Bending Rodr\\c3\\adguez,ou=people,dc=planetexpress,dc=com', 81 | attributes: [{ 82 | type: 'cn', 83 | values: ['Bender Bending Rodríguez'] 84 | }] 85 | }) 86 | }) 87 | res.on('error', (err) => { 88 | t.error(err, 'search entry error') 89 | }) 90 | res.on('end', () => { 91 | t.equal(searchEntryCount, 1, 'should have found 1 entry') 92 | t.end() 93 | }) 94 | }) 95 | }) 96 | -------------------------------------------------------------------------------- /test-integration/client/issue-883.test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const tap = require('tap') 4 | const ldapjs = require('../../lib') 5 | 6 | const SCHEME = process.env.SCHEME || 'ldap' 7 | const HOST = process.env.HOST || '127.0.0.1' 8 | const PORT = process.env.PORT || 389 9 | const baseURL = `${SCHEME}://${HOST}:${PORT}` 10 | 11 | const client = ldapjs.createClient({ url: baseURL }) 12 | 13 | tap.test('adds entries with Korean characters', t => { 14 | t.plan(4) 15 | 16 | client.bind('cn=admin,dc=planetexpress,dc=com', 'GoodNewsEveryone', (err) => { 17 | t.error(err, 'bind error') 18 | }) 19 | 20 | const nm = '홍길동' 21 | const dn = `cn=${nm},ou=people,dc=planetexpress,dc=com` 22 | const entry = { 23 | objectclass: 'person', 24 | sn: 'korean test' 25 | } 26 | 27 | client.add(dn, entry, err => { 28 | t.error(err, 'add entry error') 29 | 30 | const searchOpts = { 31 | filter: '(sn=korean test)', 32 | scope: 'subtree', 33 | attributes: ['cn', 'sn'], 34 | sizeLimit: 10, 35 | timeLimit: 0 36 | } 37 | client.search('ou=people,dc=planetexpress,dc=com', searchOpts, (err, res) => { 38 | t.error(err, 'search error') 39 | 40 | res.on('searchEntry', (entry) => { 41 | t.equal( 42 | entry.attributes.filter(a => a.type === 'cn').pop().values.pop(), 43 | nm 44 | ) 45 | }) 46 | 47 | res.on('error', (err) => { 48 | t.error(err, 'search entry error') 49 | }) 50 | 51 | res.on('end', () => { 52 | client.unbind(t.end) 53 | }) 54 | }) 55 | }) 56 | }) 57 | -------------------------------------------------------------------------------- /test-integration/client/issue-885.test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const tap = require('tap') 4 | const ldapjs = require('../../lib') 5 | const parseDN = ldapjs.parseDN 6 | 7 | const SCHEME = process.env.SCHEME || 'ldap' 8 | const HOST = process.env.HOST || '127.0.0.1' 9 | const PORT = process.env.PORT || 389 10 | const baseURL = `${SCHEME}://${HOST}:${PORT}` 11 | 12 | const client = ldapjs.createClient({ url: baseURL }) 13 | 14 | const searchOpts = { 15 | filter: '(&(objectClass=person))', 16 | scope: 'sub', 17 | paged: true, 18 | sizeLimit: 0, 19 | attributes: ['cn', 'employeeID'] 20 | } 21 | 22 | const baseDN = parseDN('ou=large_ou,dc=planetexpress,dc=com') 23 | 24 | tap.test('paged search option returns pages', t => { 25 | t.plan(4) 26 | 27 | client.bind('cn=admin,dc=planetexpress,dc=com', 'GoodNewsEveryone', (err) => { 28 | t.error(err, 'bind error') 29 | }) 30 | 31 | client.search(baseDN.toString(), searchOpts, (err, res) => { 32 | t.error(err, 'search error') 33 | 34 | let pages = 0 35 | const results = [] 36 | res.on('searchEntry', (entry) => { 37 | results.push(entry) 38 | }) 39 | 40 | res.on('page', () => { 41 | pages += 1 42 | }) 43 | 44 | res.on('error', (err) => { 45 | t.error(err, 'search entry error') 46 | }) 47 | 48 | res.on('end', () => { 49 | t.equal(results.length, 2000) 50 | t.equal(pages, 20) 51 | 52 | client.unbind(t.end) 53 | }) 54 | }) 55 | }) 56 | -------------------------------------------------------------------------------- /test-integration/client/issue-923.test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const tap = require('tap') 4 | const ldapjs = require('../../lib') 5 | const { DN } = require('@ldapjs/dn') 6 | const Change = require('@ldapjs/change') 7 | 8 | const SCHEME = process.env.SCHEME || 'ldap' 9 | const HOST = process.env.HOST || '127.0.0.1' 10 | const PORT = process.env.PORT || 389 11 | const baseURL = `${SCHEME}://${HOST}:${PORT}` 12 | 13 | const client = ldapjs.createClient({ url: baseURL }) 14 | 15 | tap.teardown(() => { 16 | client.unbind() 17 | }) 18 | 19 | tap.test('modifies entry specified by dn string', t => { 20 | t.plan(4) 21 | 22 | client.bind('cn=admin,dc=planetexpress,dc=com', 'GoodNewsEveryone', (err) => { 23 | t.error(err, 'bind error') 24 | }) 25 | 26 | const dn = 'cn=large10,ou=large_ou,dc=planetexpress,dc=com' 27 | const change = new Change({ 28 | operation: 'replace', 29 | modification: { 30 | type: 'givenName', 31 | values: ['test'] 32 | } 33 | }) 34 | 35 | client.modify(dn, change, (err) => { 36 | t.error(err, 'modify error') 37 | validateChange({ t, expected: 'test', client }) 38 | }) 39 | }) 40 | 41 | tap.test('modifies entry specified by dn object', t => { 42 | t.plan(4) 43 | 44 | client.bind('cn=admin,dc=planetexpress,dc=com', 'GoodNewsEveryone', (err) => { 45 | t.error(err, 'bind error') 46 | }) 47 | 48 | const dn = DN.fromString('cn=large10,ou=large_ou,dc=planetexpress,dc=com') 49 | const change = new Change({ 50 | operation: 'replace', 51 | modification: { 52 | type: 'givenName', 53 | values: ['test2'] 54 | } 55 | }) 56 | 57 | client.modify(dn, change, (err) => { 58 | t.error(err, 'modify error') 59 | validateChange({ t, expected: 'test2', client }) 60 | }) 61 | }) 62 | 63 | function validateChange ({ t, expected, client }) { 64 | const searchBase = 'ou=large_ou,dc=planetexpress,dc=com' 65 | const searchOpts = { 66 | filter: '(cn=large10)', 67 | scope: 'subtree', 68 | attributes: ['givenName'], 69 | sizeLimit: 10, 70 | timeLimit: 0 71 | } 72 | 73 | client.search(searchBase, searchOpts, (err, res) => { 74 | t.error(err, 'search error') 75 | 76 | res.on('searchEntry', entry => { 77 | t.equal( 78 | entry.attributes.filter(a => a.type === 'givenName').pop().values.pop(), 79 | expected 80 | ) 81 | }) 82 | 83 | res.on('error', err => { 84 | t.error(err, 'search entry error') 85 | }) 86 | 87 | res.on('end', () => { 88 | t.end() 89 | }) 90 | }) 91 | } 92 | -------------------------------------------------------------------------------- /test-integration/client/issue-940.test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const tap = require('tap') 4 | const ldapjs = require('../../lib') 5 | const Change = require('@ldapjs/change') 6 | 7 | const SCHEME = process.env.SCHEME || 'ldap' 8 | const HOST = process.env.HOST || '127.0.0.1' 9 | const PORT = process.env.PORT || 389 10 | const baseURL = `${SCHEME}://${HOST}:${PORT}` 11 | 12 | const client = ldapjs.createClient({ url: baseURL }) 13 | 14 | tap.before(() => { 15 | return new Promise((resolve, reject) => { 16 | client.bind('cn=admin,dc=planetexpress,dc=com', 'GoodNewsEveryone', (err) => { 17 | if (err) { 18 | return reject(err) 19 | } 20 | resolve() 21 | }) 22 | }) 23 | }) 24 | 25 | tap.teardown(() => { 26 | client.unbind() 27 | }) 28 | 29 | tap.test('can modify entries with non-ascii chars in RDN', t => { 30 | t.plan(6) 31 | 32 | const dn = 'cn=Mendonça,ou=people,dc=planetexpress,dc=com' 33 | const entry = { 34 | objectclass: 'person', 35 | sn: 'change me' 36 | } 37 | 38 | client.add(dn, entry, error => { 39 | t.error(error, 'add should not error') 40 | doSearch('change me', doModify) 41 | }) 42 | 43 | function doModify () { 44 | const change = new Change({ 45 | operation: 'replace', 46 | modification: { 47 | type: 'sn', 48 | values: ['changed'] 49 | } 50 | }) 51 | 52 | client.modify(dn, change, (error) => { 53 | t.error(error, 'modify should not error') 54 | doSearch('changed', t.end.bind(t)) 55 | }) 56 | } 57 | 58 | function doSearch (expected, callback) { 59 | const searchOpts = { 60 | filter: '(&(objectclass=person)(cn=Mendonça))', 61 | scope: 'subtree', 62 | attributes: ['sn'] 63 | } 64 | client.search('ou=people,dc=planetexpress,dc=com', searchOpts, (error, res) => { 65 | t.error(error, 'search should not error') 66 | 67 | res.on('searchEntry', entry => { 68 | const found = entry.attributes.filter(a => a.type === 'sn').pop().values.pop() 69 | t.equal(found, expected, `expected '${expected}' and got '${found}'`) 70 | }) 71 | 72 | res.on('error', error => { 73 | t.error(error, 'search result processing should not error') 74 | }) 75 | 76 | res.on('end', () => { 77 | callback() 78 | }) 79 | }) 80 | } 81 | }) 82 | -------------------------------------------------------------------------------- /test-integration/client/issue-946.test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const tap = require('tap') 4 | const ldapjs = require('../../lib') 5 | 6 | const SCHEME = process.env.SCHEME || 'ldap' 7 | const HOST = process.env.HOST || '127.0.0.1' 8 | const PORT = process.env.PORT || 389 9 | const baseURL = `${SCHEME}://${HOST}:${PORT}` 10 | 11 | tap.test('can use password policy response', t => { 12 | const client = ldapjs.createClient({ url: baseURL }) 13 | const targetDN = 'cn=Bender Bending Rodríguez,ou=people,dc=planetexpress,dc=com' 14 | 15 | client.bind('cn=admin,dc=planetexpress,dc=com', 'GoodNewsEveryone', (err, res) => { 16 | t.error(err) 17 | t.ok(res) 18 | t.equal(res.status, 0) 19 | 20 | const newPassword = 'bender2' 21 | changePassword(client, newPassword, () => { 22 | client.unbind() 23 | bindNewClient(newPassword, { error: 2 }, (client) => { 24 | const newPassword = 'bender' 25 | changePassword(client, newPassword, () => { 26 | client.unbind() 27 | bindNewClient(newPassword, { timeBeforeExpiration: 1000 }, (client) => { 28 | client.unbind(t.end) 29 | }) 30 | }) 31 | }) 32 | }) 33 | }) 34 | 35 | function bindNewClient (pwd, expected, callback) { 36 | const client = ldapjs.createClient({ url: baseURL }) 37 | const control = new ldapjs.PasswordPolicyControl() 38 | 39 | client.bind(targetDN, pwd, control, (err, res) => { 40 | t.error(err) 41 | t.ok(res) 42 | t.equal(res.status, 0) 43 | 44 | let error = null 45 | let timeBeforeExpiration = null 46 | let graceAuthNsRemaining = null 47 | 48 | res.controls.forEach(control => { 49 | if (control.type === ldapjs.PasswordPolicyControl.OID) { 50 | error = control.value.error ?? error 51 | timeBeforeExpiration = control.value.timeBeforeExpiration ?? timeBeforeExpiration 52 | graceAuthNsRemaining = control.value.graceAuthNsRemaining ?? graceAuthNsRemaining 53 | } 54 | }) 55 | 56 | if (expected.error !== undefined) { 57 | t.equal(error, expected.error) 58 | } 59 | if (expected.timeBeforeExpiration !== undefined) { 60 | t.equal(timeBeforeExpiration, expected.timeBeforeExpiration) 61 | } 62 | if (expected.graceAuthNsRemaining !== undefined) { 63 | t.equal(graceAuthNsRemaining, expected.graceAuthNsRemaining) 64 | } 65 | 66 | callback(client) 67 | }) 68 | } 69 | 70 | function changePassword (client, newPwd, callback) { 71 | const change = new ldapjs.Change({ 72 | operation: 'replace', 73 | modification: new ldapjs.Attribute({ 74 | type: 'userPassword', 75 | values: newPwd 76 | }) 77 | }) 78 | 79 | client.modify(targetDN, change, (err, res) => { 80 | t.error(err) 81 | t.ok(res) 82 | t.equal(res.status, 0) 83 | 84 | callback() 85 | }) 86 | } 87 | }) 88 | -------------------------------------------------------------------------------- /test-integration/client/issues.test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const tap = require('tap') 4 | const ldapjs = require('../../lib') 5 | 6 | const SCHEME = process.env.SCHEME || 'ldap' 7 | const HOST = process.env.HOST || '127.0.0.1' 8 | const PORT = process.env.PORT || 389 9 | 10 | const baseURL = `${SCHEME}://${HOST}:${PORT}` 11 | 12 | tap.test('modifyDN with long name (issue #480)', t => { 13 | // 2023-08-15: disabling this 265 character string until a bug can be 14 | // fixed in OpenLDAP. See https://github.com/ldapjs/docker-test-openldap/blob/d48bc2fb001b4ed9a152715ced4a2cb120439ec4/bootstrap/slapd-init.sh#L19-L31. 15 | // const longStr = 'a292979f2c86d513d48bbb9786b564b3c5228146e5ba46f404724e322544a7304a2b1049168803a5485e2d57a544c6a0d860af91330acb77e5907a9e601ad1227e80e0dc50abe963b47a004f2c90f570450d0e920d15436fdc771e3bdac0487a9735473ed3a79361d1778d7e53a7fb0e5f01f97a75ef05837d1d5496fc86968ff47fcb64' 16 | 17 | // 2023-08-15: this 140 character string satisfies the original issue 18 | // (https://github.com/ldapjs/node-ldapjs/issues/480) and avoids a bug 19 | // in OpenLDAP 2.5. 20 | const longStr = '292979f2c86d513d48bbb9786b564b3c5228146e5ba46f404724e322544a7304a2b1049168803a5485e2d57a544c6a0d860af91330acb77e5907a9e601ad1227e80e0dc50ab' 21 | const targetDN = 'cn=Turanga Leela,ou=people,dc=planetexpress,dc=com' 22 | const client = ldapjs.createClient({ url: baseURL }) 23 | client.bind('cn=admin,dc=planetexpress,dc=com', 'GoodNewsEveryone', bindHandler) 24 | 25 | function bindHandler (err) { 26 | t.error(err) 27 | client.modifyDN( 28 | targetDN, 29 | `cn=${longStr},ou=people,dc=planetexpress,dc=com`, 30 | modifyHandler 31 | ) 32 | } 33 | 34 | function modifyHandler (err, res) { 35 | t.error(err) 36 | t.ok(res) 37 | t.equal(res.status, 0) 38 | 39 | client.modifyDN( 40 | `cn=${longStr},ou=people,dc=planetexpress,dc=com`, 41 | targetDN, 42 | (err) => { 43 | t.error(err) 44 | client.unbind(t.end) 45 | } 46 | ) 47 | } 48 | }) 49 | 50 | tap.test('whois works correctly (issue #370)', t => { 51 | const client = ldapjs.createClient({ url: baseURL }) 52 | client.bind('cn=Philip J. Fry,ou=people,dc=planetexpress,dc=com', 'fry', (err) => { 53 | t.error(err) 54 | 55 | client.exop('1.3.6.1.4.1.4203.1.11.3', (err, value, res) => { 56 | t.error(err) 57 | t.ok(value) 58 | t.equal(value, 'dn:cn=Philip J. Fry,ou=people,dc=planetexpress,dc=com') 59 | t.ok(res) 60 | t.equal(res.status, 0) 61 | 62 | client.unbind(t.end) 63 | }) 64 | }) 65 | }) 66 | 67 | tap.test('can access large groups (issue #582)', t => { 68 | const client = ldapjs.createClient({ url: baseURL }) 69 | client.bind('cn=admin,dc=planetexpress,dc=com ', 'GoodNewsEveryone', (err) => { 70 | t.error(err) 71 | const searchOpts = { 72 | scope: 'sub', 73 | filter: '(&(objectClass=group)(cn=large_group))' 74 | } 75 | client.search('ou=large_ou,dc=planetexpress,dc=com', searchOpts, (err, response) => { 76 | t.error(err) 77 | 78 | const results = [] 79 | response.on('searchEntry', (entry) => { 80 | results.push(entry) 81 | }) 82 | response.on('error', t.error) 83 | response.on('end', (result) => { 84 | t.equal(result.status, 0) 85 | t.equal(results.length === 1, true) 86 | t.ok(results[0].attributes) 87 | 88 | const memberAttr = results[0].attributes.find(a => a.type === 'member') 89 | t.ok(memberAttr) 90 | t.ok(memberAttr.values) 91 | t.type(memberAttr.values, Array) 92 | t.equal(memberAttr.values.length, 2000) 93 | 94 | client.unbind(t.end) 95 | }) 96 | }) 97 | }) 98 | }) 99 | -------------------------------------------------------------------------------- /test/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | parserOptions: { 3 | ecmaVersion: 'latest' 4 | }, 5 | 6 | rules: { 7 | 'no-shadow': 'off' 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /test/controls/control.test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const { test } = require('tap') 4 | const { BerReader, BerWriter } = require('@ldapjs/asn1') 5 | const { Control, getControl } = require('../../lib') 6 | 7 | test('new no args', function (t) { 8 | t.ok(new Control()) 9 | t.end() 10 | }) 11 | 12 | test('new with args', function (t) { 13 | const c = new Control({ 14 | type: '2.16.840.1.113730.3.4.2', 15 | criticality: true 16 | }) 17 | t.ok(c) 18 | t.equal(c.type, '2.16.840.1.113730.3.4.2') 19 | t.ok(c.criticality) 20 | t.end() 21 | }) 22 | 23 | test('parse', function (t) { 24 | const ber = new BerWriter() 25 | ber.startSequence() 26 | ber.writeString('2.16.840.1.113730.3.4.2') 27 | ber.writeBoolean(true) 28 | ber.writeString('foo') 29 | ber.endSequence() 30 | 31 | const c = getControl(new BerReader(ber.buffer)) 32 | 33 | t.ok(c) 34 | t.equal(c.type, '2.16.840.1.113730.3.4.2') 35 | t.ok(c.criticality) 36 | t.equal(c.value.toString('utf8'), 'foo') 37 | t.end() 38 | }) 39 | 40 | test('parse no value', function (t) { 41 | const ber = new BerWriter() 42 | ber.startSequence() 43 | ber.writeString('2.16.840.1.113730.3.4.2') 44 | ber.endSequence() 45 | 46 | const c = getControl(new BerReader(ber.buffer)) 47 | 48 | t.ok(c) 49 | t.equal(c.type, '2.16.840.1.113730.3.4.2') 50 | t.equal(c.criticality, false) 51 | t.notOk(c.value, null) 52 | t.end() 53 | }) 54 | -------------------------------------------------------------------------------- /test/corked_emitter.test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const { test } = require('tap') 4 | const CorkedEmitter = require('../lib/corked_emitter') 5 | 6 | function gatherEventSequence (expectedNumber) { 7 | const gatheredEvents = [] 8 | let callback 9 | const finished = new Promise(function (resolve) { 10 | callback = function (...args) { 11 | gatheredEvents.push(...args) 12 | if (gatheredEvents.length >= expectedNumber) { 13 | // Prevent result mutation after our promise is resolved: 14 | resolve(gatheredEvents.slice()) 15 | } 16 | } 17 | }) 18 | return { 19 | finished, 20 | callback 21 | } 22 | } 23 | 24 | test('normal emit flow', function (t) { 25 | const emitter = new CorkedEmitter() 26 | const expectedSequence = [ 27 | ['searchEntry', { data: 'a' }], 28 | ['searchEntry', { data: 'b' }], 29 | ['end'] 30 | ] 31 | const gatherer = gatherEventSequence(3) 32 | emitter.on('searchEntry', function (...args) { 33 | gatherer.callback(['searchEntry', ...args]) 34 | }) 35 | emitter.on('end', function (...args) { 36 | gatherer.callback(['end', ...args]) 37 | }) 38 | emitter.emit('searchEntry', { data: 'a' }) 39 | emitter.emit('searchEntry', { data: 'b' }) 40 | emitter.emit('end') 41 | gatherer.finished.then(function (gatheredEvents) { 42 | expectedSequence.forEach(function (expectedEvent, i) { 43 | t.equal(JSON.stringify(expectedEvent), JSON.stringify(gatheredEvents[i])) 44 | }) 45 | t.end() 46 | }) 47 | }) 48 | 49 | test('reversed listener registration', function (t) { 50 | const emitter = new CorkedEmitter() 51 | const expectedSequence = [ 52 | ['searchEntry', { data: 'a' }], 53 | ['searchEntry', { data: 'b' }], 54 | ['end'] 55 | ] 56 | const gatherer = gatherEventSequence(3) 57 | // This time, we swap the event listener registrations. 58 | // The order of emits should remain unchanged. 59 | emitter.on('end', function (...args) { 60 | gatherer.callback(['end', ...args]) 61 | }) 62 | emitter.on('searchEntry', function (...args) { 63 | gatherer.callback(['searchEntry', ...args]) 64 | }) 65 | emitter.emit('searchEntry', { data: 'a' }) 66 | emitter.emit('searchEntry', { data: 'b' }) 67 | emitter.emit('end') 68 | gatherer.finished.then(function (gatheredEvents) { 69 | expectedSequence.forEach(function (expectedEvent, i) { 70 | t.equal(JSON.stringify(expectedEvent), JSON.stringify(gatheredEvents[i])) 71 | }) 72 | t.end() 73 | }) 74 | }) 75 | 76 | test('delayed listener registration', function (t) { 77 | const emitter = new CorkedEmitter() 78 | const expectedSequence = [ 79 | ['searchEntry', { data: 'a' }], 80 | ['searchEntry', { data: 'b' }], 81 | ['end'] 82 | ] 83 | const gatherer = gatherEventSequence(3) 84 | emitter.emit('searchEntry', { data: 'a' }) 85 | emitter.emit('searchEntry', { data: 'b' }) 86 | emitter.emit('end') 87 | // The listeners only appear after a brief delay - this simulates 88 | // the situation described in https://github.com/ldapjs/node-ldapjs/issues/602 89 | // and in https://github.com/ifroz/node-ldapjs/commit/5239f6c68827f2c25b4589089c199d15bb882412 90 | setTimeout(function () { 91 | emitter.on('end', function (...args) { 92 | gatherer.callback(['end', ...args]) 93 | }) 94 | emitter.on('searchEntry', function (...args) { 95 | gatherer.callback(['searchEntry', ...args]) 96 | }) 97 | }, 50) 98 | gatherer.finished.then(function (gatheredEvents) { 99 | expectedSequence.forEach(function (expectedEvent, i) { 100 | t.equal(JSON.stringify(expectedEvent), JSON.stringify(gatheredEvents[i])) 101 | }) 102 | t.end() 103 | }) 104 | }) 105 | -------------------------------------------------------------------------------- /test/errors.test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const { test } = require('tap') 4 | const { 5 | LDAPError, 6 | ConnectionError, 7 | AbandonedError, 8 | TimeoutError, 9 | ConstraintViolationError, 10 | LDAP_OTHER 11 | } = require('../lib') 12 | 13 | test('basic error', function (t) { 14 | const msg = 'mymsg' 15 | const err = new LDAPError(msg, null, null) 16 | t.ok(err) 17 | t.equal(err.name, 'LDAPError') 18 | t.equal(err.code, LDAP_OTHER) 19 | t.equal(err.dn, '') 20 | t.equal(err.message, msg) 21 | t.end() 22 | }) 23 | 24 | test('exports ConstraintViolationError', function (t) { 25 | const msg = 'mymsg' 26 | const err = new ConstraintViolationError(msg, null, null) 27 | t.ok(err) 28 | t.equal(err.name, 'ConstraintViolationError') 29 | t.equal(err.code, 19) 30 | t.equal(err.dn, '') 31 | t.equal(err.message, msg) 32 | t.end() 33 | }) 34 | 35 | test('"custom" errors', function (t) { 36 | const errors = [ 37 | { name: 'ConnectionError', Func: ConnectionError }, 38 | { name: 'AbandonedError', Func: AbandonedError }, 39 | { name: 'TimeoutError', Func: TimeoutError } 40 | ] 41 | 42 | errors.forEach(function (entry) { 43 | const msg = entry.name + 'msg' 44 | const err = new entry.Func(msg) 45 | t.ok(err) 46 | t.equal(err.name, entry.name) 47 | t.equal(err.code, LDAP_OTHER) 48 | t.equal(err.dn, '') 49 | t.equal(err.message, msg) 50 | }) 51 | 52 | t.end() 53 | }) 54 | -------------------------------------------------------------------------------- /test/imgs/test.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ldapjs/node-ldapjs/8ffd0bc9c149088a10ec4c1ec6a18450f76ad05d/test/imgs/test.jpg -------------------------------------------------------------------------------- /test/issue-845.test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const tap = require('tap') 4 | const { SearchResultEntry, SearchRequest } = require('@ldapjs/messages') 5 | const ldapjs = require('../') 6 | 7 | const server = ldapjs.createServer() 8 | 9 | const SUFFIX = '' 10 | const directory = { 11 | 'dc=example,dc=com': { 12 | objectclass: 'example', 13 | dc: 'example', 14 | cn: 'example' 15 | } 16 | } 17 | 18 | server.bind(SUFFIX, (req, res, done) => { 19 | res.end() 20 | return done() 21 | }) 22 | 23 | server.search(SUFFIX, (req, res, done) => { 24 | const dn = req.dn.toString().toLowerCase() 25 | 26 | if (Object.hasOwn(directory, dn) === false) { 27 | return done(Error('not in directory')) 28 | } 29 | 30 | switch (req.scope) { 31 | case SearchRequest.SCOPE_BASE: 32 | case SearchRequest.SCOPE_SUBTREE: { 33 | res.send(new SearchResultEntry({ objectName: `dc=${req.scopeName}` })) 34 | break 35 | } 36 | } 37 | 38 | res.end() 39 | done() 40 | }) 41 | 42 | tap.beforeEach(t => { 43 | return new Promise((resolve, reject) => { 44 | server.listen(0, '127.0.0.1', (err) => { 45 | if (err) return reject(err) 46 | t.context.url = server.url 47 | 48 | t.context.client = ldapjs.createClient({ url: [server.url] }) 49 | t.context.searchOpts = { 50 | filter: '(&(objectClass=*))', 51 | scope: 'sub', 52 | attributes: ['dn', 'cn'] 53 | } 54 | 55 | resolve() 56 | }) 57 | }) 58 | }) 59 | 60 | tap.afterEach(t => { 61 | return new Promise((resolve, reject) => { 62 | t.context.client.destroy() 63 | server.close((err) => { 64 | if (err) return reject(err) 65 | resolve() 66 | }) 67 | }) 68 | }) 69 | 70 | tap.test('rejects if search not in directory', t => { 71 | const { client, searchOpts } = t.context 72 | 73 | client.search('dc=nope', searchOpts, (err, res) => { 74 | t.error(err) 75 | res.on('error', err => { 76 | // TODO: plain error messages should not be lost 77 | // This should be fixed in a revamp of the server code. 78 | // ~ jsumners 2023-03-08 79 | t.equal(err.lde_message, 'Operations Error') 80 | t.end() 81 | }) 82 | }) 83 | }) 84 | 85 | tap.test('base scope matches', t => { 86 | const { client, searchOpts } = t.context 87 | searchOpts.scope = 'base' 88 | 89 | client.search('dc=example,dc=com', searchOpts, (err, res) => { 90 | t.error(err) 91 | res.on('error', (err) => { 92 | t.error(err) 93 | t.end() 94 | }) 95 | res.on('searchEntry', entry => { 96 | t.equal(entry.objectName.toString(), 'dc=base') 97 | t.end() 98 | }) 99 | }) 100 | }) 101 | 102 | tap.test('sub scope matches', t => { 103 | const { client, searchOpts } = t.context 104 | 105 | client.search('dc=example,dc=com', searchOpts, (err, res) => { 106 | t.error(err) 107 | res.on('error', (err) => { 108 | t.error(err) 109 | t.end() 110 | }) 111 | res.on('searchEntry', entry => { 112 | t.equal(entry.objectName.toString(), 'dc=subtree') 113 | t.end() 114 | }) 115 | }) 116 | }) 117 | -------------------------------------------------------------------------------- /test/issue-890.test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | // This test is complicated. It must simulate a server sending an unsolicited, 4 | // or a mismatched, message in order to force the client's internal message 5 | // tracker to try and find a corresponding sent message that does not exist. 6 | // In order to do that, we need to set a high test timeout and wait for the 7 | // error message to be logged. 8 | 9 | const tap = require('tap') 10 | const ldapjs = require('../') 11 | const { SearchResultEntry } = require('@ldapjs/messages') 12 | const server = ldapjs.createServer() 13 | const SUFFIX = '' 14 | 15 | tap.timeout = 10000 16 | 17 | server.bind(SUFFIX, (res, done) => { 18 | res.end() 19 | return done() 20 | }) 21 | 22 | server.search(SUFFIX, (req, res, done) => { 23 | const result = new SearchResultEntry({ 24 | objectName: `dc=${req.scopeName}` 25 | }) 26 | 27 | // Respond to the search request with a matched response. 28 | res.send(result) 29 | res.end() 30 | 31 | // After a short delay, send ANOTHER response to the client that will not 32 | // be matched by the client's internal tracker. 33 | setTimeout( 34 | () => { 35 | res.send(result) 36 | res.end() 37 | done() 38 | }, 39 | 100 40 | ) 41 | }) 42 | 43 | tap.beforeEach(t => { 44 | return new Promise((resolve, reject) => { 45 | server.listen(0, '127.0.0.1', (err) => { 46 | if (err) return reject(err) 47 | 48 | t.context.logMessages = [] 49 | t.context.logger = { 50 | child () { return this }, 51 | debug () {}, 52 | error (...args) { 53 | t.context.logMessages.push(args) 54 | }, 55 | trace () {} 56 | } 57 | 58 | t.context.url = server.url 59 | t.context.client = ldapjs.createClient({ 60 | url: [server.url], 61 | timeout: 5, 62 | log: t.context.logger 63 | }) 64 | 65 | resolve() 66 | }) 67 | }) 68 | }) 69 | 70 | tap.afterEach(t => { 71 | return new Promise((resolve, reject) => { 72 | t.context.client.destroy() 73 | server.close((err) => { 74 | if (err) return reject(err) 75 | resolve() 76 | }) 77 | }) 78 | }) 79 | 80 | tap.test('handle null messages', t => { 81 | const { client, logMessages } = t.context 82 | 83 | // There's no way to get an error from the client when it has received an 84 | // unmatched response from the server. So we need to poll our logger instance 85 | // and detect when the corresponding error message has been logged. 86 | const timer = setInterval( 87 | () => { 88 | if (logMessages.length > 0) { 89 | t.equal( 90 | logMessages.some(msg => msg[1] === 'unmatched server message received'), 91 | true 92 | ) 93 | clearInterval(timer) 94 | t.end() 95 | } 96 | }, 97 | 100 98 | ) 99 | 100 | client.search('dc=test', (error) => { 101 | t.error(error) 102 | }) 103 | }) 104 | -------------------------------------------------------------------------------- /test/laundry.test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const tap = require('tap') 4 | const { getSock, uuid } = require('./utils') 5 | const { SearchResultEntry } = require('@ldapjs/messages') 6 | const Attribute = require('@ldapjs/attribute') 7 | const ldap = require('../lib') 8 | 9 | function search (t, options, callback) { 10 | t.context.client.search(t.context.suffix, options, function (err, res) { 11 | t.error(err) 12 | t.ok(res) 13 | let found = false 14 | res.on('searchEntry', function (entry) { 15 | t.ok(entry) 16 | found = true 17 | }) 18 | res.on('end', function () { 19 | t.ok(found) 20 | if (callback) return callback() 21 | return t.end() 22 | }) 23 | }) 24 | } 25 | 26 | tap.beforeEach((t) => { 27 | return new Promise((resolve, reject) => { 28 | const suffix = `dc=${uuid()}` 29 | const server = ldap.createServer() 30 | 31 | t.context.server = server 32 | t.context.socketPath = getSock() 33 | t.context.suffix = suffix 34 | 35 | server.on('error', err => { 36 | server.close(() => reject(err)) 37 | }) 38 | 39 | server.bind('cn=root', function (req, res, next) { 40 | res.end() 41 | return next() 42 | }) 43 | 44 | server.search(suffix, function (req, res) { 45 | const entry = new SearchResultEntry({ 46 | entry: 'cn=foo,' + suffix, 47 | attributes: Attribute.fromObject({ 48 | objectclass: ['person', 'top'], 49 | cn: 'Pogo Stick', 50 | sn: 'Stick', 51 | givenname: 'ogo', 52 | mail: uuid() + '@pogostick.org' 53 | }) 54 | }) 55 | 56 | if (req.filter.matches(entry.attributes)) { 57 | res.send(entry) 58 | } 59 | 60 | res.end() 61 | }) 62 | 63 | server.listen(t.context.socketPath, function () { 64 | t.context.client = ldap.createClient({ 65 | socketPath: t.context.socketPath 66 | }) 67 | 68 | t.context.client.on('error', (err) => { 69 | t.context.server.close(() => reject(err)) 70 | }) 71 | t.context.client.on('connectError', (err) => { 72 | t.context.server.close(() => reject(err)) 73 | }) 74 | t.context.client.on('connect', (socket) => { 75 | t.context.socket = socket 76 | resolve() 77 | }) 78 | }) 79 | }) 80 | }) 81 | 82 | tap.afterEach((t) => { 83 | return new Promise((resolve, reject) => { 84 | if (!t.context.client) return resolve() 85 | t.context.client.unbind(() => { 86 | t.context.server.close((err) => { 87 | if (err) return reject(err) 88 | resolve() 89 | }) 90 | }) 91 | }) 92 | }) 93 | 94 | tap.test('Evolution search filter (GH-3)', function (t) { 95 | // This is what Evolution sends, when searching for a contact 'ogo'. Wow. 96 | const filter = 97 | '(|(cn=ogo*)(givenname=ogo*)(sn=ogo*)(mail=ogo*)(member=ogo*)' + 98 | '(primaryphone=ogo*)(telephonenumber=ogo*)(homephone=ogo*)(mobile=ogo*)' + 99 | '(carphone=ogo*)(facsimiletelephonenumber=ogo*)' + 100 | '(homefacsimiletelephonenumber=ogo*)(otherphone=ogo*)' + 101 | '(otherfacsimiletelephonenumber=ogo*)(internationalisdnnumber=ogo*)' + 102 | '(pager=ogo*)(radio=ogo*)(telex=ogo*)(assistantphone=ogo*)' + 103 | '(companyphone=ogo*)(callbackphone=ogo*)(tty=ogo*)(o=ogo*)(ou=ogo*)' + 104 | '(roomnumber=ogo*)(title=ogo*)(businessrole=ogo*)(managername=ogo*)' + 105 | '(assistantname=ogo*)(postaladdress=ogo*)(l=ogo*)(st=ogo*)' + 106 | '(postofficebox=ogo*)(postalcode=ogo*)(c=ogo*)(homepostaladdress=ogo*)' + 107 | '(mozillahomelocalityname=ogo*)(mozillahomestate=ogo*)' + 108 | '(mozillahomepostalcode=ogo*)(mozillahomecountryname=ogo*)' + 109 | '(otherpostaladdress=ogo*)(jpegphoto=ogo*)(usercertificate=ogo*)' + 110 | '(labeleduri=ogo*)(displayname=ogo*)(spousename=ogo*)(note=ogo*)' + 111 | '(anniversary=ogo*)(birthdate=ogo*)(mailer=ogo*)(fileas=ogo*)' + 112 | '(category=ogo*)(calcaluri=ogo*)(calfburl=ogo*)(icscalendar=ogo*))' 113 | 114 | return search(t, filter) 115 | }) 116 | 117 | tap.test('GH-49 Client errors on bad attributes', function (t) { 118 | const searchOpts = { 119 | filter: 'cn=*ogo*', 120 | scope: 'one', 121 | attributes: 'dn' 122 | } 123 | return search(t, searchOpts) 124 | }) 125 | 126 | tap.test('GH-55 Client emits connect multiple times', function (t) { 127 | const c = ldap.createClient({ 128 | socketPath: t.context.socketPath 129 | }) 130 | 131 | let count = 0 132 | c.on('connect', function (socket) { 133 | t.ok(socket) 134 | count++ 135 | c.bind('cn=root', 'secret', function (err) { 136 | t.error(err) 137 | c.unbind(function () { 138 | t.equal(count, 1) 139 | t.end() 140 | }) 141 | }) 142 | }) 143 | }) 144 | -------------------------------------------------------------------------------- /test/lib/client/message-tracker/ge-window.test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const { test } = require('tap') 4 | const { MAX_MSGID } = require('../../../../lib/client/constants') 5 | const geWindow = require('../../../../lib/client/message-tracker/ge-window') 6 | 7 | test('comp > (ref in upper window) => true', async t => { 8 | const ref = Math.floor(MAX_MSGID / 2) + 10 9 | const comp = ref + 10 10 | const result = geWindow(ref, comp) 11 | t.equal(result, true) 12 | }) 13 | 14 | test('comp < (ref in upper window) => false', async t => { 15 | const ref = Math.floor(MAX_MSGID / 2) + 10 16 | const comp = ref - 5 17 | const result = geWindow(ref, comp) 18 | t.equal(result, false) 19 | }) 20 | 21 | test('comp > (ref in lower window) => true', async t => { 22 | const ref = Math.floor(MAX_MSGID / 2) - 10 23 | const comp = ref + 20 24 | const result = geWindow(ref, comp) 25 | t.equal(result, true) 26 | }) 27 | 28 | test('comp < (ref in lower window) => false', async t => { 29 | const ref = Math.floor(MAX_MSGID / 2) - 10 30 | const comp = ref - 5 31 | const result = geWindow(ref, comp) 32 | t.equal(result, false) 33 | }) 34 | 35 | test('(max === MAX_MSGID) && (comp > ref) => true', async t => { 36 | const ref = MAX_MSGID - Math.floor(MAX_MSGID / 2) 37 | const comp = ref + 1 38 | const result = geWindow(ref, comp) 39 | t.equal(result, true) 40 | }) 41 | 42 | test('(max === MAX_MSGID) && (comp < ref) => false', async t => { 43 | const ref = MAX_MSGID - Math.floor(MAX_MSGID / 2) 44 | const comp = ref - 1 45 | const result = geWindow(ref, comp) 46 | t.equal(result, false) 47 | }) 48 | -------------------------------------------------------------------------------- /test/lib/client/message-tracker/id-generator.test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const { test } = require('tap') 4 | const { MAX_MSGID } = require('../../../../lib/client/constants') 5 | const idGeneratorFactory = require('../../../../lib/client/message-tracker/id-generator') 6 | 7 | test('starts at 0', async t => { 8 | const nextID = idGeneratorFactory() 9 | const currentID = nextID() 10 | t.equal(currentID, 1) 11 | }) 12 | 13 | test('handles wrapping around', async t => { 14 | const nextID = idGeneratorFactory(MAX_MSGID - 2) 15 | 16 | let currentID = nextID() 17 | t.equal(currentID, MAX_MSGID - 1) 18 | 19 | currentID = nextID() 20 | t.equal(currentID, 1) 21 | }) 22 | -------------------------------------------------------------------------------- /test/lib/client/message-tracker/index.test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const tap = require('tap') 4 | const messageTrackerFactory = require('../../../../lib/client/message-tracker/') 5 | 6 | tap.test('options', t => { 7 | t.test('requires an options object', async t => { 8 | try { 9 | messageTrackerFactory() 10 | } catch (error) { 11 | t.match(error, /options object is required/) 12 | } 13 | 14 | try { 15 | messageTrackerFactory([]) 16 | } catch (error) { 17 | t.match(error, /options object is required/) 18 | } 19 | 20 | try { 21 | messageTrackerFactory('') 22 | } catch (error) { 23 | t.match(error, /options object is required/) 24 | } 25 | 26 | try { 27 | messageTrackerFactory(42) 28 | } catch (error) { 29 | t.match(error, /options object is required/) 30 | } 31 | }) 32 | 33 | t.test('requires id to be a string', async t => { 34 | try { 35 | messageTrackerFactory({ id: {} }) 36 | } catch (error) { 37 | t.match(error, /options\.id string is required/) 38 | } 39 | 40 | try { 41 | messageTrackerFactory({ id: [] }) 42 | } catch (error) { 43 | t.match(error, /options\.id string is required/) 44 | } 45 | 46 | try { 47 | messageTrackerFactory({ id: 42 }) 48 | } catch (error) { 49 | t.match(error, /options\.id string is required/) 50 | } 51 | }) 52 | 53 | t.test('requires parser to be an object', async t => { 54 | try { 55 | messageTrackerFactory({ id: 'foo', parser: 'bar' }) 56 | } catch (error) { 57 | t.match(error, /options\.parser object is required/) 58 | } 59 | 60 | try { 61 | messageTrackerFactory({ id: 'foo', parser: 42 }) 62 | } catch (error) { 63 | t.match(error, /options\.parser object is required/) 64 | } 65 | 66 | try { 67 | messageTrackerFactory({ id: 'foo', parser: [] }) 68 | } catch (error) { 69 | t.match(error, /options\.parser object is required/) 70 | } 71 | }) 72 | 73 | t.end() 74 | }) 75 | 76 | tap.test('.pending', t => { 77 | t.test('returns 0 for no messages', async t => { 78 | const tracker = messageTrackerFactory({ id: 'foo', parser: {} }) 79 | t.equal(tracker.pending, 0) 80 | }) 81 | 82 | t.test('returns 1 for 1 message', async t => { 83 | const tracker = messageTrackerFactory({ id: 'foo', parser: {} }) 84 | tracker.track({}, () => {}) 85 | t.equal(tracker.pending, 1) 86 | }) 87 | 88 | t.end() 89 | }) 90 | 91 | tap.test('#abandon', t => { 92 | t.test('returns false if message does not exist', async t => { 93 | const tracker = messageTrackerFactory({ id: 'foo', parser: {} }) 94 | const result = tracker.abandon(1) 95 | t.equal(result, false) 96 | }) 97 | 98 | t.test('returns true if message is abandoned', async t => { 99 | const tracker = messageTrackerFactory({ id: 'foo', parser: {} }) 100 | tracker.track({}, {}) 101 | const result = tracker.abandon(1) 102 | t.equal(result, true) 103 | }) 104 | 105 | t.end() 106 | }) 107 | 108 | tap.test('#fetch', t => { 109 | t.test('returns handler for fetched message', async t => { 110 | const tracker = messageTrackerFactory({ id: 'foo', parser: {} }) 111 | tracker.track({}, handler) 112 | const { callback: fetched } = tracker.fetch(1) 113 | t.equal(fetched, handler) 114 | 115 | function handler () {} 116 | }) 117 | 118 | t.test('returns handler for fetched abandoned message', async t => { 119 | const tracker = messageTrackerFactory({ id: 'foo', parser: {} }) 120 | tracker.track({}, handler) 121 | tracker.track({ abandon: 'message' }, () => {}) 122 | tracker.abandon(1) 123 | const { callback: fetched } = tracker.fetch(1) 124 | t.equal(fetched, handler) 125 | 126 | function handler () {} 127 | }) 128 | 129 | t.test('returns null when message does not exist', async t => { 130 | const tracker = messageTrackerFactory({ id: 'foo', parser: {} }) 131 | const fetched = tracker.fetch(1) 132 | t.equal(fetched, null) 133 | }) 134 | 135 | t.end() 136 | }) 137 | 138 | tap.test('#purge', t => { 139 | t.test('invokes cb for each tracked message', async t => { 140 | t.plan(4) 141 | let count = 0 142 | const tracker = messageTrackerFactory({ id: 'foo', parser: {} }) 143 | tracker.track({}, handler1) 144 | tracker.track({}, handler2) 145 | tracker.purge(cb) 146 | 147 | function cb (msgID, handler) { 148 | if (count === 0) { 149 | t.equal(msgID, 1) 150 | t.equal(handler, handler1) 151 | count += 1 152 | return 153 | } 154 | t.equal(msgID, 2) 155 | t.equal(handler, handler2) 156 | } 157 | 158 | function handler1 () {} 159 | function handler2 () {} 160 | }) 161 | 162 | t.end() 163 | }) 164 | 165 | tap.test('#remove', t => { 166 | t.test('removes from the current track', async t => { 167 | const tracker = messageTrackerFactory({ id: 'foo', parser: {} }) 168 | tracker.track({}, () => {}) 169 | tracker.remove(1) 170 | t.equal(tracker.pending, 0) 171 | }) 172 | 173 | // Not a great test. It exercises the desired code path, but we probably 174 | // should expose some insight into the abandoned track. 175 | t.test('removes from the abandoned track', async t => { 176 | const tracker = messageTrackerFactory({ id: 'foo', parser: {} }) 177 | tracker.track({}, () => {}) 178 | tracker.track({ abandon: 'message' }, () => {}) 179 | tracker.abandon(1) 180 | tracker.remove(1) 181 | t.equal(tracker.pending, 1) 182 | }) 183 | 184 | t.end() 185 | }) 186 | 187 | tap.test('#track', t => { 188 | t.test('add messageId and tracks message', async t => { 189 | const tracker = messageTrackerFactory({ id: 'foo', parser: {} }) 190 | const msg = {} 191 | tracker.track(msg, handler) 192 | 193 | t.same(msg, { messageId: 1 }) 194 | const { callback: cb } = tracker.fetch(1) 195 | t.equal(cb, handler) 196 | 197 | function handler () {} 198 | }) 199 | 200 | t.end() 201 | }) 202 | -------------------------------------------------------------------------------- /test/lib/client/message-tracker/purge-abandoned.test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const { test } = require('tap') 4 | const { MAX_MSGID } = require('../../../../lib/client/constants') 5 | const purgeAbandoned = require('../../../../lib/client/message-tracker/purge-abandoned') 6 | 7 | test('clears queue if only one message present', async t => { 8 | t.plan(3) 9 | const abandoned = new Map() 10 | abandoned.set(1, { age: 2, cb }) 11 | 12 | purgeAbandoned(2, abandoned) 13 | t.equal(abandoned.size, 0) 14 | 15 | function cb (err) { 16 | t.equal(err.name, 'AbandonedError') 17 | t.equal(err.message, 'client request abandoned') 18 | } 19 | }) 20 | 21 | test('clears queue if multiple messages present', async t => { 22 | t.plan(5) 23 | const abandoned = new Map() 24 | abandoned.set(1, { age: 2, cb }) 25 | abandoned.set(2, { age: 3, cb }) 26 | 27 | purgeAbandoned(4, abandoned) 28 | t.equal(abandoned.size, 0) 29 | 30 | function cb (err) { 31 | t.equal(err.name, 'AbandonedError') 32 | t.equal(err.message, 'client request abandoned') 33 | } 34 | }) 35 | 36 | test('message id has wrappred around', async t => { 37 | t.plan(3) 38 | const abandoned = new Map() 39 | abandoned.set(MAX_MSGID - 1, { age: MAX_MSGID, cb }) 40 | 41 | // The "abandon" message was sent with an id of "MAX_MSGID". So the message 42 | // that is triggering the purge was the "first" message in the new sequence 43 | // of message identifiers. 44 | purgeAbandoned(1, abandoned) 45 | t.equal(abandoned.size, 0) 46 | 47 | function cb (err) { 48 | t.equal(err.name, 'AbandonedError') 49 | t.equal(err.message, 'client request abandoned') 50 | } 51 | }) 52 | 53 | test('does not clear if window not met', async t => { 54 | t.plan(1) 55 | const abandoned = new Map() 56 | abandoned.set(1, { age: 2, cb }) 57 | 58 | purgeAbandoned(1, abandoned) 59 | t.equal(abandoned.size, 1) 60 | 61 | function cb () { 62 | t.fail('should not be invoked') 63 | } 64 | }) 65 | -------------------------------------------------------------------------------- /test/lib/client/request-queue/enqueue.test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const { test } = require('tap') 4 | const enqueue = require('../../../../lib/client/request-queue/enqueue') 5 | 6 | test('rejects new requests if size is exceeded', async t => { 7 | const q = { _queue: { size: 5 }, size: 5 } 8 | const result = enqueue.call(q, 'foo', 'bar', {}, {}) 9 | t.notOk(result) 10 | }) 11 | 12 | test('rejects new requests if queue is frozen', async t => { 13 | const q = { _queue: { size: 0 }, size: 5, _frozen: true } 14 | const result = enqueue.call(q, 'foo', 'bar', {}, {}) 15 | t.notOk(result) 16 | }) 17 | 18 | test('adds a request and returns if no timeout', async t => { 19 | const q = { 20 | _queue: { 21 | size: 0, 22 | add (obj) { 23 | t.same(obj, { 24 | message: 'foo', 25 | expect: 'bar', 26 | emitter: 'baz', 27 | cb: 'bif' 28 | }) 29 | } 30 | }, 31 | _frozen: false, 32 | timeout: 0 33 | } 34 | const result = enqueue.call(q, 'foo', 'bar', 'baz', 'bif') 35 | t.ok(result) 36 | }) 37 | 38 | test('adds a request and returns timer not set', async t => { 39 | const q = { 40 | _queue: { 41 | size: 0, 42 | add (obj) { 43 | t.same(obj, { 44 | message: 'foo', 45 | expect: 'bar', 46 | emitter: 'baz', 47 | cb: 'bif' 48 | }) 49 | } 50 | }, 51 | _frozen: false, 52 | timeout: 100, 53 | _timer: null 54 | } 55 | const result = enqueue.call(q, 'foo', 'bar', 'baz', 'bif') 56 | t.ok(result) 57 | }) 58 | 59 | test('adds a request, returns true, and clears queue', t => { 60 | // Must not be an async test due to an internal `setTimeout` 61 | t.plan(4) 62 | const q = { 63 | _queue: { 64 | size: 0, 65 | add (obj) { 66 | t.same(obj, { 67 | message: 'foo', 68 | expect: 'bar', 69 | emitter: 'baz', 70 | cb: 'bif' 71 | }) 72 | } 73 | }, 74 | _frozen: false, 75 | timeout: 5, 76 | _timer: 123, 77 | freeze () { t.pass() }, 78 | purge () { t.pass() } 79 | } 80 | const result = enqueue.call(q, 'foo', 'bar', 'baz', 'bif') 81 | t.ok(result) 82 | }) 83 | -------------------------------------------------------------------------------- /test/lib/client/request-queue/flush.test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const { test } = require('tap') 4 | const flush = require('../../../../lib/client/request-queue/flush') 5 | 6 | test('clears timer', async t => { 7 | t.plan(2) 8 | const q = { 9 | _timer: 123, 10 | _queue: { 11 | values () { 12 | return [] 13 | }, 14 | clear () { 15 | t.pass() 16 | } 17 | } 18 | } 19 | flush.call(q) 20 | t.equal(q._timer, null) 21 | }) 22 | 23 | test('invokes callback with parameters', async t => { 24 | t.plan(6) 25 | const req = { 26 | message: 'foo', 27 | expect: 'bar', 28 | emitter: 'baz', 29 | cb: theCB 30 | } 31 | const q = { 32 | _timer: 123, 33 | _queue: { 34 | values () { 35 | return [req] 36 | }, 37 | clear () { 38 | t.pass() 39 | } 40 | } 41 | } 42 | flush.call(q, (message, expect, emitter, cb) => { 43 | t.equal(message, 'foo') 44 | t.equal(expect, 'bar') 45 | t.equal(emitter, 'baz') 46 | t.equal(cb, theCB) 47 | }) 48 | t.equal(q._timer, null) 49 | 50 | function theCB () {} 51 | }) 52 | -------------------------------------------------------------------------------- /test/lib/client/request-queue/purge.test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const { test } = require('tap') 4 | const purge = require('../../../../lib/client/request-queue/purge') 5 | 6 | test('flushes the queue with timeout errors', async t => { 7 | t.plan(3) 8 | const q = { 9 | flush (func) { 10 | func('a', 'b', 'c', (err) => { 11 | t.ok(err) 12 | t.equal(err.name, 'TimeoutError') 13 | t.equal(err.message, 'request queue timeout') 14 | }) 15 | } 16 | } 17 | purge.call(q) 18 | }) 19 | -------------------------------------------------------------------------------- /test/messages/parser.test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const { test } = require('tap') 4 | const { Parser } = require('../../lib') 5 | 6 | test('wrong protocol error', function (t) { 7 | const p = new Parser() 8 | 9 | p.once('error', function (err) { 10 | t.ok(err) 11 | t.end() 12 | }) 13 | 14 | // Send some bogus data to incur an error 15 | p.write(Buffer.from([16, 1, 4])) 16 | }) 17 | -------------------------------------------------------------------------------- /test/server.test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const net = require('net') 4 | const tap = require('tap') 5 | const vasync = require('vasync') 6 | const vm = require('node:vm') 7 | const { getSock } = require('./utils') 8 | const ldap = require('../lib') 9 | 10 | const SERVER_PORT = process.env.SERVER_PORT || 1389 11 | const SUFFIX = 'dc=test' 12 | 13 | tap.beforeEach(function (t) { 14 | // We do not need a `.afterEach` to clean up the sock files because that 15 | // is done when the server is destroyed. 16 | t.context.sock = getSock() 17 | }) 18 | 19 | tap.test('basic create', function (t) { 20 | const server = ldap.createServer() 21 | t.ok(server) 22 | t.end() 23 | }) 24 | 25 | tap.test('connection count', function (t) { 26 | const server = ldap.createServer() 27 | t.ok(server) 28 | server.listen(0, '127.0.0.1', function () { 29 | t.ok(true, 'server listening on ' + server.url) 30 | 31 | server.getConnections(function (err, count) { 32 | t.error(err) 33 | t.equal(count, 0) 34 | 35 | const client = ldap.createClient({ url: server.url }) 36 | client.on('connect', function () { 37 | t.ok(true, 'client connected') 38 | server.getConnections(function (err, count) { 39 | t.error(err) 40 | t.equal(count, 1) 41 | client.unbind() 42 | server.close(() => t.end()) 43 | }) 44 | }) 45 | }) 46 | }) 47 | }) 48 | 49 | tap.test('properties', function (t) { 50 | const server = ldap.createServer() 51 | t.equal(server.name, 'LDAPServer') 52 | 53 | // TODO: better test 54 | server.maxConnections = 10 55 | t.equal(server.maxConnections, 10) 56 | 57 | t.equal(server.url, null, 'url empty before bind') 58 | // listen on a random port so we have a url 59 | server.listen(0, '127.0.0.1', function () { 60 | t.ok(server.url) 61 | 62 | server.close(() => t.end()) 63 | }) 64 | }) 65 | 66 | tap.test('IPv6 URL is formatted correctly', function (t) { 67 | const server = ldap.createServer() 68 | t.equal(server.url, null, 'url empty before bind') 69 | server.listen(0, '::1', function () { 70 | t.ok(server.url) 71 | t.equal(server.url, 'ldap://[::1]:' + server.port) 72 | 73 | server.close(() => t.end()) 74 | }) 75 | }) 76 | 77 | tap.test('listen on unix/named socket', function (t) { 78 | const server = ldap.createServer() 79 | server.listen(t.context.sock, function () { 80 | t.ok(server.url) 81 | t.equal(server.url.split(':')[0], 'ldapi') 82 | server.close(() => t.end()) 83 | }) 84 | }) 85 | 86 | tap.test('listen on static port', function (t) { 87 | const server = ldap.createServer() 88 | server.listen(SERVER_PORT, '127.0.0.1', function () { 89 | const addr = server.address() 90 | t.equal(addr.port, parseInt(SERVER_PORT, 10)) 91 | t.equal(server.url, `ldap://127.0.0.1:${SERVER_PORT}`) 92 | server.close(() => t.end()) 93 | }) 94 | }) 95 | 96 | tap.test('listen on ephemeral port', function (t) { 97 | const server = ldap.createServer() 98 | server.listen(0, '127.0.0.1', function () { 99 | const addr = server.address() 100 | t.ok(addr.port > 0) 101 | t.ok(addr.port < 65535) 102 | server.close(() => t.end()) 103 | }) 104 | }) 105 | 106 | tap.test('route order', function (t) { 107 | function generateHandler (response) { 108 | const func = function handler (req, res, next) { 109 | res.send({ 110 | dn: response, 111 | attributes: { } 112 | }) 113 | res.end() 114 | return next() 115 | } 116 | return func 117 | } 118 | 119 | const server = ldap.createServer() 120 | const sock = t.context.sock 121 | const dnShort = SUFFIX 122 | const dnMed = 'dc=sub,' + SUFFIX 123 | const dnLong = 'dc=long,dc=sub,' + SUFFIX 124 | 125 | // Mount routes out of order 126 | server.search(dnMed, generateHandler(dnMed)) 127 | server.search(dnShort, generateHandler(dnShort)) 128 | server.search(dnLong, generateHandler(dnLong)) 129 | server.listen(sock, function () { 130 | t.ok(true, 'server listen') 131 | const client = ldap.createClient({ socketPath: sock }) 132 | client.on('connect', () => { 133 | vasync.forEachParallel({ 134 | func: runSearch, 135 | inputs: [dnShort, dnMed, dnLong] 136 | }, function (err) { 137 | t.error(err) 138 | client.unbind() 139 | server.close(() => t.end()) 140 | }) 141 | }) 142 | 143 | function runSearch (value, cb) { 144 | client.search(value, '(objectclass=*)', function (err, res) { 145 | t.error(err) 146 | t.ok(res) 147 | res.on('searchEntry', function (entry) { 148 | t.equal(entry.dn.toString(), value) 149 | }) 150 | res.on('end', function () { 151 | cb() 152 | }) 153 | }) 154 | } 155 | }) 156 | }) 157 | 158 | tap.test('route absent', function (t) { 159 | const server = ldap.createServer() 160 | const DN_ROUTE = 'dc=base' 161 | const DN_MISSING = 'dc=absent' 162 | 163 | server.bind(DN_ROUTE, function (req, res, next) { 164 | res.end() 165 | return next() 166 | }) 167 | 168 | server.listen(t.context.sock, function () { 169 | t.ok(true, 'server startup') 170 | vasync.parallel({ 171 | funcs: [ 172 | function presentBind (cb) { 173 | const clt = ldap.createClient({ socketPath: t.context.sock }) 174 | clt.bind(DN_ROUTE, '', function (err) { 175 | t.notOk(err) 176 | clt.unbind() 177 | cb() 178 | }) 179 | }, 180 | function absentBind (cb) { 181 | const clt = ldap.createClient({ socketPath: t.context.sock }) 182 | clt.bind(DN_MISSING, '', function (err) { 183 | t.ok(err) 184 | t.equal(err.code, ldap.LDAP_NO_SUCH_OBJECT) 185 | clt.unbind() 186 | cb() 187 | }) 188 | } 189 | ] 190 | }, function (err) { 191 | t.notOk(err) 192 | server.close(() => t.end()) 193 | }) 194 | }) 195 | }) 196 | 197 | tap.test('route unbind', function (t) { 198 | const server = ldap.createServer() 199 | 200 | server.unbind(function (req, res, next) { 201 | t.ok(true, 'server unbind successful') 202 | res.end() 203 | return next() 204 | }) 205 | 206 | server.listen(t.context.sock, function () { 207 | t.ok(true, 'server startup') 208 | const client = ldap.createClient({ socketPath: t.context.sock }) 209 | client.bind('', '', function (err) { 210 | t.error(err, 'client bind error') 211 | client.unbind(function (err) { 212 | t.error(err, 'client unbind error') 213 | server.close(() => t.end()) 214 | }) 215 | }) 216 | }) 217 | }) 218 | 219 | tap.test('bind/unbind identity anonymous', function (t) { 220 | const server = ldap.createServer({ 221 | connectionRouter: function (c) { 222 | server.newConnection(c) 223 | server.emit('testconnection', c) 224 | } 225 | }) 226 | 227 | server.unbind(function (req, res, next) { 228 | t.ok(true, 'server unbind successful') 229 | res.end() 230 | return next() 231 | }) 232 | 233 | server.bind('', function (req, res, next) { 234 | t.ok(true, 'server bind successful') 235 | res.end() 236 | return next() 237 | }) 238 | 239 | const anonDN = ldap.parseDN('cn=anonymous') 240 | 241 | server.listen(t.context.sock, function () { 242 | t.ok(true, 'server startup') 243 | 244 | const client = ldap.createClient({ socketPath: t.context.sock }) 245 | server.once('testconnection', (c) => { 246 | t.ok(anonDN.equals(c.ldap.bindDN), 'pre bind dn is correct') 247 | client.bind('', '', function (err) { 248 | t.error(err, 'client anon bind error') 249 | t.ok(anonDN.equals(c.ldap.bindDN), 'anon bind dn is correct') 250 | client.unbind(function (err) { 251 | t.error(err, 'client anon unbind error') 252 | t.ok(anonDN.equals(c.ldap.bindDN), 'anon unbind dn is correct') 253 | server.close(() => t.end()) 254 | }) 255 | }) 256 | }) 257 | }) 258 | }) 259 | 260 | tap.test('does not crash on empty DN values', function (t) { 261 | const server = ldap.createServer({ 262 | connectionRouter: function (c) { 263 | server.newConnection(c) 264 | server.emit('testconnection', c) 265 | } 266 | }) 267 | 268 | server.listen(t.context.sock, function () { 269 | const client = ldap.createClient({ socketPath: t.context.sock }) 270 | server.once('testconnection', () => { 271 | client.bind('', 'pw', function (err) { 272 | t.ok(err, 'blank bind dn throws error') 273 | client.unbind(function () { 274 | server.close(() => t.end()) 275 | }) 276 | }) 277 | }) 278 | }) 279 | }) 280 | 281 | tap.test('bind/unbind identity user', function (t) { 282 | const server = ldap.createServer({ 283 | connectionRouter: function (c) { 284 | server.newConnection(c) 285 | server.emit('testconnection', c) 286 | } 287 | }) 288 | 289 | server.unbind(function (req, res, next) { 290 | t.ok(true, 'server unbind successful') 291 | res.end() 292 | return next() 293 | }) 294 | 295 | server.bind('', function (req, res, next) { 296 | t.ok(true, 'server bind successful') 297 | res.end() 298 | return next() 299 | }) 300 | 301 | const anonDN = ldap.parseDN('cn=anonymous') 302 | const testDN = ldap.parseDN('cn=anotheruser') 303 | 304 | server.listen(t.context.sock, function () { 305 | t.ok(true, 'server startup') 306 | 307 | const client = ldap.createClient({ socketPath: t.context.sock }) 308 | server.once('testconnection', (c) => { 309 | t.ok(anonDN.equals(c.ldap.bindDN), 'pre bind dn is correct') 310 | client.bind(testDN.toString(), 'somesecret', function (err) { 311 | t.error(err, 'user bind error') 312 | t.ok(testDN.equals(c.ldap.bindDN), 'user bind dn is correct') 313 | // check rebinds too 314 | client.bind('', '', function (err) { 315 | t.error(err, 'client anon bind error') 316 | t.ok(anonDN.equals(c.ldap.bindDN), 'anon bind dn is correct') 317 | // user rebind 318 | client.bind(testDN.toString(), 'somesecret', function (err) { 319 | t.error(err, 'user bind error') 320 | t.ok(testDN.equals(c.ldap.bindDN), 'user rebind dn is correct') 321 | client.unbind(function (err) { 322 | t.error(err, 'user unbind error') 323 | t.ok(anonDN.equals(c.ldap.bindDN), 'user unbind dn is correct') 324 | server.close(() => t.end()) 325 | }) 326 | }) 327 | }) 328 | }) 329 | }) 330 | }) 331 | }) 332 | 333 | tap.test('strict routing', function (t) { 334 | const testDN = 'cn=valid' 335 | let clt 336 | let server 337 | const sock = t.context.sock 338 | vasync.pipeline({ 339 | funcs: [ 340 | function setup (_, cb) { 341 | server = ldap.createServer({}) 342 | // invalid DNs would go to default handler 343 | server.search('', function (req, res, next) { 344 | t.ok(req.dn) 345 | t.equal(typeof (req.dn), 'object') 346 | t.equal(req.dn.toString(), testDN) 347 | res.end() 348 | next() 349 | }) 350 | server.listen(sock, function () { 351 | t.ok(true, 'server startup') 352 | clt = ldap.createClient({ 353 | socketPath: sock 354 | }) 355 | cb() 356 | }) 357 | }, 358 | function testGood (_, cb) { 359 | clt.search(testDN, { scope: 'base' }, function (err, res) { 360 | t.error(err) 361 | res.once('error', function (err2) { 362 | t.error(err2) 363 | cb(err2) 364 | }) 365 | res.once('end', function (result) { 366 | t.ok(result, 'accepted invalid dn') 367 | cb() 368 | }) 369 | }) 370 | } 371 | ] 372 | }, function (err) { 373 | t.error(err) 374 | if (clt) { 375 | clt.destroy() 376 | } 377 | server.close(() => t.end()) 378 | }) 379 | }) 380 | 381 | tap.test('close accept a callback', function (t) { 382 | const server = ldap.createServer() 383 | // callback is called when the server is closed 384 | server.listen(0, function (err) { 385 | t.error(err) 386 | server.close(function (err) { 387 | t.error(err) 388 | t.end() 389 | }) 390 | }) 391 | }) 392 | 393 | tap.test('close without error calls callback', function (t) { 394 | const server = ldap.createServer() 395 | // when the server is closed without error, the callback parameter is undefined 396 | server.listen(1389, '127.0.0.1', function (err) { 397 | t.error(err) 398 | server.close(function (err) { 399 | t.error(err) 400 | t.end() 401 | }) 402 | }) 403 | }) 404 | 405 | tap.test('close passes error to callback', function (t) { 406 | const server = ldap.createServer() 407 | // when the server is closed with an error, the error is the first parameter of the callback 408 | server.close(function (err) { 409 | t.ok(err) 410 | t.end() 411 | }) 412 | }) 413 | 414 | tap.test('multithreading support via external server', function (t) { 415 | const serverOptions = { } 416 | const server = ldap.createServer(serverOptions) 417 | const fauxServer = net.createServer(serverOptions, (connection) => { 418 | server.newConnection(connection) 419 | }) 420 | fauxServer.log = serverOptions.log 421 | fauxServer.ldap = { 422 | config: serverOptions 423 | } 424 | t.ok(server) 425 | fauxServer.listen(5555, '127.0.0.1', function () { 426 | t.ok(true, 'server listening on ' + server.url) 427 | 428 | t.ok(fauxServer) 429 | const client = ldap.createClient({ url: 'ldap://127.0.0.1:5555' }) 430 | client.on('connect', function () { 431 | t.ok(client) 432 | client.unbind() 433 | fauxServer.close(() => t.end()) 434 | }) 435 | }) 436 | }) 437 | 438 | tap.test('multithreading support via hook', function (t) { 439 | const serverOptions = { 440 | connectionRouter: (connection) => { 441 | server.newConnection(connection) 442 | } 443 | } 444 | const server = ldap.createServer(serverOptions) 445 | const fauxServer = ldap.createServer(serverOptions) 446 | t.ok(server) 447 | fauxServer.listen(0, '127.0.0.1', function () { 448 | t.ok(true, 'server listening on ' + server.url) 449 | 450 | t.ok(fauxServer) 451 | const client = ldap.createClient({ url: fauxServer.url }) 452 | client.on('connect', function () { 453 | t.ok(client) 454 | client.unbind() 455 | fauxServer.close(() => t.end()) 456 | }) 457 | }) 458 | }) 459 | 460 | tap.test('cross-realm type checks', function (t) { 461 | const server = ldap.createServer() 462 | const ctx = vm.createContext({}) 463 | vm.runInContext( 464 | 'globalThis.search=function(){};\n' + 465 | 'globalThis.searches=[function(){}];' 466 | , ctx) 467 | server.search('', ctx.search) 468 | server.search('', ctx.searches) 469 | t.ok(server) 470 | t.end() 471 | }) 472 | -------------------------------------------------------------------------------- /test/url.test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const { test } = require('tap') 4 | const { parseURL } = require('../lib') 5 | 6 | test('parse empty', function (t) { 7 | const u = parseURL('ldap:///') 8 | t.equal(u.hostname, 'localhost') 9 | t.equal(u.port, 389) 10 | t.ok(!u.DN) 11 | t.ok(!u.attributes) 12 | t.equal(u.secure, false) 13 | t.end() 14 | }) 15 | 16 | test('parse hostname', function (t) { 17 | const u = parseURL('ldap://example.com/') 18 | t.equal(u.hostname, 'example.com') 19 | t.equal(u.port, 389) 20 | t.ok(!u.DN) 21 | t.ok(!u.attributes) 22 | t.equal(u.secure, false) 23 | t.end() 24 | }) 25 | 26 | test('parse host and port', function (t) { 27 | const u = parseURL('ldap://example.com:1389/') 28 | t.equal(u.hostname, 'example.com') 29 | t.equal(u.port, 1389) 30 | t.ok(!u.DN) 31 | t.ok(!u.attributes) 32 | t.equal(u.secure, false) 33 | t.end() 34 | }) 35 | 36 | test('parse full', function (t) { 37 | const u = parseURL('ldaps://ldap.example.com:1389/dc=example%20,dc=com' + 38 | '?cn,sn?sub?(cn=Babs%20Jensen)') 39 | 40 | t.equal(u.secure, true) 41 | t.equal(u.hostname, 'ldap.example.com') 42 | t.equal(u.port, 1389) 43 | t.equal(u.DN, 'dc=example ,dc=com') 44 | t.ok(u.attributes) 45 | t.equal(u.attributes.length, 2) 46 | t.equal(u.attributes[0], 'cn') 47 | t.equal(u.attributes[1], 'sn') 48 | t.equal(u.scope, 'sub') 49 | t.equal(u.filter.toString(), '(cn=Babs Jensen)') 50 | 51 | t.end() 52 | }) 53 | 54 | test('supports href', function (t) { 55 | const u = parseURL('ldaps://ldap.example.com:1389/dc=example%20,dc=com?cn,sn?sub?(cn=Babs%20Jensen)') 56 | t.equal(u.href, 'ldaps://ldap.example.com:1389/dc=example%20,dc=com?cn,sn?sub?(cn=Babs%20Jensen)') 57 | t.end() 58 | }) 59 | -------------------------------------------------------------------------------- /test/utils.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const os = require('os') 4 | const path = require('path') 5 | const crypto = require('crypto') 6 | 7 | function uuid () { 8 | return crypto.randomBytes(16).toString('hex') 9 | } 10 | 11 | function getSock () { 12 | if (process.platform === 'win32') { 13 | return '\\\\.\\pipe\\' + uuid() 14 | } else { 15 | return path.join(os.tmpdir(), uuid()) 16 | } 17 | } 18 | 19 | module.exports = { 20 | getSock, 21 | uuid 22 | } 23 | --------------------------------------------------------------------------------