First commit

This commit is contained in:
2025-10-08 11:12:59 -04:00
commit b0605a28a9
820 changed files with 100317 additions and 0 deletions

9
node_modules/@ldapjs/messages/.eslintrc generated vendored Normal file
View File

@ -0,0 +1,9 @@
{
"parserOptions": {
"ecmaVersion": "latest"
},
"extends": [
"standard"
]
}

View File

@ -0,0 +1,10 @@
name: "CI"
on:
pull_request:
push:
branches:
- master
jobs:
call-core-ci:
uses: ldapjs/.github/.github/workflows/node-ci.yml@main

5
node_modules/@ldapjs/messages/.taprc.yaml generated vendored Normal file
View File

@ -0,0 +1,5 @@
reporter: terse
coverage-map: coverage-map.js
files:
- 'lib/**/*.test.js'

21
node_modules/@ldapjs/messages/LICENSE generated vendored Normal file
View File

@ -0,0 +1,21 @@
Copyright (c) 2014 Patrick Mooney. All rights reserved.
Copyright (c) 2014 Mark Cavage, Inc. All rights reserved.
Copyright (c) 2022 The LDAPJS Collaborators.
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE

7
node_modules/@ldapjs/messages/README.md generated vendored Normal file
View File

@ -0,0 +1,7 @@
# @ldapjs/messages
Provides methods and objects to represent LDAP messages.
## License
MIT.

3
node_modules/@ldapjs/messages/coverage-map.js generated vendored Normal file
View File

@ -0,0 +1,3 @@
'use strict'
module.exports = testFile => testFile.replace(/\.test\.js$/, '.js')

41
node_modules/@ldapjs/messages/index.js generated vendored Normal file
View File

@ -0,0 +1,41 @@
'use strict'
module.exports = {
// Base objects.
LdapMessage: require('./lib/ldap-message'),
LdapResult: require('./lib/ldap-result'),
// Request objects.
AbandonRequest: require('./lib/messages/abandon-request'),
AddRequest: require('./lib/messages/add-request'),
BindRequest: require('./lib/messages/bind-request'),
CompareRequest: require('./lib/messages/compare-request'),
DeleteRequest: require('./lib/messages/delete-request'),
ExtensionRequest: require('./lib/messages/extension-request'),
ModifyRequest: require('./lib/messages/modify-request'),
ModifyDnRequest: require('./lib/messages/modifydn-request'),
SearchRequest: require('./lib/messages/search-request'),
UnbindRequest: require('./lib/messages/unbind-request'),
// Response objects.
AbandonResponse: require('./lib/messages/abandon-response'),
AddResponse: require('./lib/messages/add-response'),
BindResponse: require('./lib/messages/bind-response'),
CompareResponse: require('./lib/messages/compare-response'),
DeleteResponse: require('./lib/messages/delete-response'),
ExtensionResponse: require('./lib/messages/extension-response'),
ModifyResponse: require('./lib/messages/modify-response'),
ModifyDnResponse: require('./lib/messages/modifydn-response'),
// Search request messages.
SearchResultEntry: require('./lib/messages/search-result-entry'),
SearchResultReference: require('./lib/messages/search-result-reference'),
SearchResultDone: require('./lib/messages/search-result-done'),
// Specific extension response implementations.
PasswordModifyResponse: require('./lib/messages/extension-responses/password-modify'),
WhoAmIResponse: require('./lib/messages/extension-responses/who-am-i'),
// Miscellaneous objects.
IntermediateResponse: require('./lib/messages/intermediate-response')
}

View File

@ -0,0 +1,149 @@
'use strict'
module.exports = [
0x30, 0x82, 0x04, 0xe4, // sequence, 1_252 bytes
0x02, 0x01, 0x01, // message id (1)
0x63, 0x82, 0x04, 0xdd, // protocol op (0x63), 1_245 bytes
0x04, 0x23, // string (baseObject), 35 bytes
// "dc=dc3d09f8c56386aff779f0a9a7bc9514"
0x64, 0x63, 0x3d, 0x64, 0x63, 0x33, 0x64, 0x30, 0x39, 0x66,
0x38, 0x63, 0x35, 0x36, 0x33, 0x38, 0x36, 0x61, 0x66, 0x66,
0x37, 0x37, 0x39, 0x66, 0x30, 0x61, 0x39, 0x61, 0x37, 0x62,
0x63, 0x39, 0x35, 0x31, 0x34,
0x0a, 0x01, // scope, 1 byte
0x00, // "base"
0x0a, 0x01, // derefAliases, 1 byte
0x00, // "never"
0x02, 0x01, 0x00, // sizeLimit = 0
0x02, 0x01, 0x0a, // timeLimit = 10
0x01, 0x01, 0x00, // typesOnly = false
// See the `evolution-filter.js` fixture in `@ldapjs/filter` for a
// complete breakdown of the filter bytes.
// https://github.com/ldapjs/filter/blob/8a5892c/lib/ber-parsing/_fixtures/evolution-filter.js
0xa1, 0x82, 0x04, 0xa3, // filter, 1_187 bytes
0xa4, 0x0b, 0x04,
0x02, 0x63, 0x6e, 0x30, 0x05, 0x80, 0x03, 0x6f, 0x67, 0x6f,
0xa4, 0x12, 0x04, 0x09, 0x67, 0x69, 0x76, 0x65, 0x6e, 0x6e,
0x61, 0x6d, 0x65, 0x30, 0x05, 0x80, 0x03, 0x6f, 0x67, 0x6f,
0xa4, 0x0b, 0x04, 0x02, 0x73, 0x6e, 0x30, 0x05, 0x80, 0x03,
0x6f, 0x67, 0x6f, 0xa4, 0x0d, 0x04, 0x04, 0x6d, 0x61, 0x69,
0x6c, 0x30, 0x05, 0x80, 0x03, 0x6f, 0x67, 0x6f, 0xa4, 0x0f,
0x04, 0x06, 0x6d, 0x65, 0x6d, 0x62, 0x65, 0x72, 0x30, 0x05,
0x80, 0x03, 0x6f, 0x67, 0x6f, 0xa4, 0x15, 0x04, 0x0c, 0x70,
0x72, 0x69, 0x6d, 0x61, 0x72, 0x79, 0x70, 0x68, 0x6f, 0x6e,
0x65, 0x30, 0x05, 0x80, 0x03, 0x6f, 0x67, 0x6f, 0xa4, 0x18,
0x04, 0x0f, 0x74, 0x65, 0x6c, 0x65, 0x70, 0x68, 0x6f, 0x6e,
0x65, 0x6e, 0x75, 0x6d, 0x62, 0x65, 0x72, 0x30, 0x05, 0x80,
0x03, 0x6f, 0x67, 0x6f, 0xa4, 0x12, 0x04, 0x09, 0x68, 0x6f,
0x6d, 0x65, 0x70, 0x68, 0x6f, 0x6e, 0x65, 0x30, 0x05, 0x80,
0x03, 0x6f, 0x67, 0x6f, 0xa4, 0x0f, 0x04, 0x06, 0x6d, 0x6f,
0x62, 0x69, 0x6c, 0x65, 0x30, 0x05, 0x80, 0x03, 0x6f, 0x67,
0x6f, 0xa4, 0x11, 0x04, 0x08, 0x63, 0x61, 0x72, 0x70, 0x68,
0x6f, 0x6e, 0x65, 0x30, 0x05, 0x80, 0x03, 0x6f, 0x67, 0x6f,
0xa4, 0x21, 0x04, 0x18, 0x66, 0x61, 0x63, 0x73, 0x69, 0x6d,
0x69, 0x6c, 0x65, 0x74, 0x65, 0x6c, 0x65, 0x70, 0x68, 0x6f,
0x6e, 0x65, 0x6e, 0x75, 0x6d, 0x62, 0x65, 0x72, 0x30, 0x05,
0x80, 0x03, 0x6f, 0x67, 0x6f, 0xa4, 0x25, 0x04, 0x1c, 0x68,
0x6f, 0x6d, 0x65, 0x66, 0x61, 0x63, 0x73, 0x69, 0x6d, 0x69,
0x6c, 0x65, 0x74, 0x65, 0x6c, 0x65, 0x70, 0x68, 0x6f, 0x6e,
0x65, 0x6e, 0x75, 0x6d, 0x62, 0x65, 0x72, 0x30, 0x05, 0x80,
0x03, 0x6f, 0x67, 0x6f, 0xa4, 0x13, 0x04, 0x0a, 0x6f, 0x74,
0x68, 0x65, 0x72, 0x70, 0x68, 0x6f, 0x6e, 0x65, 0x30, 0x05,
0x80, 0x03, 0x6f, 0x67, 0x6f, 0xa4, 0x26, 0x04, 0x1d, 0x6f,
0x74, 0x68, 0x65, 0x72, 0x66, 0x61, 0x63, 0x73, 0x69, 0x6d,
0x69, 0x6c, 0x65, 0x74, 0x65, 0x6c, 0x65, 0x70, 0x68, 0x6f,
0x6e, 0x65, 0x6e, 0x75, 0x6d, 0x62, 0x65, 0x72, 0x30, 0x05,
0x80, 0x03, 0x6f, 0x67, 0x6f, 0xa4, 0x20, 0x04, 0x17, 0x69,
0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x74, 0x69, 0x6f, 0x6e,
0x61, 0x6c, 0x69, 0x73, 0x64, 0x6e, 0x6e, 0x75, 0x6d, 0x62,
0x65, 0x72, 0x30, 0x05, 0x80, 0x03, 0x6f, 0x67, 0x6f, 0xa4,
0x0e, 0x04, 0x05, 0x70, 0x61, 0x67, 0x65, 0x72, 0x30, 0x05,
0x80, 0x03, 0x6f, 0x67, 0x6f, 0xa4, 0x0e, 0x04, 0x05, 0x72,
0x61, 0x64, 0x69, 0x6f, 0x30, 0x05, 0x80, 0x03, 0x6f, 0x67,
0x6f, 0xa4, 0x0e, 0x04, 0x05, 0x74, 0x65, 0x6c, 0x65, 0x78,
0x30, 0x05, 0x80, 0x03, 0x6f, 0x67, 0x6f, 0xa4, 0x17, 0x04,
0x0e, 0x61, 0x73, 0x73, 0x69, 0x73, 0x74, 0x61, 0x6e, 0x74,
0x70, 0x68, 0x6f, 0x6e, 0x65, 0x30, 0x05, 0x80, 0x03, 0x6f,
0x67, 0x6f, 0xa4, 0x15, 0x04, 0x0c, 0x63, 0x6f, 0x6d, 0x70,
0x61, 0x6e, 0x79, 0x70, 0x68, 0x6f, 0x6e, 0x65, 0x30, 0x05,
0x80, 0x03, 0x6f, 0x67, 0x6f, 0xa4, 0x16, 0x04, 0x0d, 0x63,
0x61, 0x6c, 0x6c, 0x62, 0x61, 0x63, 0x6b, 0x70, 0x68, 0x6f,
0x6e, 0x65, 0x30, 0x05, 0x80, 0x03, 0x6f, 0x67, 0x6f, 0xa4,
0x0c, 0x04, 0x03, 0x74, 0x74, 0x79, 0x30, 0x05, 0x80, 0x03,
0x6f, 0x67, 0x6f, 0xa4, 0x0a, 0x04, 0x01, 0x6f, 0x30, 0x05,
0x80, 0x03, 0x6f, 0x67, 0x6f, 0xa4, 0x0b, 0x04, 0x02, 0x6f,
0x75, 0x30, 0x05, 0x80, 0x03, 0x6f, 0x67, 0x6f, 0xa4, 0x13,
0x04, 0x0a, 0x72, 0x6f, 0x6f, 0x6d, 0x6e, 0x75, 0x6d, 0x62,
0x65, 0x72, 0x30, 0x05, 0x80, 0x03, 0x6f, 0x67, 0x6f, 0xa4,
0x0e, 0x04, 0x05, 0x74, 0x69, 0x74, 0x6c, 0x65, 0x30, 0x05,
0x80, 0x03, 0x6f, 0x67, 0x6f, 0xa4, 0x15, 0x04, 0x0c, 0x62,
0x75, 0x73, 0x69, 0x6e, 0x65, 0x73, 0x73, 0x72, 0x6f, 0x6c,
0x65, 0x30, 0x05, 0x80, 0x03, 0x6f, 0x67, 0x6f, 0xa4, 0x14,
0x04, 0x0b, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x72, 0x6e,
0x61, 0x6d, 0x65, 0x30, 0x05, 0x80, 0x03, 0x6f, 0x67, 0x6f,
0xa4, 0x16, 0x04, 0x0d, 0x61, 0x73, 0x73, 0x69, 0x73, 0x74,
0x61, 0x6e, 0x74, 0x6e, 0x61, 0x6d, 0x65, 0x30, 0x05, 0x80,
0x03, 0x6f, 0x67, 0x6f, 0xa4, 0x16, 0x04, 0x0d, 0x70, 0x6f,
0x73, 0x74, 0x61, 0x6c, 0x61, 0x64, 0x64, 0x72, 0x65, 0x73,
0x73, 0x30, 0x05, 0x80, 0x03, 0x6f, 0x67, 0x6f, 0xa4, 0x0a,
0x04, 0x01, 0x6c, 0x30, 0x05, 0x80, 0x03, 0x6f, 0x67, 0x6f,
0xa4, 0x0b, 0x04, 0x02, 0x73, 0x74, 0x30, 0x05, 0x80, 0x03,
0x6f, 0x67, 0x6f, 0xa4, 0x16, 0x04, 0x0d, 0x70, 0x6f, 0x73,
0x74, 0x6f, 0x66, 0x66, 0x69, 0x63, 0x65, 0x62, 0x6f, 0x78,
0x30, 0x05, 0x80, 0x03, 0x6f, 0x67, 0x6f, 0xa4, 0x13, 0x04,
0x0a, 0x70, 0x6f, 0x73, 0x74, 0x61, 0x6c, 0x63, 0x6f, 0x64,
0x65, 0x30, 0x05, 0x80, 0x03, 0x6f, 0x67, 0x6f, 0xa4, 0x0a,
0x04, 0x01, 0x63, 0x30, 0x05, 0x80, 0x03, 0x6f, 0x67, 0x6f,
0xa4, 0x1a, 0x04, 0x11, 0x68, 0x6f, 0x6d, 0x65, 0x70, 0x6f,
0x73, 0x74, 0x61, 0x6c, 0x61, 0x64, 0x64, 0x72, 0x65, 0x73,
0x73, 0x30, 0x05, 0x80, 0x03, 0x6f, 0x67, 0x6f, 0xa4, 0x20,
0x04, 0x17, 0x6d, 0x6f, 0x7a, 0x69, 0x6c, 0x6c, 0x61, 0x68,
0x6f, 0x6d, 0x65, 0x6c, 0x6f, 0x63, 0x61, 0x6c, 0x69, 0x74,
0x79, 0x6e, 0x61, 0x6d, 0x65, 0x30, 0x05, 0x80, 0x03, 0x6f,
0x67, 0x6f, 0xa4, 0x19, 0x04, 0x10, 0x6d, 0x6f, 0x7a, 0x69,
0x6c, 0x6c, 0x61, 0x68, 0x6f, 0x6d, 0x65, 0x73, 0x74, 0x61,
0x74, 0x65, 0x30, 0x05, 0x80, 0x03, 0x6f, 0x67, 0x6f, 0xa4,
0x1e, 0x04, 0x15, 0x6d, 0x6f, 0x7a, 0x69, 0x6c, 0x6c, 0x61,
0x68, 0x6f, 0x6d, 0x65, 0x70, 0x6f, 0x73, 0x74, 0x61, 0x6c,
0x63, 0x6f, 0x64, 0x65, 0x30, 0x05, 0x80, 0x03, 0x6f, 0x67,
0x6f, 0xa4, 0x1f, 0x04, 0x16, 0x6d, 0x6f, 0x7a, 0x69, 0x6c,
0x6c, 0x61, 0x68, 0x6f, 0x6d, 0x65, 0x63, 0x6f, 0x75, 0x6e,
0x74, 0x72, 0x79, 0x6e, 0x61, 0x6d, 0x65, 0x30, 0x05, 0x80,
0x03, 0x6f, 0x67, 0x6f, 0xa4, 0x1b, 0x04, 0x12, 0x6f, 0x74,
0x68, 0x65, 0x72, 0x70, 0x6f, 0x73, 0x74, 0x61, 0x6c, 0x61,
0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x30, 0x05, 0x80, 0x03,
0x6f, 0x67, 0x6f, 0xa4, 0x12, 0x04, 0x09, 0x6a, 0x70, 0x65,
0x67, 0x70, 0x68, 0x6f, 0x74, 0x6f, 0x30, 0x05, 0x80, 0x03,
0x6f, 0x67, 0x6f, 0xa4, 0x18, 0x04, 0x0f, 0x75, 0x73, 0x65,
0x72, 0x63, 0x65, 0x72, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61,
0x74, 0x65, 0x30, 0x05, 0x80, 0x03, 0x6f, 0x67, 0x6f, 0xa4,
0x13, 0x04, 0x0a, 0x6c, 0x61, 0x62, 0x65, 0x6c, 0x65, 0x64,
0x75, 0x72, 0x69, 0x30, 0x05, 0x80, 0x03, 0x6f, 0x67, 0x6f,
0xa4, 0x14, 0x04, 0x0b, 0x64, 0x69, 0x73, 0x70, 0x6c, 0x61,
0x79, 0x6e, 0x61, 0x6d, 0x65, 0x30, 0x05, 0x80, 0x03, 0x6f,
0x67, 0x6f, 0xa4, 0x13, 0x04, 0x0a, 0x73, 0x70, 0x6f, 0x75,
0x73, 0x65, 0x6e, 0x61, 0x6d, 0x65, 0x30, 0x05, 0x80, 0x03,
0x6f, 0x67, 0x6f, 0xa4, 0x0d, 0x04, 0x04, 0x6e, 0x6f, 0x74,
0x65, 0x30, 0x05, 0x80, 0x03, 0x6f, 0x67, 0x6f, 0xa4, 0x14,
0x04, 0x0b, 0x61, 0x6e, 0x6e, 0x69, 0x76, 0x65, 0x72, 0x73,
0x61, 0x72, 0x79, 0x30, 0x05, 0x80, 0x03, 0x6f, 0x67, 0x6f,
0xa4, 0x12, 0x04, 0x09, 0x62, 0x69, 0x72, 0x74, 0x68, 0x64,
0x61, 0x74, 0x65, 0x30, 0x05, 0x80, 0x03, 0x6f, 0x67, 0x6f,
0xa4, 0x0f, 0x04, 0x06, 0x6d, 0x61, 0x69, 0x6c, 0x65, 0x72,
0x30, 0x05, 0x80, 0x03, 0x6f, 0x67, 0x6f, 0xa4, 0x0f, 0x04,
0x06, 0x66, 0x69, 0x6c, 0x65, 0x61, 0x73, 0x30, 0x05, 0x80,
0x03, 0x6f, 0x67, 0x6f, 0xa4, 0x11, 0x04, 0x08, 0x63, 0x61,
0x74, 0x65, 0x67, 0x6f, 0x72, 0x79, 0x30, 0x05, 0x80, 0x03,
0x6f, 0x67, 0x6f, 0xa4, 0x12, 0x04, 0x09, 0x63, 0x61, 0x6c,
0x63, 0x61, 0x6c, 0x75, 0x72, 0x69, 0x30, 0x05, 0x80, 0x03,
0x6f, 0x67, 0x6f, 0xa4, 0x11, 0x04, 0x08, 0x63, 0x61, 0x6c,
0x66, 0x62, 0x75, 0x72, 0x6c, 0x30, 0x05, 0x80, 0x03, 0x6f,
0x67, 0x6f, 0xa4, 0x14, 0x04, 0x0b, 0x69, 0x63, 0x73, 0x63,
0x61, 0x6c, 0x65, 0x6e, 0x64, 0x61, 0x72, 0x30, 0x05, 0x80,
0x03, 0x6f, 0x67, 0x6f,
0x30, 0x00 // attributes set, 0 bytes
]

16
node_modules/@ldapjs/messages/lib/deprecations.js generated vendored Normal file
View File

@ -0,0 +1,16 @@
'use strict'
const warning = require('process-warning')()
const clazz = 'LdapjsMessageWarning'
warning.create(clazz, 'LDAP_MESSAGE_DEP_001', 'messageID is deprecated. Use messageId instead.')
warning.create(clazz, 'LDAP_MESSAGE_DEP_002', 'The .json property is deprecated. Use .pojo instead.')
warning.create(clazz, 'LDAP_MESSAGE_DEP_003', 'abandonID is deprecated. Use abandonId instead.')
warning.create(clazz, 'LDAP_MESSAGE_DEP_004', 'errorMessage is deprecated. Use diagnosticMessage instead.')
warning.create(clazz, 'LDAP_ATTRIBUTE_SPEC_ERR_001', 'received attempt to define attribute with an empty name: attribute skipped.', { unlimited: true })
module.exports = warning

282
node_modules/@ldapjs/messages/lib/ldap-message.js generated vendored Normal file
View File

@ -0,0 +1,282 @@
'use strict'
const { BerReader, BerWriter } = require('@ldapjs/asn1')
const warning = require('./deprecations')
/**
* Implements a base LDAP message as defined in
* https://www.rfc-editor.org/rfc/rfc4511.html#section-4.1.1.
*/
class LdapMessage {
#messageId = 0
#protocolOp
#controls = []
/**
* @typedef {object} LdapMessageOptions
* @property {number} [messageId=1] An identifier for the message.
* @property {number} [protocolOp] The tag for the message operation.
* @property {import('@ldapjs/controls').Control[]} [controls] A set of LDAP
* controls to send with the message. See the `@ldapjs/controls` package.
*/
/**
* @param {LdapMessageOptions} [options]
*/
constructor (options = {}) {
this.#messageId = parseInt(options.messageId ?? options.messageID ?? '1', 10)
if (options.messageID !== undefined) {
warning.emit('LDAP_MESSAGE_DEP_001')
}
if (typeof options.protocolOp === 'number') {
this.#protocolOp = options.protocolOp
}
this.controls = options.controls ?? []
}
get [Symbol.toStringTag] () {
return 'LdapMessage'
}
/**
* A copy of the list of controls that will be sent with the request.
*
* @returns {import('@ldapjs/controls').Control[]}
*/
get controls () {
return this.#controls.slice(0)
}
/**
* Define the list of controls that will be sent with the request. Any
* existing controls will be discarded.
*
* @param {import('@ldapjs/controls').Control[]} values
*
* @throws When a control value is invalid.
*/
set controls (values) {
if (Array.isArray(values) !== true) {
throw Error('controls must be an array')
}
const newControls = []
for (const val of values) {
if (Object.prototype.toString.call(val) !== '[object LdapControl]') {
throw Error('control must be an instance of LdapControl')
}
newControls.push(val)
}
this.#controls = newControls
}
/**
* The message identifier.
*
* @type {number}
*/
get id () {
return this.#messageId
}
/**
* Define the message identifier for the request.
*
* @param {number} value
*/
set id (value) {
if (Number.isInteger(value) === false) {
throw Error('id must be an integer')
}
this.#messageId = value
}
/**
* Alias for {@link id}.
*
* @returns {number}
*/
get messageId () {
return this.id
}
/**
* Alias for {@link id}.
*
* @param {number} value
*/
set messageId (value) {
this.id = value
}
/**
* Alias for {@link id}.
*
* @returns {number}
*
* @deprecated
*/
get messageID () {
warning.emit('LDAP_MESSAGE_DEP_001')
return this.id
}
/**
* Alias for {@link id}.
*
* @param {number} value
*
* @deprecated
*/
set messageID (value) {
warning.emit('LDAP_MESSAGE_DEP_001')
this.id = value
}
/**
* Message type specific. Each message type must implement a `_dn` property
* that provides this value.
*
* @type {import('@ldapjs/dn').DN}
*/
get dn () {
return this._dn
}
/**
* The LDAP protocol operation code for the message.
*
* @type {number}
*/
get protocolOp () {
return this.#protocolOp
}
/**
* The name of the message class.
*
* @type {string}
*/
get type () {
return 'LdapMessage'
}
/**
* Use {@link pojo} instead.
*
* @deprecated
*/
get json () {
warning.emit('LDAP_MESSAGE_DEP_002')
return this.pojo
}
/**
* A serialized representation of the message as a plain JavaScript object.
* Specific message types must implement the `_pojo(obj)` method. The passed
* in `obj` must be extended with the specific message's unique properties
* and returned as the result.
*
* @returns {object}
*/
get pojo () {
let result = {
messageId: this.id,
protocolOp: this.#protocolOp,
type: this.type
}
if (typeof this._pojo === 'function') {
result = this._pojo(result)
}
result.controls = this.#controls.map(c => c.pojo)
return result
}
addControl (control) {
this.#controls.push(control)
}
/**
* Converts an {@link LdapMessage} object into a set of BER bytes that can
* be sent across the wire. Specific message implementations must implement
* the `_toBer(ber)` method. This method will write its unique sequence(s)
* to the passed in `ber` object.
*
* @returns {import('@ldapjs/asn1').BerReader}
*/
toBer () {
if (typeof this._toBer !== 'function') {
throw Error(`${this.type} does not implement _toBer`)
}
const writer = new BerWriter()
writer.startSequence()
writer.writeInt(this.id)
this._toBer(writer)
if (this.#controls.length > 0) {
writer.startSequence(0xa0)
for (const control of this.#controls) {
control.toBer(writer)
}
writer.endSequence()
}
writer.endSequence()
return new BerReader(writer.buffer)
}
/**
* Serializes the message into a JSON representation.
*
* @returns {string}
*/
toString () {
return JSON.stringify(this.pojo)
}
/**
* Parses a BER into a message object. The offset of the BER _must_ point
* to the start of an LDAP Message sequence. That is, the first few bytes
* must indicate:
*
* 1. a sequence tag and how many bytes are in that sequence
* 2. an integer representing the message identifier
* 3. a protocol operation, e.g. BindRequest, and the number of bytes in
* that operation
*
* @param {import('@ldapjs/asn1').BerReader} ber
*
* @returns {LdapMessage}
*/
static parse (ber) {
// We must require here because `parseToMessage` imports subclasses
// that need `LdapMessage` to be defined. If we try importing earlier,
// then `LdapMessage` will not be available, and we will get errors about
// trying to subclass null objects.
return require('./parse-to-message')(ber)
}
/**
* When invoked on specific message types, e.g. {@link BindRequest}, this
* method will parse a BER into a plain JavaScript object that is usable as
* an options object for constructing that specific message object.
*
* @param {import('@ldapjs/asn1').BerReader} ber A BER to parse. The reader
* offset must point to the start of a valid sequence, i.e. the "tag" byte
* in the TLV tuple, that represents the message to be parsed. For example,
* in a {@link BindRequest} the starting sequence and message identifier must
* already be read such that the offset is at the protocol operation sequence
* byte.
*/
static parseToPojo (ber) {
throw Error('Use LdapMessage.parse, or a specific message type\'s parseToPojo, instead.')
}
}
module.exports = LdapMessage

315
node_modules/@ldapjs/messages/lib/ldap-message.test.js generated vendored Normal file
View File

@ -0,0 +1,315 @@
'use strict'
const tap = require('tap')
const { BerReader } = require('@ldapjs/asn1')
const warning = require('./deprecations')
const { Control } = require('@ldapjs/controls')
const LdapMessage = require('./ldap-message')
// Silence the standard warning logs. We will test the messages explicitly.
process.removeAllListeners('warning')
const {
abandonRequestBytes,
bindRequestBytes,
deleteRequestBytes
} = require('./messages/_fixtures/message-byte-arrays')
tap.test('constructor', t => {
t.test('no args', async t => {
const message = new LdapMessage()
t.strictSame(message.pojo, {
messageId: 1,
protocolOp: undefined,
type: 'LdapMessage',
controls: []
})
})
t.test('all options supplied', t => {
process.on('warning', handler)
t.teardown(async () => {
process.removeListener('warning', handler)
warning.emitted.set('LDAP_MESSAGE_DEP_001', false)
})
const message = new LdapMessage({
messageID: 10,
protocolOp: 0x01,
controls: [new Control({ type: 'foo', value: 'foo' })]
})
t.strictSame(message.pojo, {
messageId: 10,
protocolOp: 0x01,
type: 'LdapMessage',
controls: [{
type: 'foo',
value: 'foo',
criticality: false
}]
})
function handler (error) {
t.equal(error.message, 'messageID is deprecated. Use messageId instead.')
t.end()
}
})
t.end()
})
tap.test('misc', t => {
t.test('toStringTag is correct', async t => {
const message = new LdapMessage()
t.equal(Object.prototype.toString.call(message), '[object LdapMessage]')
})
t.test('dn returns _dn', async t => {
class Foo extends LdapMessage {
get _dn () {
return 'foo'
}
}
const message = new Foo()
t.equal(message.dn, 'foo')
})
t.test('protocolOp returns code', async t => {
const message = new LdapMessage({ protocolOp: 1 })
t.equal(message.protocolOp, 1)
})
t.test('json emits warning', t => {
process.on('warning', handler)
t.teardown(async () => {
process.removeListener('warning', handler)
warning.emitted.set('LDAP_MESSAGE_DEP_002', false)
})
const message = new LdapMessage()
t.ok(message.json)
function handler (error) {
t.equal(
error.message,
'The .json property is deprecated. Use .pojo instead.'
)
t.end()
}
})
t.test('toString returns JSON', async t => {
const message = new LdapMessage()
const expected = JSON.stringify(message.pojo)
t.equal(message.toString(), expected)
})
t.end()
})
tap.test('.controls', t => {
t.test('sets/gets', async t => {
const req = new LdapMessage()
t.strictSame(req.controls, [])
req.controls = [new Control()]
t.strictSame(req.pojo, {
messageId: 1,
protocolOp: undefined,
type: 'LdapMessage',
controls: [{
type: '',
value: null,
criticality: false
}]
})
})
t.test('rejects for non-array', async t => {
const req = new LdapMessage()
t.throws(
() => {
req.controls = {}
},
'controls must be an array'
)
})
t.test('rejects if array item is not a control', async t => {
const req = new LdapMessage()
t.throws(
() => {
req.controls = ['foo']
},
'control must be an instance of LdapControl'
)
})
t.end()
})
tap.test('.id', t => {
t.test('sets/gets', async t => {
const req = new LdapMessage()
t.equal(req.id, 1)
req.id = 2
t.equal(req.id, 2)
t.equal(req.messageId, 2)
req.messageId = 3
t.equal(req.id, 3)
})
t.test('throws if not an integer', async t => {
const req = new LdapMessage()
t.throws(
() => {
req.id = 1.5
},
'id must be an integer'
)
})
t.test('get messageID is deprecated', t => {
process.on('warning', handler)
t.teardown(async () => {
process.removeListener('warning', handler)
warning.emitted.set('LDAP_MESSAGE_DEP_001', false)
})
const message = new LdapMessage()
t.ok(message.messageID)
function handler (error) {
t.equal(
error.message,
'messageID is deprecated. Use messageId instead.'
)
t.end()
}
})
t.test('set messageID is deprecated', t => {
process.on('warning', handler)
t.teardown(async () => {
process.removeListener('warning', handler)
warning.emitted.set('LDAP_MESSAGE_DEP_001', false)
})
const message = new LdapMessage()
message.messageID = 2
function handler (error) {
t.equal(
error.message,
'messageID is deprecated. Use messageId instead.'
)
t.end()
}
})
t.end()
})
tap.test('toBer', t => {
t.test('throws for bad subclass', async t => {
class Foo extends LdapMessage {
}
const message = new Foo()
t.throws(
() => message.toBer(),
Error('LdapMessage does not implement _toBer')
)
})
t.test('converts BindRequest to BER', async t => {
const reqBuffer = Buffer.from(bindRequestBytes)
const reader = new BerReader(reqBuffer)
const message = LdapMessage.parse(reader)
const ber = message.toBer()
t.equal('[object BerReader]', Object.prototype.toString.call(ber))
t.equal(reqBuffer.compare(ber.buffer), 0)
})
t.test('converts DeleteRequest to BER', async t => {
const reqBuffer = Buffer.from(deleteRequestBytes)
const reader = new BerReader(reqBuffer)
const message = LdapMessage.parse(reader)
const ber = message.toBer()
t.equal(reqBuffer.compare(ber.buffer), 0)
})
t.end()
})
tap.test('#parse', t => {
t.test('parses an abandon request', async t => {
const reader = new BerReader(Buffer.from(abandonRequestBytes))
const message = LdapMessage.parse(reader)
t.strictSame(message.pojo, {
messageId: 6,
protocolOp: 0x50,
type: 'AbandonRequest',
abandonId: 5,
controls: []
})
})
t.test('parses a bind request', async t => {
const reader = new BerReader(Buffer.from(bindRequestBytes))
const message = LdapMessage.parse(reader)
t.strictSame(message.pojo, {
messageId: 1,
protocolOp: 0x60,
type: 'BindRequest',
version: 3,
name: 'uid=admin,ou=system',
authenticationType: 'simple',
credentials: 'secret',
controls: []
})
t.equal(message.name, 'uid=admin,ou=system')
})
t.test('parses a delete request with controls', async t => {
const reader = new BerReader(Buffer.from(deleteRequestBytes))
const message = LdapMessage.parse(reader)
// We need to parse the JSON representation because stringSame will return
// false when comparing a plain object to an instance of Control.
t.strictSame(JSON.parse(message.toString()), {
messageId: 5,
protocolOp: 0x4a,
type: 'DeleteRequest',
entry: 'dc=example,dc=com',
controls: [{
type: '1.2.840.113556.1.4.805',
criticality: true,
value: null
}]
})
})
t.end()
})
tap.test('#parseToPojo', t => {
t.test('throws because not implemented', async t => {
const expected = Error('Use LdapMessage.parse, or a specific message type\'s parseToPojo, instead.')
t.throws(
() => LdapMessage.parseToPojo(),
expected
)
})
t.end()
})

244
node_modules/@ldapjs/messages/lib/ldap-result.js generated vendored Normal file
View File

@ -0,0 +1,244 @@
'use strict'
const LdapMessage = require('./ldap-message')
const { resultCodes, operations } = require('@ldapjs/protocol')
const warning = require('./deprecations')
/**
* Implements the base LDAP response message as defined in
* https://www.rfc-editor.org/rfc/rfc4511.html#section-4.1.9.
*/
class LdapResult extends LdapMessage {
#connection = null
#diagnosticMessage
#matchedDN
#referrals = []
#status
/**
* @typedef {LdapMessageOptions} LdapResultOptions
* @property {number} [status=0] An LDAP status code.
* @param {string} [matchedDN=''] The DN that matched the request.
* @param {string[]} [referrals=[]] A set of servers to query for references.
* @param {string} [diagnosticMessage] A message indicating why a request
* failed.
*/
/**
* @param {LdapResultOptions} options
*/
constructor (options = {}) {
super(options)
this.#status = options.status ?? resultCodes.SUCCESS
this.#matchedDN = options.matchedDN || ''
this.#referrals = options.referrals || []
this.#diagnosticMessage = options.diagnosticMessage || options.errorMessage || ''
if (options.errorMessage) {
warning.emit('LDAP_MESSAGE_DEP_004')
}
}
/**
* The failure message as returned by the server if one is present.
*
* @returns {string}
*/
get diagnosticMessage () {
return this.#diagnosticMessage
}
/**
* Add a diagnostic message to the instance.
*
* @param {string} message
*/
set diagnosticMessage (message) {
this.#diagnosticMessage = message
}
/**
* The DN that a request matched.
*
* @returns {string}
*/
get matchedDN () {
return this.#matchedDN
}
/**
* Define which DN a request matched.
*
* @param {string} dn
*/
set matchedDN (dn) {
this.#matchedDN = dn
}
/**
* A serialized representation of the message as a plain JavaScript object.
* Specific message types must implement the `_pojo(obj)` method. The passed
* in `obj` must be extended with the specific message's unique properties
* and returned as the result.
*
* @returns {object}
*/
get pojo () {
let result = {
status: this.status,
matchedDN: this.matchedDN,
diagnosticMessage: this.diagnosticMessage,
referrals: this.referrals
}
if (typeof this._pojo === 'function') {
result = this._pojo(result)
}
return result
}
/**
* The list of servers that should be consulted to get an answer
* to the query.
*
* @returns {string[]}
*/
get referrals () {
return this.#referrals.slice(0)
}
/**
* The LDAP response code for the request.
*
* @returns {number}
*/
get status () {
return this.#status
}
/**
* Set the response code for the request.
*
* @param {number} s
*/
set status (s) {
this.#status = s
}
/**
* The name of the request type.
*
* @type {string}
*/
get type () {
return 'LdapResult'
}
/**
* Add a new server to the list of servers that should be
* consulted for an answer to the query.
*
* @param {string} referral
*/
addReferral (referral) {
this.#referrals.push(referral)
}
/**
* Internal use only. Subclasses may implement a `_writeResponse`
* method to add to the sequence after any referrals.
*
* @param {import('@ldapjs/asn1').BerWriter} ber
*
* @returns {import('@ldapjs/asn1').BerWriter}
*
* @private
*/
_toBer (ber) {
ber.startSequence(this.protocolOp)
ber.writeEnumeration(this.status)
ber.writeString(this.matchedDN)
ber.writeString(this.diagnosticMessage)
if (this.referrals.length > 0) {
ber.startSequence(operations.LDAP_RES_REFERRAL)
ber.writeStringArray(this.referrals)
ber.endSequence()
}
if (typeof this._writeResponse === 'function') {
this._writeResponse(ber)
}
ber.endSequence()
}
/**
* When invoked on specific message types, e.g. {@link AddResponse}, this
* method will parse a BER into a plain JavaScript object that is usable as
* an options object for constructing that specific message object.
*
* @param {import('@ldapjs/asn1').BerReader} ber A BER to parse. The reader
* offset must point to the start of a valid sequence, i.e. the "tag" byte
* in the TLV tuple, that represents the message to be parsed. For example,
* in a {@link AddResponse} the starting sequence and message identifier must
* already be read such that the offset is at the protocol operation sequence
* byte.
*/
static parseToPojo (ber) {
throw Error('Use LdapMessage.parse, or a specific message type\'s parseToPojo, instead.')
}
/**
* Internal use only.
*
* Response messages are a little more generic to parse than request messages.
* However, they still need to recognize the correct protocol operation. So
* the public {@link parseToPojo} for each response object should invoke this
* private static method to parse the BER and indicate the correct protocol
* operation to recognize.
*
* @param {object} input
* @param {number} input.opCode The expected protocol operation to look for.
* @param {import('@ldapjs/asn1').BerReader} berReader The BER to process. It
* must start at an offset representing a protocol operation tag.
* @param {object} [input.pojo] A plain JavaScript object to populate with
* the parsed keys and values.
*
* @returns {object}
*
* @private
*/
static _parseToPojo ({ opCode, berReader, pojo = {} }) {
const protocolOp = berReader.readSequence()
if (protocolOp !== opCode) {
const op = protocolOp.toString(16).padStart(2, '0')
throw Error(`found wrong protocol operation: 0x${op}`)
}
const status = berReader.readEnumeration()
const matchedDN = berReader.readString()
const diagnosticMessage = berReader.readString()
const referrals = []
if (berReader.peek() === operations.LDAP_RES_REFERRAL) {
// Advance the offset to the start of the value and
// put the sequence length into the `reader.length` field.
berReader.readSequence(operations.LDAP_RES_REFERRAL)
const end = berReader.length
while (berReader.offset < end) {
referrals.push(berReader.readString())
}
}
pojo.status = status
pojo.matchedDN = matchedDN
pojo.diagnosticMessage = diagnosticMessage
pojo.referrals = referrals
return pojo
}
}
module.exports = LdapResult

283
node_modules/@ldapjs/messages/lib/ldap-result.test.js generated vendored Normal file
View File

@ -0,0 +1,283 @@
'use strict'
const tap = require('tap')
const warning = require('./deprecations')
const { resultCodes, operations } = require('@ldapjs/protocol')
const { BerReader } = require('@ldapjs/asn1')
const {
addResponseBasicBytes,
addResponseNoSuchObjectBytes,
addResponseReferralsBytes,
extensionDisconnectionNotificationResponseBytes
} = require('./messages/_fixtures/message-byte-arrays')
const RECOGNIZED_OIDS = require('./messages/extension-utils/recognized-oids')
const LdapResult = require('./ldap-result')
const ExtensionResponse = require('./messages/extension-response')
// Silence the standard warning logs. We will test the messages explicitly.
process.removeAllListeners('warning')
tap.test('constructor', t => {
t.test('no args', async t => {
const res = new LdapResult()
t.equal(res.status, 0)
t.equal(res.matchedDN, '')
t.strictSame(res.referrals, [])
t.equal(res.diagnosticMessage, '')
})
t.test('emits warning for abandonID', t => {
process.on('warning', handler)
t.teardown(async () => {
process.removeListener('warning', handler)
warning.emitted.set('LDAP_MESSAGE_DEP_004', false)
})
const res = new LdapResult({
errorMessage: 'foo'
})
t.ok(res)
function handler (error) {
t.equal(
error.message,
'errorMessage is deprecated. Use diagnosticMessage instead.'
)
t.end()
}
})
t.test('with options', async t => {
const res = new LdapResult({
status: 1,
matchedDN: 'foo',
referrals: ['foo.example.com'],
diagnosticMessage: 'bar'
})
t.equal(res.status, 1)
t.equal(res.matchedDN, 'foo')
t.strictSame(res.referrals, ['foo.example.com'])
t.equal(res.diagnosticMessage, 'bar')
})
t.end()
})
tap.test('.diagnosticMessage', t => {
t.test('sets and gets', async t => {
const res = new LdapResult()
t.equal(res.diagnosticMessage, '')
res.diagnosticMessage = 'foo'
t.equal(res.diagnosticMessage, 'foo')
})
t.end()
})
tap.test('.matchedDN', t => {
t.test('sets and gets', async t => {
const res = new LdapResult()
t.equal(res.matchedDN, '')
res.matchedDN = 'foo'
t.equal(res.matchedDN, 'foo')
})
t.end()
})
tap.test('.pojo', t => {
t.test('returns a plain JavaScript object', async t => {
const res = new LdapResult()
t.strictSame(res.pojo, {
status: 0,
matchedDN: '',
diagnosticMessage: '',
referrals: []
})
})
t.test('returns a plain JavaScript object from subclass', async t => {
class Foo extends LdapResult {
_pojo (obj) {
obj.foo = 'foo'
return obj
}
}
const res = new Foo()
t.strictSame(res.pojo, {
status: 0,
matchedDN: '',
diagnosticMessage: '',
referrals: [],
foo: 'foo'
})
})
t.end()
})
tap.test('.referrals', t => {
t.test('gets', async t => {
const res = new LdapResult({ referrals: ['foo'] })
t.strictSame(res.referrals, ['foo'])
})
t.end()
})
tap.test('.status', t => {
t.test('sets and gets', async t => {
const res = new LdapResult()
t.equal(res.status, 0)
res.status = 1
t.equal(res.status, 1)
})
t.end()
})
tap.test('.type', t => {
t.test('gets', async t => {
const res = new LdapResult()
t.equal(res.type, 'LdapResult')
})
t.end()
})
tap.test('addReferral', t => {
t.test('adds to existing list', async t => {
const res = new LdapResult({ referrals: ['foo'] })
t.strictSame(res.referrals, ['foo'])
res.addReferral('bar')
t.strictSame(res.referrals, ['foo', 'bar'])
})
t.end()
})
tap.test('_toBer', t => {
t.test('returns basic bytes', async t => {
const res = new LdapResult({
protocolOp: operations.LDAP_RES_ADD,
messageId: 2
})
const ber = res.toBer()
const expected = Buffer.from(addResponseBasicBytes)
t.equal(expected.compare(ber.buffer), 0)
})
t.test('returns bytes with referrals', async t => {
const res = new LdapResult({
protocolOp: operations.LDAP_RES_ADD,
messageId: 3,
status: resultCodes.REFERRAL,
diagnosticMessage: 'This server is read-only. Try a different one.',
referrals: [
'ldap://alternate1.example.com:389/uid=jdoe,ou=Remote,dc=example,dc=com',
'ldap://alternate2.example.com:389/uid=jdoe,ou=Remote,dc=example,dc=com'
]
})
const ber = res.toBer()
const expected = Buffer.from(addResponseReferralsBytes)
t.equal(expected.compare(ber.buffer), 0)
})
t.test('hands off to _writeResponse', async t => {
const res = new ExtensionResponse({
protocolOp: operations.LDAP_RES_EXTENSION,
messageId: 0,
status: resultCodes.UNAVAILABLE,
diagnosticMessage: 'The Directory Server is shutting down',
referrals: [],
responseName: RECOGNIZED_OIDS.get('DISCONNECTION_NOTIFICATION')
})
const ber = res.toBer()
const expected = Buffer.from(extensionDisconnectionNotificationResponseBytes)
t.equal(expected.compare(ber.buffer), 0)
})
t.end()
})
tap.test('#parseToPojo', t => {
t.test('throws because not implemented', async t => {
const expected = Error('Use LdapMessage.parse, or a specific message type\'s parseToPojo, instead.')
t.throws(
() => LdapResult.parseToPojo(),
expected
)
})
t.end()
})
tap.test('#_parseToPojo', async t => {
t.test('throws if protocol op is wrong', async t => {
const bytes = addResponseBasicBytes.slice(5)
bytes[0] = 0x68
const berReader = new BerReader(Buffer.from(bytes))
t.throws(
() => LdapResult._parseToPojo({
opCode: operations.LDAP_RES_ADD,
berReader
}),
Error('found wrong protocol operation: 0x68')
)
})
t.test('parses a basic object', async t => {
const bytes = addResponseBasicBytes.slice(5)
const berReader = new BerReader(Buffer.from(bytes))
const pojo = { foo: 'foo' }
LdapResult._parseToPojo({
opCode: operations.LDAP_RES_ADD,
berReader,
pojo
})
t.strictSame(pojo, {
status: 0,
matchedDN: '',
diagnosticMessage: '',
referrals: [],
foo: 'foo'
})
})
t.test('parses object with matched dn and diagnostic message', async t => {
const bytes = addResponseNoSuchObjectBytes.slice(6)
const berReader = new BerReader(Buffer.from(bytes))
const pojo = LdapResult._parseToPojo({
opCode: operations.LDAP_RES_ADD,
berReader
})
t.strictSame(pojo, {
status: resultCodes.NO_SUCH_OBJECT,
referrals: [],
matchedDN: 'ou=People, dc=example, dc=com',
diagnosticMessage: [
'Entry uid=missing1, ou=missing2, ou=People, dc=example, dc=com cannot',
' be created because its parent does not exist.'
].join('')
})
})
t.test('parses object with referrals', async t => {
const bytes = addResponseReferralsBytes.slice(6)
const berReader = new BerReader(Buffer.from(bytes))
const pojo = LdapResult._parseToPojo({
opCode: operations.LDAP_RES_ADD,
berReader
})
t.strictSame(pojo, {
status: resultCodes.REFERRAL,
referrals: [
'ldap://alternate1.example.com:389/uid=jdoe,ou=Remote,dc=example,dc=com',
'ldap://alternate2.example.com:389/uid=jdoe,ou=Remote,dc=example,dc=com'
],
matchedDN: '',
diagnosticMessage: 'This server is read-only. Try a different one.'
})
})
})

View File

@ -0,0 +1,828 @@
'use strict'
// The byte arrays in this file are used by the
// `parseToMessage` function test suite. Any byte block
// added to this file will automatically get picked up by
// that test suite. Thus, any byte block added here should be
// parseable, and serializable, by the generic LDAP object
// type associated with that byte block.
module.exports.abandonRequestBytes = [
0x30, 0x06, // sequence, 6 bytes
0x02, 0x01, 0x06, // message id (integer value "6")
0x50, 0x01, 0x05 // abandon request protocol op (application primitive integer 5)
]
/**
* Technically, this is nonsense. The spec does not define an abandon response.
* We are making something up here just to test the ldapjs specific response
* object.
*/
module.exports.abandonResponseBytes = [
0x30, 0x0c, // start sequence, 12 bytes
0x02, 0x01, 0x01, // message id (integer value 1)
0x00, 0x07, // protocol op (0x61) bind response
0x0a, 0x01, 0x00, // success result code (enumerated value 0)
0x04, 0x00, // no matched DN (0-byte octet string)
0x04, 0x00 // no diagnostic message (0-byte octet string)
]
/**
* Represents a basic ADD response.
* Taken from https://web.archive.org/web/20220630073105/https://nawilson.com/ldapv3-wire-protocol-reference-add/
*/
module.exports.addResponseBasicBytes = [
0x30, 0x0c, // sequence, 12 bytes
0x02, 0x01, 0x02, // message id 2
0x69, 0x07, // add response op, 7 bytes
0x0a, 0x01, 0x00, // success result code (enumerated value 0)
0x04, 0x00, // no matched DN (0-byte octet string)
0x04, 0x00 // no diagnostic message (0-byte octet string)
]
/**
* Represents an ADD response with an error and a diagnostic message.
* Taken from https://web.archive.org/web/20220630073105/https://nawilson.com/ldapv3-wire-protocol-reference-add/
*/
module.exports.addResponseNoSuchObjectBytes = [
0x30, 0x81, 0x9d, // sequence, 157 bytes
0x02, 0x01, 0x03, // message id 3
0x69, 0x81, 0x97, // add response op, 151 bytes
0x0a, 0x01, 0x20, // noSuchObject result code (enumerated value 32)
0x04, 0x1d, // octet string, 29 bytes (matched dn)
0x6f, 0x75, 0x3d, 0x50, 0x65, 0x6f, 0x70, 0x6c, // "ou=Peopl"
0x65, 0x2c, 0x20, 0x64, 0x63, 0x3d, 0x65, 0x78, // "e, dc=ex"
0x61, 0x6d, 0x70, 0x6c, 0x65, 0x2c, 0x20, 0x64, // "ample, d"
0x63, 0x3d, 0x63, 0x6f, 0x6d, // "c=com"
0x04, 0x73, // octet string, 115 bytes (diagnostic message)
// "Entry uid=missing1, ou=missing2, ou=People, dc=example, dc=com cannot be
// created because its parent does not exist."
0x45, 0x6e, 0x74, 0x72, 0x79, 0x20, 0x75, 0x69,
0x64, 0x3d, 0x6d, 0x69, 0x73, 0x73, 0x69, 0x6e,
0x67, 0x31, 0x2c, 0x20, 0x6f, 0x75, 0x3d, 0x6d,
0x69, 0x73, 0x73, 0x69, 0x6e, 0x67, 0x32, 0x2c,
0x20, 0x6f, 0x75, 0x3d, 0x50, 0x65, 0x6f, 0x70,
0x6c, 0x65, 0x2c, 0x20, 0x64, 0x63, 0x3d, 0x65,
0x78, 0x61, 0x6d, 0x70, 0x6c, 0x65, 0x2c, 0x20,
0x64, 0x63, 0x3d, 0x63, 0x6f, 0x6d, 0x20, 0x63,
0x61, 0x6e, 0x6e, 0x6f, 0x74, 0x20, 0x62, 0x65,
0x20, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x64,
0x20, 0x62, 0x65, 0x63, 0x61, 0x75, 0x73, 0x65,
0x20, 0x69, 0x74, 0x73, 0x20, 0x70, 0x61, 0x72,
0x65, 0x6e, 0x74, 0x20, 0x64, 0x6f, 0x65, 0x73,
0x20, 0x6e, 0x6f, 0x74, 0x20, 0x65, 0x78, 0x69,
0x73, 0x74, 0x2e
]
/**
* Represents an ADD response with referrals.
* Taken from https://web.archive.org/web/20220630073105/https://nawilson.com/ldapv3-wire-protocol-reference-add/
*/
module.exports.addResponseReferralsBytes = [
0x30, 0x81, 0xcf, // sequence, 207 bytes
0x02, 0x01, 0x03, // message id 3
0x69, 0x81, 0xc9, // add response op, 201 bytes
0x0a, 0x01, 0x0a, // referral result code (enumerated value 10)
0x04, 0x00, // no matched dn (0-byte octet string)
0x04, 0x2f, // octet string, 47 bytes (diagnostic message)
// "This server is read-only. Try a different one."
0x54, 0x68, 0x69, 0x73, 0x20, 0x73, 0x65, 0x72,
0x76, 0x65, 0x72, 0x20, 0x69, 0x73, 0x20, 0x72,
0x65, 0x61, 0x64, 0x2d, 0x6f, 0x6e, 0x6c, 0x79,
0x2e, 0x20, 0x20, 0x54, 0x72, 0x79, 0x20, 0x61,
0x20, 0x64, 0x69, 0x66, 0x66, 0x65, 0x72, 0x65,
0x6e, 0x74, 0x20, 0x6f, 0x6e, 0x65, 0x2e,
0xa3, 0x81, 0x90, // referrals sequence, 144 bytes
0x04, 0x46, // string, 70 bytes (first url)
// "ldap://alternate1.example.com:389/uid=jdoe,ou=Remote,dc=example,dc=com"
0x6c, 0x64, 0x61, 0x70, 0x3a, 0x2f, 0x2f, 0x61,
0x6c, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x74, 0x65,
0x31, 0x2e, 0x65, 0x78, 0x61, 0x6d, 0x70, 0x6c,
0x65, 0x2e, 0x63, 0x6f, 0x6d, 0x3a, 0x33, 0x38,
0x39, 0x2f, 0x75, 0x69, 0x64, 0x3d, 0x6a, 0x64,
0x6f, 0x65, 0x2c, 0x6f, 0x75, 0x3d, 0x52, 0x65,
0x6d, 0x6f, 0x74, 0x65, 0x2c, 0x64, 0x63, 0x3d,
0x65, 0x78, 0x61, 0x6d, 0x70, 0x6c, 0x65, 0x2c,
0x64, 0x63, 0x3d, 0x63, 0x6f, 0x6d,
0x04, 0x46, // string, 70 bytes (second url)
// "ldap://alternate2.example.com:389/uid=jdoe,ou=Remote,dc=example,dc=com"
0x6c, 0x64, 0x61, 0x70, 0x3a, 0x2f, 0x2f, 0x61,
0x6c, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x74, 0x65,
0x32, 0x2e, 0x65, 0x78, 0x61, 0x6d, 0x70, 0x6c,
0x65, 0x2e, 0x63, 0x6f, 0x6d, 0x3a, 0x33, 0x38,
0x39, 0x2f, 0x75, 0x69, 0x64, 0x3d, 0x6a, 0x64,
0x6f, 0x65, 0x2c, 0x6f, 0x75, 0x3d, 0x52, 0x65,
0x6d, 0x6f, 0x74, 0x65, 0x2c, 0x64, 0x63, 0x3d,
0x65, 0x78, 0x61, 0x6d, 0x70, 0x6c, 0x65, 0x2c,
0x64, 0x63, 0x3d, 0x63, 0x6f, 0x6d
]
/**
* Represents an ADD request with attributes.
* Taken from https://web.archive.org/web/20220630073105/https://nawilson.com/ldapv3-wire-protocol-reference-add/
*/
module.exports.addRequestBytes = [
0x30, 0x49, // start sequence, 73 bytes
0x02, 0x01, 0x02, // message id 2
0x68, 0x44, // add op, 68 bytes
0x04, 0x11, // entry dn string, 17 bytes
0x64, 0x63, 0x3d, 0x65, // "dc=example,dc=com"
0x78, 0x61, 0x6d, 0x70,
0x6c, 0x65, 0x2c, 0x64,
0x63, 0x3d, 0x63, 0x6f,
0x6d,
0x30, 0x2f, // start attributes sequence, 47 bytes
0x30, 0x1c, // start first attribute sequence, 28 bytes
0x04, 0x0b, // string, 11 bytes
0x6f, 0x62, 0x6a, 0x65, // "objectclass"
0x63, 0x74, 0x63, 0x6c,
0x61, 0x73, 0x73,
0x31, 0x0d, // start value sequence, 13 bytes
0x04, 0x03, 0x74, 0x6f, 0x70, // string: "top"
0x04, 0x06, 0x64, 0x6f, 0x6d, 0x61, 0x69, 0x6e, // string: "domain"
0x30, 0x0f, // start second attribute sequence, 15 bytes
0x04, 0x02, 0x64, 0x63, // string: "dc"
0x31, 0x09, // start second value sequence, 9 bytes
0x04, 0x07, 0x65, 0x78, 0x61, 0x6d, 0x70, 0x6c, 0x65 // string: "example"
]
/**
* Represents an LDAP BIND Request Message as defined by
* RFC 4511 §4.2. It has a message id of "1" and attempts to bind
* with simple authentication as user "uid=admin,ou=sys" with the password
* "secret".
*/
module.exports.bindRequestBytes = [
0x30, 0x25, // sequence, 37 bytes
0x02, 0x01, 0x01, // message id (integer value "1")
0x60, 0x20, // protocol op (0x60), 32 bytes
0x02, 0x01, 0x03, // version (integer value "3")
0x04, 0x13, // string, 19 bytes
0x75, 0x69, 0x64, 0x3d, // "uid="
0x61, 0x64, 0x6d, 0x69, 0x6e, 0x2c, // "admin,"
0x6f, 0x75, 0x3d, 0x73, 0x79, 0x73, // "ou=sys"
0x74, 0x65, 0x6d, // "tem"
0x80, 0x06, // auth choice 0 ("simple"), 6 bytes
0x73, 0x65, 0x63, 0x72, 0x65, 0x74 // "secret"
]
/**
* Represents an anonymous BIND request.
*/
module.exports.bindRequestAnonymousBytes = [
0x30, 0x0c,
0x02, 0x01, 0x01,
0x60, 0x07,
0x02, 0x01, 0x03,
0x04, 0x00,
0x80, 0x00
]
/**
* Represents a BIND response with attributes.
* Taken from https://web.archive.org/web/20220518232654/https://nawilson.com/ldapv3-wire-protocol-reference-bind/
*/
module.exports.bindResponseBytes = [
0x30, 0x0c, // start sequence, 12 bytes
0x02, 0x01, 0x01, // message id (integer value 1)
0x61, 0x07, // protocol op (0x61) bind response
0x0a, 0x01, 0x00, // success result code (enumerated value 0)
0x04, 0x00, // no matched DN (0-byte octet string)
0x04, 0x00 // no diagnostic message (0-byte octet string)
]
/**
* Represents a COMPARE request with attributes.
* Taken from https://web.archive.org/web/20220630080454/https://nawilson.com/ldapv3-wire-protocol-reference-compare/
*/
module.exports.compareRequestBytes = [
0x30, 0x45, // start sequence, 69 bytes
0x02, 0x01, 0x02, // message id (integer value 2)
0x6e, 0x40, // protocol op (0x6e) compare request
0x04, 0x24, // string (target entry dn), 36 bytes
// "uid=jdoe,ou=People,dc=example,dc=com")
0x75, 0x69, 0x64, 0x3d, 0x6a, 0x64, 0x6f, 0x65,
0x2c, 0x6f, 0x75, 0x3d, 0x50, 0x65, 0x6f, 0x70,
0x6c, 0x65, 0x2c, 0x64, 0x63, 0x3d, 0x65, 0x78,
0x61, 0x6d, 0x70, 0x6c, 0x65, 0x2c, 0x64, 0x63,
0x3d, 0x63, 0x6f, 0x6d,
0x30, 0x18, // sequence, 24 bytes
0x04, 0x0c, // string (attribute name), 12 bytes
// "employeeType"
0x65, 0x6d, 0x70, 0x6c,
0x6f, 0x79, 0x65, 0x65,
0x54, 0x79, 0x70, 0x65,
0x04, 0x08, // string (assertion value), 8 bytes
// "salaried"
0x73, 0x61, 0x6c, 0x61,
0x72, 0x69, 0x65, 0x64
]
/**
* Represents a COMPARE response with attributes.
* Taken from https://web.archive.org/web/20220630080454/https://nawilson.com/ldapv3-wire-protocol-reference-compare/
*/
module.exports.compareResponseBytes = [
0x30, 0x0c, // start sequence, 12 bytes
0x02, 0x01, 0x02, // message id (integer value 2)
0x6f, 0x07, // protocol op (0x6f), 7 bytes
0x0a, 0x01, 0x06, // compareTrue result code (enumerated value 6)
0x04, 0x00, // No matched DN (0-byte octet string)
0x04, 0x00 // No diagnostic message (0-byte octet string)
]
/**
* Represents a DELETE request with controls supplied for recursive removal.
* Taken from https://web.archive.org/web/20220629203240/https://ldap.com/ldapv3-wire-protocol-reference-ldap-message/#ldapmessage-example
*/
module.exports.deleteRequestBytes = [
0x30, 0x35, // start sequence, 53 bytes
0x02, 0x01, 0x05, // message id 5
0x4a, 0x11, // delete request op (entry), 17 bytes
0x64, 0x63, 0x3d, 0x65, 0x78, 0x61, 0x6d, 0x70, 0x6c, // dc=exampl
0x65, 0x2c, 0x64, 0x63, 0x3d, 0x63, 0x6f, 0x6d, // e,dc=com
0xa0, 0x1d, // start sequence (controls), 29 bytes
0x30, 0x1b, // start first control sequence, 27 bytes
0x04, 0x16, 0x31, 0x2e, // control OID (octet string):
0x32, 0x2e, 0x38, 0x34, // 1.2.840.113556.1.4.805
0x30, 0x2e, 0x31, 0x31,
0x33, 0x35, 0x35, 0x36,
0x2e, 0x31, 0x2e, 0x34,
0x2e, 0x38, 0x30, 0x35,
0x01, 0x01, 0xff // control criticality (boolean true)
]
/**
* Represents a DELETE response.
* Taken from https://web.archive.org/web/20220518193408/https://nawilson.com/ldapv3-wire-protocol-reference-delete/
*/
module.exports.deleteResponseBytes = [
0x30, 0x0c, // start sequence, 12 bytes
0x02, 0x01, 0x02, // message id (integer value 2)
0x6b, 0x07, // protocol op (0x6b), 7 bytes
0x0a, 0x01, 0x00, // success result code (enumerated value 0)
0x04, 0x00, // no matched DN (0-byte octet string)
0x04, 0x00 // no diagnostic message (0-byte octet string)
]
/**
* Represents a simple EXTENSION request with a string name
* and string value.
*/
module.exports.extensionNameAndValueRequestBytes = [
0x30, 0x18, // start sequence, 24 bytes
0x02, 0x01, 0x01, // message ID (integer value 1)
0x77, 0x13, // protocol op (0x77), 19 bytes
0x80, 0x09, // string (request name), 9 bytes
// "1.3.6.1.1"
0x31, 0x2e, 0x33, 0x2e, 0x36, 0x2e, 0x31, 0x2e, 0x31,
0x81, 0x06, // string (request value), 6 bytes
// "foobar"
0x66, 0x6f, 0x6f, 0x62, 0x61, 0x72
]
/**
* Represents a simple EXTENSION request with a string name
* and no value.
*/
module.exports.extensionNameAndNoValueRequestBytes = [
0x30, 0x10, // start sequence, 16 bytes
0x02, 0x01, 0x01, // message ID (integer value 1)
0x77, 0x0b, // protocol op (0x77), 11 bytes
0x80, 0x09, // string (request name), 9 bytes
// "1.3.6.1.1"
0x31, 0x2e, 0x33, 0x2e, 0x36, 0x2e, 0x31, 0x2e, 0x31
]
/**
* Represents an EXTENSION request to modify a password.
* Does not include a new password.
* Taken from https://web.archive.org/web/20220518193613/https://nawilson.com/ldapv3-wire-protocol-reference-extended/
*/
module.exports.extensionChangePasswordRequestBytes = [
0x30, 0x53, // start sequence, 83 bytes
0x02, 0x01, 0x01, // message ID (integer value 1)
0x77, 0x4e, // protocol op (0x77), 78 bytes
0x80, 0x17, // string (request name; "oid"), 23 bytes
// "1.3.6.1.4.1.4203.1.11.1"
0x31, 0x2e, 0x33, 0x2e, 0x36, 0x2e, 0x31, 0x2e, // The extended request OID
0x34, 0x2e, 0x31, 0x2e, 0x34, 0x32, 0x30, 0x33,
0x2e, 0x31, 0x2e, 0x31, 0x31, 0x2e, 0x31,
0x81, 0x33, // sequence (request value), 51 bytes
0x30, 0x31, // sequence, 49 bytes
0x80, 0x24, // extension specific string (entry), 36 bytes
// "uid=jdoe,ou=People,dc=example,dc=com"
0x75, 0x69, 0x64, 0x3d, 0x6a, 0x64, 0x6f, 0x65,
0x2c, 0x6f, 0x75, 0x3d, 0x50, 0x65, 0x6f, 0x70,
0x6c, 0x65, 0x2c, 0x64, 0x63, 0x3d, 0x65, 0x78,
0x61, 0x6d, 0x70, 0x6c, 0x65, 0x2c, 0x64, 0x63,
0x3d, 0x63, 0x6f, 0x6d,
0x81, 0x09, // extension specific string (value), 9 bytes
// "secret123"
0x73, 0x65, 0x63, 0x72,
0x65, 0x74, 0x31, 0x32,
0x33
]
/**
* Represents an EXTENSION request to modify a password.
* Only provides the new password.
*/
module.exports.extensionChangePasswordWithNewPasswordBytes = [
0x30, 0x2d, // start sequence, 45 bytes
0x02, 0x01, 0x01, // message ID (integer value 1)
0x77, 0x28, // protocol op (0x77), 40 bytes
0x80, 0x17, // string (request name; "oid"), 23 bytes
// "1.3.6.1.4.1.4203.1.11.1"
0x31, 0x2e, 0x33, 0x2e, 0x36, 0x2e, 0x31, 0x2e, // The extended request OID
0x34, 0x2e, 0x31, 0x2e, 0x34, 0x32, 0x30, 0x33,
0x2e, 0x31, 0x2e, 0x31, 0x31, 0x2e, 0x31,
0x81, 0x0d, // sequence (request value), 13 bytes
0x30, 0x0b, // sequence, 11 bytes
0x82, 0x09, // extension specific string (value), 9 bytes
// "secret123"
0x73, 0x65, 0x63, 0x72,
0x65, 0x74, 0x31, 0x32,
0x33
]
/**
* Represents an EXTENSION response that is an unsolicited response.
* Taken from https://web.archive.org/web/20220518193613/https://nawilson.com/ldapv3-wire-protocol-reference-extended/
*/
module.exports.extensionDisconnectionNotificationResponseBytes = [
0x30, 0x49, // start sequence, 73 bytes
0x02, 0x01, 0x00, // message ID (integer value 0)
0x78, 0x44, // protocol op (0x78), 68 bytes
0x0a, 0x01, 0x34, // unavailable result code (enumerated value 52)
0x04, 0x00, // no matched DN (0-byte octet string)
0x04, 0x25, // string (diagnostic message), 37 bytes
// "The Directory Server is shutting down"
0x54, 0x68, 0x65, 0x20, 0x44, 0x69, 0x72, 0x65,
0x63, 0x74, 0x6f, 0x72, 0x79, 0x20, 0x53, 0x65,
0x72, 0x76, 0x65, 0x72, 0x20, 0x69, 0x73, 0x20,
0x73, 0x68, 0x75, 0x74, 0x74, 0x69, 0x6e, 0x67,
0x20, 0x64, 0x6f, 0x77, 0x6e,
0x8a, 0x16, // string (oid)
// "1.3.6.1.4.1.1466.20036"
0x31, 0x2e, 0x33, 0x2e, 0x36, 0x2e, 0x31, 0x2e,
0x34, 0x2e, 0x31, 0x2e, 0x31, 0x34, 0x36, 0x36,
0x2e, 0x32, 0x30, 0x30, 0x33, 0x36
]
/**
* Represents an EXTENSION request for a Who Am I? request.
* Taken from https://www.rfc-editor.org/rfc/rfc4532#section-2.1
*/
module.exports.extensionWhoAmIRequestBytes = [
0x30, 0x1e, // start sequence, 30 bytes
0x02, 0x01, 0x02, // message id, 2
0x77, 0x19, // protocol op (0x77), 25 bytes
0x80, 0x17, // string (oid), 23 bytes
// "1.3.6.1.4.1.4203.1.11.3"
0x31, 0x2e, 0x33, 0x2e, 0x36, 0x2e, 0x31,
0x2e, 0x34, 0x2e, 0x31, 0x2e, 0x34, 0x32,
0x30, 0x33, 0x2e, 0x31, 0x2e, 0x31, 0x31,
0x2e, 0x33
]
/**
* Represents a CANCEL request as described in
* https://www.rfc-editor.org/rfc/rfc3909#section-2.1.
*/
module.exports.extensionCancelRequestBytes = [
0x30, 0x19, // start sequence, 25 bytes
0x02, 0x01, 0x02, // message id, 2
0x77, 0x14, // protocol op (0x77), 20 bytes
0x80, 0x0b, // string (oid), 11 bytes
0x31, 0x2e, 0x33, 0x2e, 0x36, 0x2e,
0x31, 0x2e, 0x31, 0x2e, 0x38,
0x81, 0x05, // value sequence, 5 bytes
0x30, 0x03, // sequence, 3 bytes
0x02, 0x01, 0x01 // message id 1
]
/**
* Represents a CANCEL response as described in
* https://www.rfc-editor.org/rfc/rfc3909#section-2.2.
*/
module.exports.extensionCancelResponseBytes = [
0x30, 0x0c, // sequence, 12 bytes
0x02, 0x01, 0x02, // message id (integer 2)
0x78, 0x07, // protocol op (0x78), 7 bytes
0x0a, 0x01, 0x76, // status code, 118 (canceled)
0x04, 0x00, // no matched dn
0x04, 0x00 // no diagnostic message
]
/**
* Represents a Start TLS request as described in
* https://www.rfc-editor.org/rfc/rfc4511.html#section-4.14.1.
*
* Taken from
* https://web.archive.org/web/20220518193613/https://nawilson.com/ldapv3-wire-protocol-reference-extended/
*/
module.exports.extenstionStartTLSRequestBytes = [
0x30, 0x1d, // sequence, 29 bytes
0x02, 0x01, 0x01, // message id (integer value 1)
0x77, 0x18, // protocol op (0x77), 24 bytes
0x80, 0x16, // name sequence, 22 bytes
// "1.3.6.1.4.1.1466.20037"
0x31, 0x2e, 0x33, 0x2e, 0x36, 0x2e, 0x31, 0x2e,
0x34, 0x2e, 0x31, 0x2e, 0x31, 0x34, 0x36, 0x36,
0x2e, 0x32, 0x30, 0x30, 0x33, 0x37
]
/**
* Represents a Start TLS response as described in
* https://www.rfc-editor.org/rfc/rfc4511.html#section-4.14.2.
*
* Taken from
* https://web.archive.org/web/20220518193613/https://nawilson.com/ldapv3-wire-protocol-reference-extended/
*/
module.exports.extensionStartTLSResponseBytes = [
0x30, 0x24, // sequence, 36 bytes
0x02, 0x01, 0x01, // message id (integer value 1)
0x78, 0x1f, // protocol op (0x78), 31 bytes
0x0a, 0x01, 0x00, // success result code (enumerated value 0)
0x04, 0x00, // no matched dn (0-byte octet string)
0x04, 0x00, // do diagnostic message (0-byte octet string)
0x8a, 0x16, // value string, 22 bytes
// "1.3.6.1.4.1.1466.20037"
0x31, 0x2e, 0x33, 0x2e, 0x36, 0x2e, 0x31, 0x2e,
0x34, 0x2e, 0x31, 0x2e, 0x31, 0x34, 0x36, 0x36,
0x2e, 0x32, 0x30, 0x30, 0x33, 0x37
]
/**
* Represents a MODIFY request.
* Taken from https://web.archive.org/web/20220518184303/https://nawilson.com/ldapv3-wire-protocol-reference-modify/.
*/
module.exports.modifyRequestBytes = [
0x30, 0x81, 0x80, // sequence start, 128 bytes
0x02, 0x01, 0x02, // message id (integer value 2)
0x66, 0x7b, // protocol op (0x66), 123 bytes
0x04, 0x24, // string, 36 bytes
// "uid=jdoe,ou=People,dc=example,dc=com"
0x75, 0x69, 0x64, 0x3d, 0x6a, 0x64, 0x6f, 0x65,
0x2c, 0x6f, 0x75, 0x3d, 0x50, 0x65, 0x6f, 0x70,
0x6c, 0x65, 0x2c, 0x64, 0x63, 0x3d, 0x65, 0x78,
0x61, 0x6d, 0x70, 0x6c, 0x65, 0x2c, 0x64, 0x63,
0x3d, 0x63, 0x6f, 0x6d,
0x30, 0x53, // start sequence (modifications), 83 bytes
0x30, 0x18, // sequence, 24 bytes
0x0a, 0x01, 0x01, // delete modification type (enumerated value 1)
0x30, 0x13, // sequence, 19 bytes
0x04, 0x09, // string, 9 bytes
// "givenName"
0x67, 0x69, 0x76, 0x65,
0x6e, 0x4e, 0x61, 0x6d,
0x65,
0x31, 0x06, // string, 6 bytes
// "John"
0x04, 0x04, 0x4a,
0x6f, 0x68, 0x6e,
0x30, 0x1c, // sequence, 28 bytes
0x0a, 0x01, 0x00, // add modification type (enumerated value 0)
0x30, 0x17, // sequence, 23 bytes
0x04, 0x09, // string, 9 bytes
// "givenName"
0x67, 0x69, 0x76, 0x65,
0x6e, 0x4e, 0x61, 0x6d,
0x65,
0x31, 0x0a, // string, 10 bytes
// "Jonathan"
0x04, 0x08, 0x4a, 0x6f, 0x6e,
0x61, 0x74, 0x68, 0x61, 0x6e,
0x30, 0x19, // sequence, 25 bytes
0x0a, 0x01, 0x02, // replace modification type (enumerated value 2)
0x30, 0x14, // sequence, 20 bytes
0x04, 0x02, // string, 2 bytes
// "cn"
0x63, 0x6e,
0x31, 0x0e, // string, 14 bytes
// "Jonathan Doe"
0x04, 0x0c, 0x4a, 0x6f,
0x6e, 0x61, 0x74, 0x68,
0x61, 0x6e, 0x20, 0x44,
0x6f, 0x65
]
/**
* Represents a MODIFY response.
* Taken from https://web.archive.org/web/20220518184303/https://nawilson.com/ldapv3-wire-protocol-reference-modify/.
*/
module.exports.modifyResponseBytes = [
0x30, 0x0c, // sequence, 12 bytes
0x02, 0x01, 0x02, // message id (integer value 2)
0x67, 0x07, // protocol op (0x67), 7 bytes
0x0a, 0x01, 0x00, // success result code (enumerated value 0)
0x04, 0x00, // no matched dn
0x04, 0x00 // no diagnostic message
]
/**
* Represents a MODIFYDN request.
* Taken from https://web.archive.org/web/20220630073359/https://nawilson.com/ldapv3-wire-protocol-reference-modify-dn/.
*/
module.exports.modifyDnRequestBytes = [
0x30, 0x58, // sequence, 88 bytes
0x02, 0x01, 0x02, // message id (integer value 2)
0x6c, 0x53, // protocol op (0x6c), 83 bytes
0x04, 0x24, // string, 36 bytes
// "uid=jdoe,ou=People,dc=example,dc=com"
0x75, 0x69, 0x64, 0x3d, 0x6a, 0x64, 0x6f, 0x65,
0x2c, 0x6f, 0x75, 0x3d, 0x50, 0x65, 0x6f, 0x70,
0x6c, 0x65, 0x2c, 0x64, 0x63, 0x3d, 0x65, 0x78,
0x61, 0x6d, 0x70, 0x6c, 0x65, 0x2c, 0x64, 0x63,
0x3d, 0x63, 0x6f, 0x6d,
0x04, 0x0c, // string, 12 bytes
// "uid=john.doe"
0x75, 0x69, 0x64, 0x3d,
0x6a, 0x6f, 0x68, 0x6e,
0x2e, 0x64, 0x6f, 0x65,
0x01, 0x01, 0xff, // Delete the old RDN value (boolean true)
0x80, 0x1a, // context specific string, 26 bytes
// "ou=Users,dc=example,dc=com"
0x6f, 0x75, 0x3d, 0x55, 0x73, 0x65, 0x72, 0x73,
0x2c, 0x64, 0x63, 0x3d, 0x65, 0x78, 0x61, 0x6d,
0x70, 0x6c, 0x65, 0x2c, 0x64, 0x63, 0x3d, 0x63,
0x6f, 0x6d
]
/**
* Represents a MODIFYDN response.
* Taken from https://web.archive.org/web/20220630073359/https://nawilson.com/ldapv3-wire-protocol-reference-modify-dn/.
*/
module.exports.modifyDnResponseBytes = [
0x30, 0x0c, // sequence, 12 bytes
0x02, 0x01, 0x02, // message id (integer value 2)
0x6d, 0x07, // protocol op (0x6d), 7 bytes
0x0a, 0x01, 0x00, // success result code (enumerated value 0)
0x04, 0x00, // no matched dn
0x04, 0x00 // no diagnostic message
]
/**
* Represents an UNBIND request.
* Taken from https://web.archive.org/web/20220518231541/https://nawilson.com/ldapv3-wire-protocol-reference-unbind/.
*/
module.exports.unbindRequestBytes = [
0x30, 0x05, // sequence, 5 bytes
0x02, 0x01, 0x03, // message id (integer value 3)
0x42, 0x00 // protocol op (0x42), 0 bytes
]
/**
* Represents a SEARCHREQUEST request.
* Taken from https://web.archive.org/web/20220518215838/https://nawilson.com/ldapv3-wire-protocol-reference-search/.
*/
module.exports.searchRequestBytes = [
0x30, 0x56, // sequence, 86 bytes
0x02, 0x01, 0x02, // message id (integer value 2)
0x63, 0x51, // protocol op, 81 bytes
0x04, 0x11, // string sequence, 17 bytes
// "dc=example,dc=com"
0x64, 0x63, 0x3d, 0x65,
0x78, 0x61, 0x6d, 0x70,
0x6c, 0x65, 0x2c, 0x64,
0x63, 0x3d, 0x63, 0x6f,
0x6d,
0x0a, 0x01, 0x02, // wholeSubtree scope (enumerated value 2)
0x0a, 0x01, 0x00, // neverDerefAliases (enumerated value 0)
0x02, 0x02, 0x03, 0xe8, // size limit (integer value 1000)
0x02, 0x01, 0x1e, // time limit (integer value 30)
0x01, 0x01, 0x00, // typesOnly (boolean false)
0xa0, 0x24, // begin AND filter, 36 bytes
0xa3, 0x15, // begin an EQUALITY filter, 21 bytes
0x04, 0x0b, // string, 11 bytes
// "objectClass"
0x6f, 0x62, 0x6a, 0x65,
0x63, 0x74, 0x43, 0x6c,
0x61, 0x73, 0x73,
0x04, 0x06, // string, 6 bytes
// "person"
0x70, 0x65, 0x72, 0x73,
0x6f, 0x6e, // The assertion value (octet string "person")
0xa3, 0x0b, // begin an EQUALITY filter, 11 bytes
0x04, 0x03, // string, 3 bytes
// "uid"
0x75, 0x69, 0x64,
0x04, 0x04, // string, 4 bytes
// "jdoe"
0x6a, 0x64, 0x6f, 0x65,
0x30, 0x06, // begin the set of requested attributes, 6 bytes
0x04, 0x01, // string, 1 byte
// "*"
0x2a,
0x04, 0x01, // string, 1 byte
// "+"
0x2b
]
/**
* Represents a SEARCHRESULTENTRY response.
* Taken from https://web.archive.org/web/20220518215838/https://nawilson.com/ldapv3-wire-protocol-reference-search/.
*/
module.exports.searchResultEntryBytes = [
0x30, 0x49, // sequence, 73 bytes
0x02, 0x01, 0x02, // message id (integer value 2)
0x64, 0x44, // protocol op (0x64), 68 bytes
0x04, 0x11, // string, 17 bytes
// "dc=example,dc=com"
0x64, 0x63, 0x3d, 0x65,
0x78, 0x61, 0x6d, 0x70,
0x6c, 0x65, 0x2c, 0x64,
0x63, 0x3d, 0x63, 0x6f,
0x6d,
0x30, 0x2f, // sequence, 47 bytes (attributes list)
0x30, 0x1c, // sequence, 28 bytes (first attribute)
0x04, 0x0b, // string, 11 bytes
// "objectClass"
0x6f, 0x62, 0x6a, 0x65,
0x63, 0x74, 0x43, 0x6c,
0x61, 0x73, 0x73,
0x31, 0x0d, // set sequence, 13 bytes
0x04, 0x03, // string, 3 bytes
// "top"
0x74, 0x6f, 0x70,
0x04, 0x06, // string, 6 bytes
// "domain"
0x64, 0x6f, 0x6d, 0x61, 0x69, 0x6e,
0x30, 0x0f, // sequence, 15 bytes (second attribute)
0x04, 0x02, // string, 2 bytes
// "dc"
0x64, 0x63,
0x31, 0x09, // set sequence, 9 bytes
0x04, 0x07, // string, 7 bytes
// "example"
0x65, 0x78, 0x61, 0x6d, 0x70, 0x6c, 0x65
]
/**
* Represents a SEARCHRESULTENTRY response that only returns attribute names.
* Taken from https://web.archive.org/web/20220518215838/https://nawilson.com/ldapv3-wire-protocol-reference-search/.
*/
module.exports.searchResultEntryNoValuesBytes = [
0x30, 0x33, // sequence, 51 bytes
0x02, 0x01, 0x02, // message id (integer value 2)
0x64, 0x2e, // protocol op (0x64), 46 bytes
0x04, 0x11, // string, 17 bytes
// "dc=example,dc=com"
0x64, 0x63, 0x3d, 0x65,
0x78, 0x61, 0x6d, 0x70,
0x6c, 0x65, 0x2c, 0x64,
0x63, 0x3d, 0x63, 0x6f,
0x6d,
0x30, 0x19, // sequence, 25 bytes (attributes list)
0x30, 0x0f, // sequence, 15 bytes (first attribute)
0x04, 0x0b, // string, 11 bytes
// "objectClass"
0x6f, 0x62, 0x6a, 0x65,
0x63, 0x74, 0x43, 0x6c,
0x61, 0x73, 0x73,
0x31, 0x00, // sequence, 0 bytes
0x30, 0x06, // sequence, 6 bytes (second attribute)
0x04, 0x02, // string, 2 bytes
// "dc"
0x64, 0x63,
0x31, 0x00 // sequence, 0 bytes
]
/**
* Represents a SEARCHRESULTREFERENCE response.
* Taken from https://web.archive.org/web/20220518215838/https://nawilson.com/ldapv3-wire-protocol-reference-search/.
*/
module.exports.searchResultReferenceBytes = [
0x30, 0x6d, // sequence, 109 bytes
0x02, 0x01, 0x02, // message id (integer value 2)
0x73, 0x68, // protocol op (0x73), 104 bytes
0x04, 0x32, // string, 50 bytes
// "ldap://ds1.example.com:389/dc=example,dc=com??sub?"
0x6c, 0x64, 0x61, 0x70, 0x3a, 0x2f, 0x2f, 0x64,
0x73, 0x31, 0x2e, 0x65, 0x78, 0x61, 0x6d, 0x70,
0x6c, 0x65, 0x2e, 0x63, 0x6f, 0x6d, 0x3a, 0x33,
0x38, 0x39, 0x2f, 0x64, 0x63, 0x3d, 0x65, 0x78,
0x61, 0x6d, 0x70, 0x6c, 0x65, 0x2c, 0x64, 0x63,
0x3d, 0x63, 0x6f, 0x6d, 0x3f, 0x3f, 0x73, 0x75,
0x62, 0x3f,
0x04, 0x32, // string, 50 bytes
// "ldap://ds2.example.com:389/dc=example,dc=com??sub?"
0x6c, 0x64, 0x61, 0x70, 0x3a, 0x2f, 0x2f, 0x64,
0x73, 0x32, 0x2e, 0x65, 0x78, 0x61, 0x6d, 0x70,
0x6c, 0x65, 0x2e, 0x63, 0x6f, 0x6d, 0x3a, 0x33,
0x38, 0x39, 0x2f, 0x64, 0x63, 0x3d, 0x65, 0x78,
0x61, 0x6d, 0x70, 0x6c, 0x65, 0x2c, 0x64, 0x63,
0x3d, 0x63, 0x6f, 0x6d, 0x3f, 0x3f, 0x73, 0x75,
0x62, 0x3f
]
/**
* Represents a SEARCHRESULTDONE response.
* Taken from https://web.archive.org/web/20220518215838/https://nawilson.com/ldapv3-wire-protocol-reference-search/.
*/
module.exports.searchResultDoneBytes = [
0x30, 0x0c, // sequence, 12 bytes
0x02, 0x01, 0x02, // message id (integer value 2)
0x65, 0x07, // protocol op (0x65), 7 bytes
0x0a, 0x01, 0x00, // success result code (enumerated value 0)
0x04, 0x00, // no matched DN (0-byte octet string)
0x04, 0x00 // no diagnostic message (0-byte octet string)
]
/**
* Represents an INTERMEDIATE response with a name and value.
*/
module.exports.intermediateResponseBytes = [
0x30, 0x11, // sequence, 17 bytes
0x02, 0x01, 0x02, // message id (integer value 2)
0x79, 0x0c, // protocol op (0x79), 12 bytes
0x80, 0x5, // string, 5 bytes
// "1.2.3"
0x31, 0x2e, 0x32, 0x2e, 0x33,
0x81, 0x03, // string, 3 bytes
// "foo"
0x66, 0x6f, 0x6f
]
/**
* Represents an INTERMEDIATE response with a name but no value.
*/
module.exports.intermediateResponseNoValueBytes = [
0x30, 0x0c, // sequence, 12 bytes
0x02, 0x01, 0x02, // message id (integer value 2)
0x79, 0x07, // protocol op (0x79), 12 bytes
0x80, 0x5, // string, 5 bytes
// "1.2.3"
0x31, 0x2e, 0x32, 0x2e, 0x33
]
/**
* Represents an INTERMEDIATE response with a value but no name.
*/
module.exports.intermediateResponseNoNameBytes = [
0x30, 0x0a, // sequence, 10 bytes
0x02, 0x01, 0x02, // message id (integer value 2)
0x79, 0x05, // protocol op (0x79), 5 bytes
0x81, 0x03, // string, 3 bytes
// "foo"
0x66, 0x6f, 0x6f
]

View File

@ -0,0 +1,104 @@
'use strict'
const LdapMessage = require('../ldap-message')
const Protocol = require('@ldapjs/protocol')
const warning = require('../deprecations')
/**
* Implements the abandon request message as described in
* https://www.rfc-editor.org/rfc/rfc4511.html#section-4.11
*/
class AbandonRequest extends LdapMessage {
#abandonId
/**
* @typedef {LdapMessageOptions} AbandonRequestOptions
* @property {number} [abandonId=0] The message id of the request to abandon.
*/
/**
* @param {AbandonRequestOptions} [options]
*/
constructor (options = {}) {
options.protocolOp = Protocol.operations.LDAP_REQ_ABANDON
super(options)
const abandonId = options.abandonId || options.abandonID || 0
if (options.abandonID) {
warning.emit('LDAP_MESSAGE_DEP_003')
}
this.#abandonId = abandonId
}
/**
* The identifier for the request that the instance will request be abandoned.
*
* @type {number}
*/
get abandonId () {
return this.#abandonId
}
/**
* Use {@link abandonId} instead.
*
* @deprecated
*/
get abandonID () {
warning.emit('LDAP_MESSAGE_DEP_003')
return this.#abandonId
}
/**
* The name of the request type.
*
* @type {string}
*/
get type () {
return 'AbandonRequest'
}
/**
* Internal use only.
*
* @param {import('@ldapjs/asn1').BerWriter} ber
*
* @returns {import('@ldapjs/asn1').BerWriter}
*/
_toBer (ber) {
ber.writeInt(this.#abandonId, Protocol.operations.LDAP_REQ_ABANDON)
return ber
}
/**
* Internal use only.
*
* @param {object}
*
* @returns {object}
*/
_pojo (obj = {}) {
obj.abandonId = this.#abandonId
return obj
}
/**
* Implements the standardized `parseToPojo` method.
*
* @see LdapMessage.parseToPojo
*
* @param {import('@ldapjs/asn1').BerReader} ber
*/
static parseToPojo (ber) {
const protocolOp = ber.peek()
if (protocolOp !== Protocol.operations.LDAP_REQ_ABANDON) {
const op = protocolOp.toString(16).padStart(2, '0')
throw Error(`found wrong protocol operation: 0x${op}`)
}
const abandonId = ber.readInt(Protocol.operations.LDAP_REQ_ABANDON)
return { protocolOp, abandonId }
}
}
module.exports = AbandonRequest

View File

@ -0,0 +1,125 @@
'use strict'
const tap = require('tap')
const { BerReader } = require('@ldapjs/asn1')
const warning = require('../deprecations')
const AbandonRequest = require('./abandon-request')
// Silence the standard warning logs. We will test the messages explicitly.
process.removeAllListeners('warning')
const { abandonRequestBytes } = require('./_fixtures/message-byte-arrays')
tap.test('basic', t => {
t.test('constructor no args', async t => {
const req = new AbandonRequest()
t.strictSame(req.pojo, {
messageId: 1,
protocolOp: 0x50,
type: 'AbandonRequest',
abandonId: 0,
controls: []
})
})
t.test('emits warning for abandonID', t => {
process.on('warning', handler)
t.teardown(async () => {
process.removeListener('warning', handler)
warning.emitted.set('LDAP_MESSAGE_DEP_003', false)
})
const req = new AbandonRequest({
abandonID: 1
})
t.ok(req)
function handler (error) {
t.equal(
error.message,
'abandonID is deprecated. Use abandonId instead.'
)
t.end()
}
})
t.test('properties return correct values', t => {
process.on('warning', handler)
t.teardown(async () => {
process.removeListener('warning', handler)
warning.emitted.set('LDAP_MESSAGE_DEP_003', false)
})
const req = new AbandonRequest({
abandonId: 1
})
t.equal(req.abandonId, 1)
t.equal(req.abandonID, 1)
function handler (error) {
t.equal(
error.message,
'abandonID is deprecated. Use abandonId instead.'
)
t.end()
}
})
t.end()
})
tap.test('toBer', t => {
t.test('converts AbandonRequest to BER', async t => {
const reqBuffer = Buffer.from(abandonRequestBytes)
const reader = new BerReader(reqBuffer)
const message = AbandonRequest.parse(reader)
const ber = message.toBer()
t.equal('[object BerReader]', Object.prototype.toString.call(ber))
t.equal(reqBuffer.compare(ber.buffer), 0)
})
t.end()
})
tap.test('_pojo', t => {
t.test('returns implementation properties', async t => {
const message = new AbandonRequest()
const pojo = message._pojo()
t.strictSame(pojo, { abandonId: 0 })
})
t.end()
})
tap.test('#parseToPojo', t => {
t.test('throws if operation incorrect', async t => {
const reqBuffer = Buffer.from(abandonRequestBytes)
reqBuffer[5] = 0x61
const reader = new BerReader(reqBuffer)
reader.readSequence()
reader.readInt()
t.throws(
() => AbandonRequest.parseToPojo(reader),
Error('found wrong protocol operation: 0x61')
)
})
t.test('returns a pojo representation', async t => {
const reqBuffer = Buffer.from(abandonRequestBytes)
const reader = new BerReader(reqBuffer)
reader.readSequence()
reader.readInt()
const pojo = AbandonRequest.parseToPojo(reader)
t.strictSame(pojo, {
protocolOp: 0x50,
abandonId: 5
})
})
t.end()
})

View File

@ -0,0 +1,43 @@
'use strict'
const LdapResult = require('../ldap-result')
/**
* Implements the ldapjs specific ABANDON response object.
*/
class AbandonResponse extends LdapResult {
/**
* @param {LdapResultOptions} options
*/
constructor (options = {}) {
options.protocolOp = 0x00
super(options)
}
/**
* The name of the request type.
*
* @type {string}
*/
get type () {
return 'AbandonResponse'
}
/**
* Implements the standardized `parseToPojo` method.
*
* @see LdapMessage.parseToPojo
*
* @param {import('@ldapjs/asn1').BerReader} ber
*
* @returns {object}
*/
static parseToPojo (ber) {
return LdapResult._parseToPojo({
opCode: 0x00,
berReader: ber
})
}
}
module.exports = AbandonResponse

View File

@ -0,0 +1,53 @@
'use strict'
const tap = require('tap')
const { BerReader } = require('@ldapjs/asn1')
const {
abandonResponseBytes
} = require('./_fixtures/message-byte-arrays')
const AbandonResponse = require('./abandon-response')
tap.test('basic', async t => {
const res = new AbandonResponse()
t.equal(res.protocolOp, 0x00)
t.equal(res.type, 'AbandonResponse')
})
tap.test('toBer', t => {
tap.test('returns basic bytes', async t => {
const res = new AbandonResponse({ messageId: 1 })
const ber = res.toBer()
const expected = Buffer.from(abandonResponseBytes)
t.equal(expected.compare(ber.buffer), 0)
})
t.end()
})
tap.test('#parseToPojo', t => {
t.test('parses a basic object', async t => {
const bytes = abandonResponseBytes.slice(5)
const reader = new BerReader(Buffer.from(bytes))
const pojo = AbandonResponse.parseToPojo(reader)
t.strictSame(pojo, {
status: 0,
matchedDN: '',
diagnosticMessage: '',
referrals: []
})
})
t.test('throws if protocol op is wrong', async t => {
const bytes = abandonResponseBytes.slice(5)
bytes[0] = 0x68
const reader = new BerReader(Buffer.from(bytes))
t.throws(
() => AbandonResponse.parseToPojo(reader),
Error('found wrong protocol operation: 0x68')
)
})
t.comment('see the LdapResult test suite for further tests')
t.end()
})

View File

@ -0,0 +1,277 @@
'use strict'
const LdapMessage = require('../ldap-message')
const Attribute = require('@ldapjs/attribute')
const Protocol = require('@ldapjs/protocol')
const { DN } = require('@ldapjs/dn')
/**
* Implements the add request message as described in
* https://www.rfc-editor.org/rfc/rfc4511.html#section-4.7
*/
class AddRequest extends LdapMessage {
/**
* Path to the LDAP object.
*
* @type {null | import('@ldapjs/dn').DN}
*/
#entry
/**
* A set of attribute objects.
*
* @type {import('@ldapjs/attribute')[]}
*/
#attributes = []
/**
* @typedef {LdapMessageOptions} AddRequestOptions
* @property {string} [entry=null] The path to the LDAP object.
* @property {import('@ldapjs/attribute')[]} [attributes=[]] A set of
* attributes to store at the `entry` path.
*/
/**
* @param {AddRequestOptions} [options]
*
* @throws When the provided attributes list is invalid.
*/
constructor (options = {}) {
options.protocolOp = Protocol.operations.LDAP_REQ_ADD
super(options)
this.entry = options.entry || null
this.attributes = options.attributes || []
}
/**
* Get a copy of the attributes associated with the request.
*
* @returns {import('@ldapjs/attribute')[]}
*/
get attributes () {
return this.#attributes.slice(0)
}
/**
* Set the attributes to be added to the entry. Replaces any existing
* attributes.
*
* @param {import('@ldapjs/attribute')[]} attrs
*
* @throws If the input is not an array, or any element is not an
* {@link Attribute} or attribute-like object.
*/
set attributes (attrs) {
if (Array.isArray(attrs) === false) {
throw Error('attrs must be an array')
}
const newAttrs = []
for (const attr of attrs) {
if (Attribute.isAttribute(attr) === false) {
throw Error('attr must be an Attribute instance or Attribute-like object')
}
if (Object.prototype.toString.call(attr) !== '[object LdapAttribute]') {
newAttrs.push(new Attribute(attr))
continue
}
newAttrs.push(attr)
}
this.#attributes = newAttrs
}
/**
* The directory path to the object to add.
*
* @type {string}
*/
get entry () {
return this.#entry ?? null
}
/**
* Define the entry path to the LDAP object.
*
* @param {string | import('@ldapjs/dn').DN} path
*/
set entry (path) {
if (path === null) return
if (typeof path === 'string') {
this.#entry = DN.fromString(path)
} else if (Object.prototype.toString.call(path) === '[object LdapDn]') {
this.#entry = path
} else {
throw Error('entry must be a valid DN string or instance of LdapDn')
}
}
/**
* Alias of {@link entry}.
*
* @type {string}
*/
get _dn () {
return this.entry
}
/**
* The name of the request type.
*
* @type {string}
*/
get type () {
return 'AddRequest'
}
/**
* Add a new {@link Attribute} to the list of request attributes.
*
* @param {import('@ldapjs/attribute')} attr
*
* @throws When the input is not an {@link Attribute} instance.
*/
addAttribute (attr) {
if (Object.prototype.toString.call(attr) !== '[object LdapAttribute]') {
throw Error('attr must be an instance of Attribute')
}
this.#attributes.push(attr)
}
/**
* Get the list of attribute names for the attributes in the
* request.
*
* @returns {string[]}
*/
attributeNames () {
return this.#attributes.map(attr => attr.type)
}
/**
* Retrieve an attribute by name from the attributes associated with
* the request.
*
* @param {string} attributeName
*
* @returns {import('@ldapjs/attribute')|null}
*
* @throws When `attributeName` is not a string.
*/
getAttribute (attributeName) {
if (typeof attributeName !== 'string') {
throw Error('attributeName must be a string')
}
for (const attr of this.#attributes) {
if (attr.type === attributeName) {
return attr
}
}
return null
}
/**
* Find the index of an {@link Attribute} in the request's
* attribute set.
*
* @param {string} attributeName
*
* @returns {number} The index of the attribute, or `-1` if not
* found.
*
* @throws When `attributeName` is not a string.
*/
indexOf (attributeName) {
if (typeof attributeName !== 'string') {
throw Error('attributeName must be a string')
}
for (let i = 0; i < this.#attributes.length; i += 1) {
if (this.#attributes[i].type === attributeName) {
return i
}
}
return -1
}
/**
* Internal use only.
*
* @param {import('@ldapjs/asn1').BerWriter} ber
*
* @returns {import('@ldapjs/asn1').BerWriter}
*/
_toBer (ber) {
ber.startSequence(Protocol.operations.LDAP_REQ_ADD)
ber.writeString(this.#entry.toString())
ber.startSequence()
for (const attr of this.#attributes) {
const attrBer = attr.toBer()
ber.appendBuffer(attrBer.buffer)
}
ber.endSequence()
ber.endSequence()
return ber
}
/**
* Internal use only.
*
* @param {object}
*
* @returns {object}
*/
_pojo (obj = {}) {
obj.entry = this.#entry ? this.#entry.toString() : null
obj.attributes = []
for (const attr of this.#attributes) {
obj.attributes.push(attr.pojo)
}
return obj
}
/**
* Implements the standardized `parseToPojo` method.
*
* @see LdapMessage.parseToPojo
*
* @param {import('@ldapjs/asn1').BerReader} ber
*
* @returns {object}
*/
static parseToPojo (ber) {
const protocolOp = ber.readSequence()
if (protocolOp !== Protocol.operations.LDAP_REQ_ADD) {
const op = protocolOp.toString(16).padStart(2, '0')
throw Error(`found wrong protocol operation: 0x${op}`)
}
const entry = ber.readString()
const attributes = []
// Advance to the first attribute sequence in the set
// of attribute sequences.
ber.readSequence()
const endOfAttributesPos = ber.offset + ber.length
while (ber.offset < endOfAttributesPos) {
const attribute = Attribute.fromBer(ber)
attribute.type = attribute.type.toLowerCase()
if (attribute.type === 'objectclass') {
for (let i = 0; i < attribute.values.length; i++) {
attribute.values[i] = attribute.values[i].toLowerCase()
}
}
attributes.push(attribute)
}
return { protocolOp, entry, attributes }
}
}
module.exports = AddRequest

View File

@ -0,0 +1,316 @@
'use strict'
const tap = require('tap')
const Attribute = require('@ldapjs/attribute')
const { BerReader, BerWriter } = require('@ldapjs/asn1')
const { DN } = require('@ldapjs/dn')
const AddRequest = require('./add-request')
const { addRequestBytes } = require('./_fixtures/message-byte-arrays')
tap.test('basic', t => {
t.test('constructor no args', async t => {
const req = new AddRequest()
t.strictSame(req.pojo, {
messageId: 1,
protocolOp: 0x68,
type: 'AddRequest',
entry: null,
attributes: [],
controls: []
})
})
t.test('constructor with args', async t => {
const req = new AddRequest({
entry: 'dn=foo,dc=example,dc=com',
attributes: [{
type: 'cn',
values: ['foo']
}]
})
t.strictSame(req.pojo, {
messageId: 1,
protocolOp: 0x68,
type: 'AddRequest',
entry: 'dn=foo,dc=example,dc=com',
attributes: [{
type: 'cn',
values: ['foo']
}],
controls: []
})
})
t.end()
})
tap.test('.attributes', t => {
t.test('returns a copy of the attributes', async t => {
const inputAttrs = [{ type: 'cn', values: ['foo'] }]
const req = new AddRequest({
entry: 'cn=foo',
attributes: inputAttrs
})
const attrs = req.attributes
t.not(attrs, inputAttrs)
t.equal(attrs.length, inputAttrs.length)
})
t.test('replaces attributes list', async t => {
const req = new AddRequest({
entry: 'cn=foo',
attributes: [new Attribute({ type: 'cn', values: ['foo'] })]
})
t.strictSame(req.attributes[0].pojo, { type: 'cn', values: ['foo'] })
req.attributes = [new Attribute({ type: 'sn', values: ['bar'] })]
t.strictSame(req.attributes[0].pojo, { type: 'sn', values: ['bar'] })
})
t.test('throws if not an array', async t => {
const input = { type: 'cn', values: ['foo'] }
t.throws(
() => new AddRequest({ entry: 'cn=foo', attributes: input }),
Error('attrs must be an array')
)
})
t.test('throws if attribute is invalid', async t => {
const input = [{ type: 42, values: ['foo'] }]
t.throws(
() => new AddRequest({ entry: 'cn=foo', attributes: input }),
Error('attr must be an Attribute instance or Attribute-like object')
)
})
t.end()
})
tap.test('.entry', t => {
t.test('gets and sets', async t => {
let req = new AddRequest({ entry: 'cn=foo' })
t.equal(req.entry.toString(), 'cn=foo')
t.equal(req._dn.toString(), 'cn=foo')
req.entry = 'sn=bar'
t.equal(req.entry.toString(), 'sn=bar')
t.equal(req._dn.toString(), 'sn=bar')
req.entry = DN.fromString('cn=baz')
t.equal(req.entry.toString(), 'cn=baz')
req = new AddRequest()
t.equal(req.entry, null)
})
t.test('throws for bad value', async t => {
const req = new AddRequest()
t.throws(
() => {
req.entry = { cn: 'foo' }
},
'entry must be a valid DN string or instance of LdapDn'
)
})
t.end()
})
tap.test('addAttribute', t => {
t.test('throws for invalid input', async t => {
const req = new AddRequest({ entry: 'cn=foo' })
t.throws(
() => req.addAttribute({ type: 'foo', values: ['foo'] }),
Error('attr must be an instance of Attribute')
)
})
t.test('adds an attribute', async t => {
const req = new AddRequest({ entry: 'cn=foo' })
req.addAttribute(new Attribute({ type: 'bar', values: ['baz'] }))
t.strictSame(req.pojo, {
messageId: 1,
protocolOp: 0x68,
type: 'AddRequest',
entry: 'cn=foo',
attributes: [{
type: 'bar',
values: ['baz']
}],
controls: []
})
})
t.end()
})
tap.test('attributeNames', t => {
t.test('returns the names list', async t => {
const req = new AddRequest({
entry: 'cn=foo',
attributes: [
new Attribute({ type: 'bar' }),
new Attribute({ type: 'baz' }),
new Attribute({ type: 'foobar' }),
new Attribute({ type: 'barfoo' })
]
})
t.strictSame(req.attributeNames(), [
'bar',
'baz',
'foobar',
'barfoo'
])
})
t.end()
})
tap.test('getAttribute', t => {
t.test('throws for invalid parameter', async t => {
const req = new AddRequest({ entry: 'cn=foo' })
t.throws(
() => req.getAttribute(42),
Error('attributeName must be a string')
)
})
t.test('returns null for not found', async t => {
const req = new AddRequest({ entry: 'cn=foo' })
t.equal(req.getAttribute('bar'), null)
})
t.test('returns the correct attribute', async t => {
const req = new AddRequest({
entry: 'cn=foo',
attributes: [
new Attribute({ type: 'bar' }),
new Attribute({ type: 'baz', values: ['baz', 'baz', 'baz'] }),
new Attribute({ type: 'foobar' }),
new Attribute({ type: 'barfoo' })
]
})
const attr = req.getAttribute('baz')
t.strictSame(attr.pojo, {
type: 'baz',
values: ['baz', 'baz', 'baz']
})
})
t.end()
})
tap.test('indexOf', t => {
t.test('throws for invalid parameter', async t => {
const req = new AddRequest({ entry: 'cn=foo' })
t.throws(
() => req.indexOf(42),
Error('attributeName must be a string')
)
})
t.test('finds an attribute', async t => {
const req = new AddRequest({
entry: 'cn=foo',
attributes: [
new Attribute({ type: 'bar' }),
new Attribute({ type: 'baz' }),
new Attribute({ type: 'foobar' }),
new Attribute({ type: 'barfoo' })
]
})
t.equal(req.indexOf('foobar'), 2)
})
t.test('returns for not found', async t => {
const req = new AddRequest({
entry: 'cn=foo',
attributes: [
new Attribute({ type: 'bar' }),
new Attribute({ type: 'baz' }),
new Attribute({ type: 'foobar' }),
new Attribute({ type: 'barfoo' })
]
})
t.equal(req.indexOf('zifbang'), -1)
})
t.end()
})
tap.test('_toBer', t => {
tap.test('converts instance to BER', async t => {
const req = new AddRequest({
entry: 'dc=example,dc=com',
attributes: [
new Attribute({ type: 'objectclass', values: ['top', 'domain'] }),
new Attribute({ type: 'dc', values: ['example'] })
]
})
const writer = new BerWriter()
req._toBer(writer)
t.equal(
Buffer.from(addRequestBytes.slice(5)).compare(writer.buffer),
0
)
})
t.end()
})
tap.test('_pojo', t => {
t.test('returns a pojo representation', async t => {
const req = new AddRequest({
entry: 'cn=foo',
attributes: [{ type: 'bar', values: ['baz'] }]
})
t.strictSame(req._pojo(), {
entry: 'cn=foo',
attributes: [{
type: 'bar',
values: ['baz']
}]
})
})
t.end()
})
tap.test('#parseToPojo', t => {
t.test('throws if operation incorrect', async t => {
const reqBuffer = Buffer.from(addRequestBytes)
reqBuffer[5] = 0x61
const reader = new BerReader(reqBuffer)
reader.readSequence()
reader.readInt()
t.throws(
() => AddRequest.parseToPojo(reader),
Error('found wrong protocol operation: 0x61')
)
})
t.test('returns a pojo representation', async t => {
const reqBuffer = Buffer.from(addRequestBytes)
const reader = new BerReader(reqBuffer)
reader.readSequence()
reader.readInt()
const pojo = AddRequest.parseToPojo(reader)
t.equal(pojo.protocolOp, 0x68)
t.equal(pojo.entry, 'dc=example,dc=com')
t.strictSame(pojo.attributes[0].pojo, {
type: 'objectclass',
values: ['top', 'domain']
})
t.strictSame(pojo.attributes[1].pojo, {
type: 'dc',
values: ['example']
})
})
t.end()
})

View File

@ -0,0 +1,45 @@
'use strict'
const LdapResult = require('../ldap-result')
const { operations } = require('@ldapjs/protocol')
/**
* Implements the add response message as described in
* https://www.rfc-editor.org/rfc/rfc4511.html#section-4.7
*/
class AddResponse extends LdapResult {
/**
* @param {LdapResultOptions} options
*/
constructor (options = {}) {
options.protocolOp = operations.LDAP_RES_ADD
super(options)
}
/**
* The name of the request type.
*
* @type {string}
*/
get type () {
return 'AddResponse'
}
/**
* Implements the standardized `parseToPojo` method.
*
* @see LdapMessage.parseToPojo
*
* @param {import('@ldapjs/asn1').BerReader} ber
*
* @returns {object}
*/
static parseToPojo (ber) {
return LdapResult._parseToPojo({
opCode: operations.LDAP_RES_ADD,
berReader: ber
})
}
}
module.exports = AddResponse

View File

@ -0,0 +1,56 @@
'use strict'
const tap = require('tap')
const { BerReader } = require('@ldapjs/asn1')
const { operations } = require('@ldapjs/protocol')
const {
addResponseBasicBytes
} = require('./_fixtures/message-byte-arrays')
const AddResponse = require('./add-response')
tap.test('basic', async t => {
const res = new AddResponse()
t.equal(res.protocolOp, operations.LDAP_RES_ADD)
t.equal(res.type, 'AddResponse')
})
tap.test('toBer', t => {
tap.test('returns basic bytes', async t => {
const res = new AddResponse({ messageId: 2 })
const ber = res.toBer()
const expected = Buffer.from(addResponseBasicBytes)
t.equal(expected.compare(ber.buffer), 0)
})
t.comment('see the LdapResult test suite for further tests')
t.end()
})
tap.test('#parseToPojo', t => {
t.test('parses a basic object', async t => {
const bytes = addResponseBasicBytes.slice(5)
const reader = new BerReader(Buffer.from(bytes))
const pojo = AddResponse.parseToPojo(reader)
t.strictSame(pojo, {
status: 0,
matchedDN: '',
diagnosticMessage: '',
referrals: []
})
})
t.test('throws if protocol op is wrong', async t => {
const bytes = addResponseBasicBytes.slice(5)
bytes[0] = 0x68
const reader = new BerReader(Buffer.from(bytes))
t.throws(
() => AddResponse.parseToPojo(reader),
Error('found wrong protocol operation: 0x68')
)
})
t.comment('see the LdapResult test suite for further tests')
t.end()
})

View File

@ -0,0 +1,167 @@
'use strict'
const LdapMessage = require('../ldap-message')
const Protocol = require('@ldapjs/protocol')
const { BerTypes } = require('@ldapjs/asn1')
/**
* Implements the bind request message as described in
* https://www.rfc-editor.org/rfc/rfc4511.html#section-4.2.
*
* The bind request is further defined by:
* https://www.rfc-editor.org/rfc/rfc4513#section-5.
*/
class BindRequest extends LdapMessage {
static SIMPLE_BIND = 'simple'
static SASL_BIND = 'sasl'
#version = 0x03
#name
#authentication = BindRequest.SIMPLE_BIND
#credentials = ''
/**
* @typedef {LdapMessageOptions} BindRequestOptions
* @property {number} [version=3] Version of the protocol being used.
* @property {string} [name=null] The "username" (dn) to connect with.
* @property {string} [authentication='simple'] The authentication
* mechanism to use. Currently, only `simple` is supported.
* @property {string} [credentials=''] The password to use.
*/
/**
* @param {BindRequestOptions} [options]
*/
constructor (options = {}) {
options.protocolOp = Protocol.operations.LDAP_REQ_BIND
super(options)
const {
version = 0x03,
name = null,
authentication = BindRequest.SIMPLE_BIND,
credentials = ''
} = options
this.#version = version
this.#name = name
this.#authentication = authentication
this.#credentials = credentials
}
/**
* The authentication credentials for the request.
*
* @returns {string}
*/
get credentials () {
return this.#credentials
}
/**
* The DN, or "username", that is to be used in the bind request.
*
* @type {string}
*/
get name () {
return this.#name
}
/**
* The name of the request type.
*
* @type {string}
*/
get type () {
return 'BindRequest'
}
/**
* The version number that the bind request conforms to.
*
* @type {number}
*/
get version () {
return this.#version
}
/**
* Use {@link name} instead.
*
* @type {string}
*/
get _dn () {
return this.#name
}
/**
* Internal use only.
*
* @param {import('@ldapjs/asn1').BerWriter} ber
*
* @returns {import('@ldapjs/asn1').BerWriter}
*/
_toBer (ber) {
ber.startSequence(Protocol.operations.LDAP_REQ_BIND)
ber.writeInt(this.#version)
ber.writeString(this.#name || '')
// TODO add support for SASL et al
ber.writeString(this.#credentials || '', BerTypes.Context)
ber.endSequence()
return ber
}
/**
* Internal use only.
*
* @param {object}
*
* @returns {object}
*/
_pojo (obj = {}) {
obj.version = this.#version
obj.name = this.#name
obj.authenticationType = this.#authentication
obj.credentials = this.#credentials
return obj
}
/**
* Implements the standardized `parseToPojo` method.
*
* @see LdapMessage.parseToPojo
*
* @param {import('@ldapjs/asn1').BerReader} ber
*/
static parseToPojo (ber) {
const protocolOp = ber.readSequence()
if (protocolOp !== Protocol.operations.LDAP_REQ_BIND) {
const op = protocolOp.toString(16).padStart(2, '0')
throw Error(`found wrong protocol operation: 0x${op}`)
}
const version = ber.readInt()
const name = ber.readString()
const tag = ber.peek()
// TODO: add support for SASL et al
if (tag !== BerTypes.Context) {
// Currently only support 0x80. To support SASL, must support 0x83.
const authType = tag.toString(16).padStart(2, '0')
throw Error(`authentication 0x${authType} not supported`)
}
const authentication = BindRequest.SIMPLE_BIND
const credentials = ber.readString(BerTypes.Context)
return {
protocolOp,
version,
name,
authentication,
credentials
}
}
}
module.exports = BindRequest

View File

@ -0,0 +1,128 @@
'use strict'
const tap = require('tap')
const { BerReader } = require('@ldapjs/asn1')
const BindRequest = require('./bind-request')
const {
bindRequestBytes,
bindRequestAnonymousBytes
} = require('./_fixtures/message-byte-arrays')
tap.test('basic', t => {
t.test('constructor no args', async t => {
const req = new BindRequest()
t.strictSame(req.pojo, {
messageId: 1,
protocolOp: 0x60,
type: 'BindRequest',
version: 3,
name: null,
authenticationType: 'simple',
credentials: '',
controls: []
})
})
t.test('properties return correct values', async t => {
const req = new BindRequest({
messageId: 1,
version: 999,
name: 'foobar',
authentication: 'nonsense',
credentials: 'secret'
})
t.equal(req.credentials, 'secret')
t.equal(req.name, 'foobar')
t.equal(req.type, 'BindRequest')
t.equal(req.version, 999)
t.equal(req._dn, 'foobar')
})
t.end()
})
tap.test('toBer', t => {
t.test('converts BindRequest to BER', async t => {
const reqBuffer = Buffer.from(bindRequestBytes)
const reader = new BerReader(reqBuffer)
const message = BindRequest.parse(reader)
const ber = message.toBer()
t.equal('[object BerReader]', Object.prototype.toString.call(ber))
t.equal(reqBuffer.compare(ber.buffer), 0)
})
t.test('converts an anonymous bind request', async t => {
const reqBuffer = Buffer.from(bindRequestAnonymousBytes)
const message = new BindRequest()
const ber = message.toBer()
t.equal(reqBuffer.compare(ber.buffer), 0)
})
t.end()
})
tap.test('_pojo', t => {
t.test('returns implementation properties', async t => {
const message = new BindRequest()
const pojo = message._pojo()
t.strictSame(pojo, {
version: 3,
name: null,
authenticationType: 'simple',
credentials: ''
})
})
t.end()
})
tap.test('#parseToPojo', t => {
t.test('throws if operation incorrect', async t => {
const reqBuffer = Buffer.from(bindRequestBytes)
reqBuffer[5] = 0x61
const reader = new BerReader(reqBuffer)
reader.readSequence()
reader.readInt()
t.throws(
() => BindRequest.parseToPojo(reader),
Error('found wrong protocol operation: 0x61')
)
})
t.test('throws if simple auth credentials is tagged wrong', async t => {
const reqBuffer = Buffer.from(bindRequestBytes)
reqBuffer[31] = 0x83
const reader = new BerReader(reqBuffer)
reader.readSequence()
reader.readInt()
t.throws(
() => BindRequest.parseToPojo(reader),
Error('authentication 0x83 not supported')
)
})
t.test('returns a pojo representation', async t => {
const reqBuffer = Buffer.from(bindRequestBytes)
const reader = new BerReader(reqBuffer)
reader.readSequence()
reader.readInt()
const pojo = BindRequest.parseToPojo(reader)
t.strictSame(pojo, {
protocolOp: 0x60,
version: 3,
name: 'uid=admin,ou=system',
authentication: 'simple',
credentials: 'secret'
})
})
t.end()
})

View File

@ -0,0 +1,45 @@
'use strict'
const LdapResult = require('../ldap-result')
const { operations } = require('@ldapjs/protocol')
/**
* Implements the bind response message as described in
* https://www.rfc-editor.org/rfc/rfc4511.html#section-4.2.2
*/
class BindResponse extends LdapResult {
/**
* @param {LdapResultOptions} options
*/
constructor (options = {}) {
options.protocolOp = operations.LDAP_RES_BIND
super(options)
}
/**
* The name of the request type.
*
* @type {string}
*/
get type () {
return 'BindResponse'
}
/**
* Implements the standardized `parseToPojo` method.
*
* @see LdapMessage.parseToPojo
*
* @param {import('@ldapjs/asn1').BerReader} ber
*
* @returns {object}
*/
static parseToPojo (ber) {
return LdapResult._parseToPojo({
opCode: operations.LDAP_RES_BIND,
berReader: ber
})
}
}
module.exports = BindResponse

View File

@ -0,0 +1,54 @@
'use strict'
const tap = require('tap')
const { BerReader } = require('@ldapjs/asn1')
const { operations } = require('@ldapjs/protocol')
const {
bindResponseBytes
} = require('./_fixtures/message-byte-arrays')
const BindResponse = require('./bind-response')
tap.test('basic', async t => {
const res = new BindResponse()
t.equal(res.protocolOp, operations.LDAP_RES_BIND)
t.equal(res.type, 'BindResponse')
})
tap.test('toBer', t => {
tap.test('returns basic bytes', async t => {
const res = new BindResponse({ messageId: 1 })
const ber = res.toBer()
const expected = Buffer.from(bindResponseBytes)
t.equal(expected.compare(ber.buffer), 0)
})
t.end()
})
tap.test('#parseToPojo', t => {
t.test('parses a basic object', async t => {
const bytes = bindResponseBytes.slice(5)
const reader = new BerReader(Buffer.from(bytes))
const pojo = BindResponse.parseToPojo(reader)
t.strictSame(pojo, {
status: 0,
matchedDN: '',
diagnosticMessage: '',
referrals: []
})
})
t.test('throws if protocol op is wrong', async t => {
const bytes = bindResponseBytes.slice(5)
bytes[0] = 0x68
const reader = new BerReader(Buffer.from(bytes))
t.throws(
() => BindResponse.parseToPojo(reader),
Error('found wrong protocol operation: 0x68')
)
})
t.comment('see the LdapResult test suite for further tests')
t.end()
})

View File

@ -0,0 +1,173 @@
'use strict'
const { operations } = require('@ldapjs/protocol')
const { DN } = require('@ldapjs/dn')
const LdapMessage = require('../ldap-message')
/**
* Implements the compare request message as described in
* https://www.rfc-editor.org/rfc/rfc4511.html#section-4.10.
*/
class CompareRequest extends LdapMessage {
#attribute
#entry
#value
/**
* @typedef {LdapMessageOptions} CompareRequestOptions
* @property {string|null} [attribute] The attribute name to compare
* against.
* @property {string} [entry] The target LDAP entity whose attribute
* will be compared.
* @property {string} [value] The value of the attribute to compare.
*/
/**
* @param {CompareRequestOptions} [options]
*/
constructor (options = {}) {
options.protocolOp = operations.LDAP_REQ_COMPARE
super(options)
this.attribute = options.attribute || ''
this.entry = options.entry || null
this.value = options.value || ''
}
/**
* The property of an LDAP entry to compare against.
*
* @returns {string}
*/
get attribute () {
return this.#attribute
}
/**
* Define the LDAP entry property to compare against.
*
* @param {string} value
*/
set attribute (value) {
this.#attribute = value
}
/**
* The LDAP entry that will be inspected.
*
* @returns {string | null}
*/
get entry () {
return this.#entry ?? null
}
/**
* Define the LDAP entity to inspect.
*
* @param {string | null} value
*/
set entry (value) {
if (value === null) return
if (typeof value === 'string') {
this.#entry = DN.fromString(value)
} else if (Object.prototype.toString.call(value) === '[object LdapDn]') {
this.#entry = value
} else {
throw Error('entry must be a valid DN string or instance of LdapDn')
}
}
/**
* The name of the request type.
*
* @type {string}
*/
get type () {
return 'CompareRequest'
}
/**
* The value the attribute should be set to.
*
* @returns {string}
*/
get value () {
return this.#value
}
/**
* Define the value the attribute should match.
*
* @param {string} value
*/
set value (value) {
this.#value = value
}
get _dn () {
return this.#entry
}
/**
* Internal use only.
*
* @param {import('@ldapjs/asn1').BerWriter} ber
*
* @returns {import('@ldapjs/asn1').BerWriter}
*/
_toBer (ber) {
ber.startSequence(operations.LDAP_REQ_COMPARE)
ber.writeString(this.#entry.toString())
ber.startSequence()
ber.writeString(this.#attribute)
ber.writeString(this.#value)
ber.endSequence()
ber.endSequence()
return ber
}
/**
* Internal use only.
*
* @param {object}
*
* @returns {object}
*/
_pojo (obj = {}) {
obj.attribute = this.#attribute
obj.entry = this.#entry ? this.#entry.toString() : null
obj.value = this.#value
return obj
}
/**
* Implements the standardized `parseToPojo` method.
*
* @see LdapMessage.parseToPojo
*
* @param {import('@ldapjs/asn1').BerReader} ber
*/
static parseToPojo (ber) {
const protocolOp = ber.readSequence()
if (protocolOp !== operations.LDAP_REQ_COMPARE) {
const op = protocolOp.toString(16).padStart(2, '0')
throw Error(`found wrong protocol operation: 0x${op}`)
}
const entry = ber.readString()
ber.readSequence()
const attribute = ber.readString()
const value = ber.readString()
return {
protocolOp,
entry,
attribute,
value
}
}
}
module.exports = CompareRequest

View File

@ -0,0 +1,168 @@
'use strict'
const tap = require('tap')
const { operations } = require('@ldapjs/protocol')
const { BerReader, BerWriter } = require('@ldapjs/asn1')
const { DN } = require('@ldapjs/dn')
const CompareRequest = require('./compare-request')
const { compareRequestBytes } = require('./_fixtures/message-byte-arrays')
tap.test('basic', t => {
t.test('constructor no args', async t => {
const req = new CompareRequest()
t.strictSame(req.pojo, {
messageId: 1,
protocolOp: operations.LDAP_REQ_COMPARE,
type: 'CompareRequest',
entry: null,
attribute: '',
value: '',
controls: []
})
})
t.test('constructor with args', async t => {
const req = new CompareRequest({
entry: 'dn=foo,dc=example,dc=com',
attribute: 'foo',
value: 'bar'
})
t.strictSame(req.pojo, {
messageId: 1,
protocolOp: operations.LDAP_REQ_COMPARE,
type: 'CompareRequest',
entry: 'dn=foo,dc=example,dc=com',
attribute: 'foo',
value: 'bar',
controls: []
})
})
t.test('.type', async t => {
const req = new CompareRequest()
t.equal(req.type, 'CompareRequest')
})
t.test('.dn', async t => {
const req = new CompareRequest({ entry: 'dn=foo,dc=example,dc=com' })
t.equal(req.dn.toString(), 'dn=foo,dc=example,dc=com')
})
t.end()
})
tap.test('.attribute', t => {
t.test('gets and sets', async t => {
const req = new CompareRequest()
t.equal(req.attribute, '')
req.attribute = 'foo'
t.equal(req.attribute, 'foo')
})
t.end()
})
tap.test('.entry', t => {
t.test('gets and sets', async t => {
const req = new CompareRequest()
t.equal(req.entry, null)
req.entry = 'cn=foo'
t.equal(req.entry.toString(), 'cn=foo')
req.entry = DN.fromString('sn=bar')
t.equal(req.entry.toString(), 'sn=bar')
})
t.test('throws for bad value', async t => {
const req = new CompareRequest()
t.throws(
() => {
req.entry = { cn: 'foo' }
},
'entry must be a valid DN string or instance of LdapDn'
)
})
t.end()
})
tap.test('.value', t => {
t.test('gets and sets', async t => {
const req = new CompareRequest()
t.equal(req.value, '')
req.value = 'foo'
t.equal(req.value, 'foo')
})
t.end()
})
tap.test('_toBer', t => {
t.test('converts instance to BER', async t => {
const req = new CompareRequest({
messageId: 2,
entry: 'uid=jdoe,ou=People,dc=example,dc=com',
attribute: 'employeeType',
value: 'salaried'
})
const writer = new BerWriter()
req._toBer(writer)
t.equal(
Buffer.from(compareRequestBytes.slice(5)).compare(writer.buffer),
0
)
})
t.end()
})
tap.test('_pojo', t => {
t.test('returns a pojo representation', async t => {
const req = new CompareRequest({
entry: 'cn=foo',
attribute: 'bar',
value: 'baz'
})
t.strictSame(req._pojo(), {
entry: 'cn=foo',
attribute: 'bar',
value: 'baz'
})
})
t.end()
})
tap.test('#parseToPojo', t => {
t.test('throws if operation incorrect', async t => {
const reqBuffer = Buffer.from(compareRequestBytes)
reqBuffer[5] = 0x61
const reader = new BerReader(reqBuffer)
reader.readSequence()
reader.readInt()
t.throws(
() => CompareRequest.parseToPojo(reader),
Error('found wrong protocol operation: 0x61')
)
})
t.test('returns a pojo representation', async t => {
const reqBuffer = Buffer.from(compareRequestBytes)
const reader = new BerReader(reqBuffer)
reader.readSequence()
reader.readInt()
const pojo = CompareRequest.parseToPojo(reader)
t.equal(pojo.protocolOp, operations.LDAP_REQ_COMPARE)
t.equal(pojo.entry, 'uid=jdoe,ou=People,dc=example,dc=com')
t.equal(pojo.attribute, 'employeeType')
t.equal(pojo.value, 'salaried')
})
t.end()
})

View File

@ -0,0 +1,45 @@
'use strict'
const LdapResult = require('../ldap-result')
const { operations } = require('@ldapjs/protocol')
/**
* Implements the compare response message as described in
* https://www.rfc-editor.org/rfc/rfc4511.html#section-4.10.
*/
class CompareResponse extends LdapResult {
/**
* @param {LdapResultOptions} options
*/
constructor (options = {}) {
options.protocolOp = operations.LDAP_RES_COMPARE
super(options)
}
/**
* The name of the request type.
*
* @type {string}
*/
get type () {
return 'CompareResponse'
}
/**
* Implements the standardized `parseToPojo` method.
*
* @see LdapMessage.parseToPojo
*
* @param {import('@ldapjs/asn1').BerReader} ber
*
* @returns {object}
*/
static parseToPojo (ber) {
return LdapResult._parseToPojo({
opCode: operations.LDAP_RES_COMPARE,
berReader: ber
})
}
}
module.exports = CompareResponse

View File

@ -0,0 +1,55 @@
'use strict'
const tap = require('tap')
const { BerReader } = require('@ldapjs/asn1')
const { operations, resultCodes } = require('@ldapjs/protocol')
const {
compareResponseBytes
} = require('./_fixtures/message-byte-arrays')
const CompareResponse = require('./compare-response')
tap.test('basic', async t => {
const res = new CompareResponse()
t.equal(res.protocolOp, operations.LDAP_RES_COMPARE)
t.equal(res.type, 'CompareResponse')
})
tap.test('toBer', t => {
tap.test('returns basic bytes', async t => {
const res = new CompareResponse({
messageId: 2,
status: resultCodes.COMPARE_TRUE
})
const ber = res.toBer()
const expected = Buffer.from(compareResponseBytes)
t.equal(expected.compare(ber.buffer), 0)
})
t.end()
})
tap.test('#parseToPojo', t => {
t.test('parses a basic object', async t => {
const bytes = compareResponseBytes.slice(5)
const reader = new BerReader(Buffer.from(bytes))
const pojo = CompareResponse.parseToPojo(reader)
t.strictSame(pojo, {
status: resultCodes.COMPARE_TRUE,
matchedDN: '',
diagnosticMessage: '',
referrals: []
})
})
t.test('throws if protocol op is wrong', async t => {
const bytes = compareResponseBytes.slice(5)
bytes[0] = 0x68
const reader = new BerReader(Buffer.from(bytes))
t.throws(
() => CompareResponse.parseToPojo(reader),
Error('found wrong protocol operation: 0x68')
)
})
t.end()
})

View File

@ -0,0 +1,116 @@
'use strict'
const LdapMessage = require('../ldap-message')
const Protocol = require('@ldapjs/protocol')
const { DN } = require('@ldapjs/dn')
/**
* Implements the delete request message as described in
* https://www.rfc-editor.org/rfc/rfc4511.html#section-4.8
*/
class DeleteRequest extends LdapMessage {
#entry
/**
* @typedef {LdapMessageOptions} DeleteRequestOptions
* @property {string} [entry=null] The LDAP entry path to remove.
*/
/**
* @param {DeleteRequestOptions} [options]
*/
constructor (options = {}) {
options.protocolOp = Protocol.operations.LDAP_REQ_DELETE
super(options)
this.entry = options.entry ?? null
}
/**
* Alias of {@link name}.
*
* @type {string}
*/
get _dn () {
return this.entry
}
/**
* The identifier for the request that the instance will request be abandoned.
*
* @type {number}
*/
get entry () {
return this.#entry ?? null
}
/**
* Define the path to the LDAP object that will be deleted.
*
* @param {string | null | import('@ldapjs/dn').DN} value
*/
set entry (value) {
if (value === null) return
if (typeof value === 'string') {
this.#entry = DN.fromString(value)
} else if (Object.prototype.toString.call(value) === '[object LdapDn]') {
this.#entry = value
} else {
throw Error('entry must be a valid DN string or instance of LdapDn')
}
}
/**
* The name of the request type.
*
* @type {string}
*/
get type () {
return 'DeleteRequest'
}
/**
* Internal use only.
*
* @param {import('@ldapjs/asn1').BerWriter} ber
*
* @returns {import('@ldapjs/asn1').BerWriter}
*/
_toBer (ber) {
ber.writeString(this.#entry.toString(), Protocol.operations.LDAP_REQ_DELETE)
return ber
}
/**
* Internal use only.
*
* @param {object}
*
* @returns {object}
*/
_pojo (obj = {}) {
obj.protocolOp = Protocol.operations.LDAP_REQ_DELETE
obj.entry = this.#entry ? this.#entry.toString() : null
return obj
}
/**
* Implements the standardized `parseToPojo` method.
*
* @see LdapMessage.parseToPojo
*
* @param {import('@ldapjs/asn1').BerReader} ber
*/
static parseToPojo (ber) {
const protocolOp = ber.peek()
if (protocolOp !== Protocol.operations.LDAP_REQ_DELETE) {
const op = protocolOp.toString(16).padStart(2, '0')
throw Error(`found wrong protocol operation: 0x${op}`)
}
const entry = ber.readString(Protocol.operations.LDAP_REQ_DELETE)
return { protocolOp, entry }
}
}
module.exports = DeleteRequest

View File

@ -0,0 +1,115 @@
'use strict'
const tap = require('tap')
const { BerReader, BerWriter } = require('@ldapjs/asn1')
const { DN } = require('@ldapjs/dn')
const DeleteRequest = require('./delete-request')
tap.test('constructor', t => {
t.test('no args', async t => {
const req = new DeleteRequest()
t.strictSame(req.pojo, {
messageId: 1,
type: 'DeleteRequest',
protocolOp: 0x4a,
entry: null,
controls: []
})
})
t.test('with options', async t => {
const req = new DeleteRequest({
messageId: 5,
entry: 'dc=example,dc=com'
})
t.strictSame(req.pojo, {
messageId: 5,
type: 'DeleteRequest',
protocolOp: 0x4a,
entry: 'dc=example,dc=com',
controls: []
})
t.equal(req._dn.toString(), 'dc=example,dc=com')
t.equal(req.entry.toString(), 'dc=example,dc=com')
t.equal(req.type, 'DeleteRequest')
})
t.end()
})
tap.test('.entry', t => {
t.test('sets/gets', async t => {
const req = new DeleteRequest()
t.equal(req.entry, null)
t.equal(req._dn, null)
req.entry = 'cn=foo'
t.equal(req.entry.toString(), 'cn=foo')
t.equal(req._dn.toString(), 'cn=foo')
req.entry = DN.fromString('sn=bar')
t.equal(req.entry.toString(), 'sn=bar')
})
t.test('throws for bad value', async t => {
const req = new DeleteRequest()
t.throws(
() => {
req.entry = { cn: 'foo' }
},
'entry must be a valid DN string or instance of LdapDn'
)
})
t.end()
})
tap.test('_toBer', t => {
t.test('writes a correct sequence', async t => {
const req = new DeleteRequest({ entry: 'cn=foo' })
const ber = new BerWriter()
req._toBer(ber)
const expected = Buffer.from([
0x4a, 0x06, 0x63, 0x6e,
0x3d, 0x66, 0x6f, 0x6f
])
t.equal(expected.compare(ber.buffer), 0)
})
t.end()
})
tap.test('_pojo', t => {
t.test('returns implementation properties', async t => {
const req = new DeleteRequest({ entry: 'cn=foo' })
t.strictSame(req._pojo(), {
protocolOp: 0x4a,
entry: 'cn=foo'
})
})
t.end()
})
tap.test('#parseToPojo', t => {
t.test('throws if tag is wrong', async t => {
const input = Buffer.from([0x4b, 0x03, 0x66, 0x6f, 0x6f])
t.throws(
() => DeleteRequest.parseToPojo(new BerReader(input)),
Error('found wrong protocol operation: 0x4b')
)
})
t.test('returns a pojo', async t => {
const input = Buffer.from([0x4a, 0x03, 0x66, 0x6f, 0x6f])
const pojo = DeleteRequest.parseToPojo(new BerReader(input))
t.strictSame(pojo, {
protocolOp: 0x4a,
entry: 'foo'
})
})
t.end()
})

View File

@ -0,0 +1,45 @@
'use strict'
const LdapResult = require('../ldap-result')
const { operations } = require('@ldapjs/protocol')
/**
* Implements the delete response message as described in
* https://www.rfc-editor.org/rfc/rfc4511.html#section-4.8.
*/
class DeleteResponse extends LdapResult {
/**
* @param {LdapResultOptions} options
*/
constructor (options = {}) {
options.protocolOp = operations.LDAP_RES_DELETE
super(options)
}
/**
* The name of the request type.
*
* @type {string}
*/
get type () {
return 'DeleteResponse'
}
/**
* Implements the standardized `parseToPojo` method.
*
* @see LdapMessage.parseToPojo
*
* @param {import('@ldapjs/asn1').BerReader} ber
*
* @returns {object}
*/
static parseToPojo (ber) {
return LdapResult._parseToPojo({
opCode: operations.LDAP_RES_DELETE,
berReader: ber
})
}
}
module.exports = DeleteResponse

View File

@ -0,0 +1,52 @@
'use strict'
const tap = require('tap')
const { BerReader } = require('@ldapjs/asn1')
const { operations } = require('@ldapjs/protocol')
const {
deleteResponseBytes
} = require('./_fixtures/message-byte-arrays')
const DeleteResponse = require('./delete-response')
tap.test('basic', async t => {
const res = new DeleteResponse()
t.equal(res.protocolOp, operations.LDAP_RES_DELETE)
t.equal(res.type, 'DeleteResponse')
})
tap.test('toBer', t => {
tap.test('returns basic bytes', async t => {
const res = new DeleteResponse({ messageId: 2 })
const ber = res.toBer()
const expected = Buffer.from(deleteResponseBytes)
t.equal(expected.compare(ber.buffer), 0)
})
t.end()
})
tap.test('#parseToPojo', t => {
t.test('parses a basic object', async t => {
const bytes = deleteResponseBytes.slice(5)
const reader = new BerReader(Buffer.from(bytes))
const pojo = DeleteResponse.parseToPojo(reader)
t.strictSame(pojo, {
status: 0,
matchedDN: '',
diagnosticMessage: '',
referrals: []
})
})
t.test('throws if protocol op is wrong', async t => {
const bytes = deleteResponseBytes.slice(5)
bytes[0] = 0x68
const reader = new BerReader(Buffer.from(bytes))
t.throws(
() => DeleteResponse.parseToPojo(reader),
Error('found wrong protocol operation: 0x68')
)
})
t.end()
})

View File

@ -0,0 +1,284 @@
'use strict'
const LdapMessage = require('../ldap-message')
const { operations } = require('@ldapjs/protocol')
const RECOGNIZED_OIDS = require('./extension-utils/recognized-oids')
/**
* Implements the extension request message as described in
* https://www.rfc-editor.org/rfc/rfc4511.html#section-4.12.
*
* There is a set of supported extension request OIDs supported. Any
* unrecognized OID will be treated a simple string pair, i.e. both
* `requestName` and `requestValue` will be assumed to be simple strings.
*/
class ExtensionRequest extends LdapMessage {
#requestName
#requestValue
/**
* @typedef {LdapMessageOptions} ExtensionRequestOptions
* @property {string} [requestName=''] The name of the extension, i.e.
* OID for the request.
* @property {string|object} [requestValue] The value for the request.
* If `undefined`, no value will be sent. If the request requires a simple
* string value, provide such a string. For complex valued requests, e.g.
* for a password modify request, it should be a plain object with the
* appropriate properties. See the implementation of {@link parseToPojo}
* for the set of supported objects.
*/
/**
* @param {ExtensionRequestOptions} [options]
*/
constructor (options = {}) {
options.protocolOp = operations.LDAP_REQ_EXTENSION
super(options)
this.requestName = options.requestName || ''
this.requestValue = options.requestValue
}
/**
* Alias of {@link requestName}.
*
* @type {string}
*/
get _dn () {
return this.#requestName
}
/**
* The name (OID) of the request.
*
* @returns {string}
*/
get requestName () {
return this.#requestName
}
/**
* Set the name for the request. Should be an OID that
* matches a specification.
*
* @param {string} value
*/
set requestName (value) {
this.#requestName = value
}
/**
* The name of the request type.
*
* @type {string}
*/
get type () {
return 'ExtensionRequest'
}
/**
* The value, if any, for the request.
*
* @returns {undefined|string|object} value
*/
get requestValue () {
return this.#requestValue
}
/**
* Set the value for the request. The value should conform
* to the specification identified by the {@link requestName}.
* See the implemenation of {@link parseToPojo} for valid
* value shapes.
*
* @param {undefined|string|object} value
*/
set requestValue (val) {
this.#requestValue = val
}
/**
* Internal use only.
*
* @param {import('@ldapjs/asn1').BerWriter} ber
*
* @returns {import('@ldapjs/asn1').BerWriter}
*/
_toBer (ber) {
ber.startSequence(operations.LDAP_REQ_EXTENSION)
ber.writeString(this.requestName, 0x80)
if (this.requestValue) {
switch (this.requestName) {
case RECOGNIZED_OIDS.get('CANCEL_REQUEST'): {
encodeCancelRequest({ ber, requestValue: this.requestValue })
break
}
case RECOGNIZED_OIDS.get('PASSWORD_MODIFY'): {
encodePasswordModify({
ber,
requestValue: this.requestValue
})
break
}
default: {
// We assume the value is a plain string since
// we do not recognize the request OID, or we know
// that the OID uses a plain string value.
ber.writeString(this.requestValue, 0x81)
}
}
}
ber.endSequence()
return ber
}
/**
* Internal use only.
*
* @param {object}
*
* @returns {object}
*/
_pojo (obj = {}) {
obj.requestName = this.requestName
obj.requestValue = this.requestValue
return obj
}
/**
* Implements the standardized `parseToPojo` method.
*
* @see LdapMessage.parseToPojo
*
* @param {import('@ldapjs/asn1').BerReader} ber
*/
static parseToPojo (ber) {
const protocolOp = ber.readSequence()
if (protocolOp !== operations.LDAP_REQ_EXTENSION) {
const op = protocolOp.toString(16).padStart(2, '0')
throw Error(`found wrong protocol operation: 0x${op}`)
}
// While the requestName is an OID, it is not an
// _encoded_ OID. It is a plain string. So we do
// not use `.readOID` here.
const requestName = ber.readString(0x80)
if (ber.peek() !== 0x81) {
// There is not a request value present, so we just
// return an empty value representation.
return { protocolOp, requestName }
}
let requestValue
switch (requestName) {
case RECOGNIZED_OIDS.get('CANCEL_REQUEST'): {
requestValue = readCancelRequest(ber)
break
}
case RECOGNIZED_OIDS.get('PASSWORD_MODIFY'): {
requestValue = readPasswordModify(ber)
break
}
default: {
// We will assume it is a plain string value
// since we do not recognize the OID, or we know
// that the OID uses a plain string value.
requestValue = ber.readString(0x81)
break
}
}
return { protocolOp, requestName, requestValue }
}
/**
* A list of EXTENDED operation OIDs that this module
* recognizes. Key names are named according to the common name
* of the extension. Key values are the OID associated with that
* extension. For example, key `PASSWORD_MODIFY` corresponds to
* OID `1.3.6.1.4.1.4203.1.11.1`.
*
* @returns {Map<string, string>}
*/
static recognizedOIDs () {
return RECOGNIZED_OIDS
}
}
module.exports = ExtensionRequest
/**
* @param {object} input
* @param {@import('@ldapjs/asn1').BerWriter} input.ber
* @param {object} requestValue
*/
function encodeCancelRequest ({ ber, requestValue }) {
ber.startSequence(0x81)
ber.startSequence()
ber.writeInt(requestValue)
ber.endSequence()
ber.endSequence()
}
/**
* @param {@import('@ldapjs/asn1').BerReader} ber
* @returns {number}
*/
function readCancelRequest (ber) {
ber.readSequence(0x81)
ber.readSequence()
return ber.readInt()
}
/**
* @param {object} input
* @param {@import('@ldapjs/asn1').BerWriter} input.ber
* @param {object} requestValue
*/
function encodePasswordModify ({ ber, requestValue }) {
// start the value sequence
ber.startSequence(0x81)
// start the generic packed sequence
ber.startSequence()
if (requestValue.userIdentity) {
ber.writeString(requestValue.userIdentity, 0x80)
}
if (requestValue.oldPassword) {
ber.writeString(requestValue.oldPassword, 0x81)
}
if (requestValue.newPassword) {
ber.writeString(requestValue.newPassword, 0x82)
}
ber.endSequence()
ber.endSequence()
}
/**
* @param {@import('@ldapjs/asn1').BerReader} ber
* @returns {object}
*/
function readPasswordModify (ber) {
// advance to the embedded sequence
ber.readSequence(0x81)
// advance to the value of the embedded sequence
ber.readSequence()
let userIdentity
if (ber.peek() === 0x80) {
userIdentity = ber.readString(0x80)
}
let oldPassword
if (ber.peek() === 0x81) {
oldPassword = ber.readString(0x81)
}
let newPassword
if (ber.peek() === 0x82) {
newPassword = ber.readString(0x82)
}
return { userIdentity, oldPassword, newPassword }
}

View File

@ -0,0 +1,254 @@
'use strict'
const tap = require('tap')
const { operations } = require('@ldapjs/protocol')
const ExtensionRequest = require('./extension-request')
const {
extensionCancelRequestBytes,
extensionNameAndValueRequestBytes,
extensionNameAndNoValueRequestBytes,
extensionChangePasswordRequestBytes,
extensionChangePasswordWithNewPasswordBytes
} = require('./_fixtures/message-byte-arrays')
const { BerReader, BerWriter } = require('@ldapjs/asn1')
tap.test('basic', t => {
t.test('constructor no args', async t => {
const req = new ExtensionRequest()
t.strictSame(req.pojo, {
messageId: 1,
protocolOp: operations.LDAP_REQ_EXTENSION,
type: 'ExtensionRequest',
requestName: '',
requestValue: undefined,
controls: []
})
})
t.test('constructor with args', async t => {
const req = new ExtensionRequest({
requestName: 'foo',
requestValue: 'bar'
})
t.strictSame(req.pojo, {
messageId: 1,
protocolOp: operations.LDAP_REQ_EXTENSION,
type: 'ExtensionRequest',
requestName: 'foo',
requestValue: 'bar',
controls: []
})
})
t.test('.type', async t => {
const req = new ExtensionRequest()
t.equal(req.type, 'ExtensionRequest')
})
t.test('.dn', async t => {
const req = new ExtensionRequest({ requestName: 'foo' })
t.equal(req.dn, 'foo')
})
t.test('#recognizedOIDs returns map', async t => {
const oids = ExtensionRequest.recognizedOIDs()
t.type(oids, 'Map')
})
t.end()
})
tap.test('.name', t => {
t.test('gets and sets', async t => {
const req = new ExtensionRequest()
t.equal(req.requestName, '')
req.requestName = 'foo'
t.equal(req.requestName, 'foo')
})
t.end()
})
tap.test('.value', t => {
t.test('gets and sets', async t => {
const req = new ExtensionRequest()
t.equal(req.requestValue, undefined)
req.requestValue = 'foo'
t.equal(req.requestValue, 'foo')
})
t.end()
})
tap.test('_toBer', t => {
t.test('converts simple instance to BER', async t => {
const req = new ExtensionRequest({
requestName: '1.3.6.1.1',
requestValue: 'foobar'
})
const writer = new BerWriter()
req._toBer(writer)
t.equal(
Buffer.from(extensionNameAndValueRequestBytes.slice(5)).compare(writer.buffer),
0
)
})
t.test('converts a simple instance with no value to BER', async t => {
const req = new ExtensionRequest({ requestName: '1.3.6.1.1' })
const writer = new BerWriter()
req._toBer(writer)
t.equal(
Buffer.from(extensionNameAndNoValueRequestBytes.slice(5)).compare(writer.buffer),
0
)
})
t.test('converts a cancel request', async t => {
const req = new ExtensionRequest({
requestName: '1.3.6.1.1.8',
requestValue: 1
})
const writer = new BerWriter()
req._toBer(writer)
t.equal(
Buffer.from(extensionCancelRequestBytes.slice(5)).compare(writer.buffer),
0
)
})
t.test('converts a modify password request with userIdentity and oldPassword', async t => {
const req = new ExtensionRequest({
requestName: '1.3.6.1.4.1.4203.1.11.1',
requestValue: {
userIdentity: 'uid=jdoe,ou=People,dc=example,dc=com',
oldPassword: 'secret123'
}
})
const writer = new BerWriter()
req._toBer(writer)
t.equal(
Buffer.from(extensionChangePasswordRequestBytes.slice(5)).compare(writer.buffer),
0
)
})
t.test('converts a modify password request with newPassword', async t => {
const req = new ExtensionRequest({
requestName: '1.3.6.1.4.1.4203.1.11.1',
requestValue: {
newPassword: 'secret123'
}
})
const writer = new BerWriter()
req._toBer(writer)
t.equal(
Buffer.from(extensionChangePasswordWithNewPasswordBytes.slice(5)).compare(writer.buffer),
0
)
})
t.end()
})
tap.test('_pojo', t => {
t.test('returns a pojo representation', async t => {
const req = new ExtensionRequest({
requestName: 'foo',
requestValue: 'bar'
})
t.strictSame(req._pojo(), {
requestName: 'foo',
requestValue: 'bar'
})
})
t.end()
})
tap.test('#parseToPojo', t => {
t.test('throws if operation incorrect', async t => {
const reqBuffer = Buffer.from(extensionNameAndValueRequestBytes)
reqBuffer[5] = 0x61
const reader = new BerReader(reqBuffer)
reader.readSequence()
reader.readInt()
t.throws(
() => ExtensionRequest.parseToPojo(reader),
Error('found wrong protocol operation: 0x61')
)
})
t.test('returns a pojo for a name only operation', async t => {
const reqBuffer = Buffer.from(extensionNameAndNoValueRequestBytes)
const reader = new BerReader(reqBuffer)
reader.readSequence()
reader.readInt()
const pojo = ExtensionRequest.parseToPojo(reader)
t.equal(pojo.requestName, '1.3.6.1.1')
t.equal(pojo.requestValue, undefined)
})
t.test('returns a pojo for simple string name and value operation', async t => {
const reqBuffer = Buffer.from(extensionNameAndValueRequestBytes)
const reader = new BerReader(reqBuffer)
reader.readSequence()
reader.readInt()
const pojo = ExtensionRequest.parseToPojo(reader)
t.equal(pojo.requestName, '1.3.6.1.1')
t.equal(pojo.requestValue, 'foobar')
})
t.test('returns pojo for a cancel request', async t => {
const reqBuffer = Buffer.from(extensionCancelRequestBytes)
const reader = new BerReader(reqBuffer)
reader.readSequence()
reader.readInt()
const pojo = ExtensionRequest.parseToPojo(reader)
t.equal(pojo.requestName, '1.3.6.1.1.8')
t.equal(pojo.requestValue, 1)
})
t.test('returns a pojo for a modify password with user and old pass', async t => {
const reqBuffer = Buffer.from(extensionChangePasswordRequestBytes)
const reader = new BerReader(reqBuffer)
reader.readSequence()
reader.readInt()
const pojo = ExtensionRequest.parseToPojo(reader)
t.equal(pojo.requestName, '1.3.6.1.4.1.4203.1.11.1')
t.strictSame(pojo.requestValue, {
userIdentity: 'uid=jdoe,ou=People,dc=example,dc=com',
oldPassword: 'secret123',
newPassword: undefined
})
})
t.test('returns a pojo for a modify password new pass', async t => {
const reqBuffer = Buffer.from(extensionChangePasswordWithNewPasswordBytes)
const reader = new BerReader(reqBuffer)
reader.readSequence()
reader.readInt()
const pojo = ExtensionRequest.parseToPojo(reader)
t.equal(pojo.requestName, '1.3.6.1.4.1.4203.1.11.1')
t.strictSame(pojo.requestValue, {
userIdentity: undefined,
oldPassword: undefined,
newPassword: 'secret123'
})
})
t.end()
})

View File

@ -0,0 +1,156 @@
'use strict'
const LdapResult = require('../ldap-result')
const { operations } = require('@ldapjs/protocol')
/**
* Implements the extension response message as described in
* https://www.rfc-editor.org/rfc/rfc4511.html#section-4.12.
*
* The type of response is impossible to determine in isolation.
* Most EXTENSION responses do not include the request OID. And they
* all encode their values in unique ways. Therefore, this object's
* {@link parseToPojo} never attempts to parse the response value.
* Instead, if it is present, it reads the value as a buffer and
* encodes it into a hexadecimal string prefixed with a `<buffer>`
* token. This string is then used by the `#fromExtension` method
* on specific implementations to build a new object. It is left up to
* the implementor to know when certain responses are expected and
* to act accordingly.
*/
class ExtensionResponse extends LdapResult {
#responseName
#responseValue
/**
* @typedef {LdapResultOptions} ExtensionResponseOptions
* @property {string|undefined} [responseName] The name of the extension, i.e.
* OID for the response.
* @property {string|undefined} [responseValue] The value for the
* response. It may be a buffer string; such a string is a series of
* hexadecimal pairs preceded by the token `<buffer>`. Buffer strings
* are used by specific response object types to get that type's specific
* encoded value.
*/
/**
* @param {ExtensionResponseOptions} [options]
*/
constructor (options = {}) {
options.protocolOp = operations.LDAP_RES_EXTENSION
super(options)
this.responseName = options.responseName
this.responseValue = options.responseValue
}
/**
* The OID, if any, of the response.
*
* @returns {string|undefined}
*/
get responseName () {
return this.#responseName
}
/**
* Define the name (OID) of the response.
*
* @param {string} value
*/
set responseName (value) {
this.#responseName = value
}
/**
* The response value, if any. For specific extensions that
* are not simple string values, the initial value is a buffer string.
* That is, it is a hexadecimal string of bytes prefixed with `<buffer>`.
* To parse this value, use a specific extension's `#fromResponse` method.
*
* @returns {string|undefined}
*/
get responseValue () {
return this.#responseValue
}
/**
* Set the response value. Should be a buffer string if the value is
* an encoded value.
*
* @param {string} value
*/
set responseValue (value) {
this.#responseValue = value
}
/**
* The name of the request type.
*
* @type {string}
*/
get type () {
return 'ExtensionResponse'
}
/**
* Internal use only. Used to write the response name and
* response value into the BER object.
*
* @param {import('@ldapjs/asn1').BerWriter} ber
*
* @returns {import('@ldapjs/asn1').BerWriter}
*/
_writeResponse (ber) {
if (this.responseName) {
ber.writeString(this.responseName, 0x8a)
}
if (this.responseValue === undefined) {
return ber
}
switch (this.responseName) {
default: {
// We assume the value is a plain string since
// we do not recognize the response OID, or we
// know it would be a plain string.
ber.writeString(this.responseValue, 0x8b)
}
}
return ber
}
/**
* Implements the standardized `parseToPojo` method.
*
* @see LdapMessage.parseToPojo
*
* @param {import('@ldapjs/asn1').BerReader} ber
*
* @returns {object}
*/
static parseToPojo (ber) {
const pojo = LdapResult._parseToPojo({
opCode: operations.LDAP_RES_EXTENSION,
berReader: ber
})
let responseName
if (ber.peek() === 0x8a) {
responseName = ber.readString(0x8a)
}
if (ber.peek() !== 0x8b) {
return { ...pojo, responseName }
}
const valueBuffer = ber.readTag(0x8b)
const responseValue = `<buffer>${valueBuffer.toString('hex')}`
return { ...pojo, responseName, responseValue }
}
}
module.exports = ExtensionResponse

View File

@ -0,0 +1,117 @@
'use strict'
const tap = require('tap')
const { BerReader, BerWriter } = require('@ldapjs/asn1')
const { operations, resultCodes } = require('@ldapjs/protocol')
const {
extensionDisconnectionNotificationResponseBytes
} = require('./_fixtures/message-byte-arrays')
const ExtensionResponse = require('./extension-response')
tap.test('basic', async t => {
const res = new ExtensionResponse()
t.equal(res.protocolOp, operations.LDAP_RES_EXTENSION)
t.equal(res.type, 'ExtensionResponse')
})
tap.test('.responseName', t => {
t.test('gets and sets', async t => {
const res = new ExtensionResponse()
t.equal(res.responseName, undefined)
res.responseName = 'foo'
t.equal(res.responseName, 'foo')
})
t.end()
})
tap.test('.responseValue', t => {
t.test('gets and sets', async t => {
const res = new ExtensionResponse()
t.equal(res.responseValue, undefined)
res.responseValue = 'foo'
t.equal(res.responseValue, 'foo')
})
t.end()
})
tap.test('toBer', t => {
tap.test('returns basic bytes', async t => {
const res = new ExtensionResponse({
messageId: 0,
status: resultCodes.UNAVAILABLE,
diagnosticMessage: 'The Directory Server is shutting down',
responseName: '1.3.6.1.4.1.1466.20036'
})
const ber = res.toBer()
const expected = Buffer.from(extensionDisconnectionNotificationResponseBytes)
t.equal(expected.compare(ber.buffer), 0)
})
t.end()
})
tap.test('_writeResponse', t => {
t.test('writes a response with a name', async t => {
const req = new ExtensionResponse({ responseName: 'foo' })
const expected = Buffer.from([0x8a, 0x03, 0x66, 0x6f, 0x6f])
const writer = new BerWriter()
req._writeResponse(writer)
t.equal(expected.compare(writer.buffer), 0)
})
t.test('writes response with value', async t => {
const req = new ExtensionResponse({ responseValue: 'foo' })
const expected = Buffer.from([0x8b, 0x03, 0x66, 0x6f, 0x6f])
const writer = new BerWriter()
req._writeResponse(writer)
t.equal(expected.compare(writer.buffer), 0)
})
t.end()
})
tap.test('#parseToPojo', t => {
t.test('throws if protocol op is wrong', async t => {
const bytes = extensionDisconnectionNotificationResponseBytes.slice(5)
bytes[0] = 0x68
const reader = new BerReader(Buffer.from(bytes))
t.throws(
() => ExtensionResponse.parseToPojo(reader),
Error('found wrong protocol operation: 0x68')
)
})
t.test('parses a basic object', async t => {
const bytes = extensionDisconnectionNotificationResponseBytes.slice(5)
const reader = new BerReader(Buffer.from(bytes))
const pojo = ExtensionResponse.parseToPojo(reader)
t.strictSame(pojo, {
status: resultCodes.UNAVAILABLE,
matchedDN: '',
diagnosticMessage: 'The Directory Server is shutting down',
referrals: [],
responseName: '1.3.6.1.4.1.1466.20036'
})
})
t.test('values are buffer strings', async t => {
const {
withGeneratedPasswordBytes
} = require('./extension-responses/_fixtures/password-modify')
const bytes = withGeneratedPasswordBytes.slice(5)
const reader = new BerReader(Buffer.from(bytes))
const pojo = ExtensionResponse.parseToPojo(reader)
t.strictSame(pojo, {
status: 0,
matchedDN: '',
diagnosticMessage: '',
referrals: [],
responseName: undefined,
responseValue: '<buffer>301f801d4164617074657253746576656e736f6e477261696e506c61796d617465'
})
})
t.end()
})

View File

@ -0,0 +1,34 @@
'use strict'
/**
* Represents an EXTENSION response that is for a password change
* without providing a new one (server generates password).
* Taken from https://web.archive.org/web/20220518193613/https://nawilson.com/ldapv3-wire-protocol-reference-extended/
*/
module.exports.withGeneratedPasswordBytes = [
0x30, 0x2f, // start sequence, 47 bytes
0x02, 0x01, 0x01, // message ID (integer value 1)
0x78, 0x2a, // protocol op (0x78), 42 bytes
0x0a, 0x01, 0x00, // success result code (enumerated value 0)
0x04, 0x00, // no matched DN (0-byte octet string)
0x04, 0x00, // no diagnostic message (0-byte octet string)
0x8b, 0x21, // sequence (response value), 33 bytes
0x30, 0x1f, // sequence, 31 bytes
0x80, 0x1d, // extension specific string, 29 bytes
// "AdapterStevensonGrainPlaymate"
0x41, 0x64, 0x61, 0x70, 0x74, 0x65, 0x72, 0x53,
0x74, 0x65, 0x76, 0x65, 0x6e, 0x73, 0x6f, 0x6e,
0x47, 0x72, 0x61, 0x69, 0x6e, 0x50, 0x6c, 0x61,
0x79, 0x6d, 0x61, 0x74, 0x65
]
module.exports.basicResponse = [
0x30, 0x0c, // start sequence, 47 bytes
0x02, 0x01, 0x01, // message ID (integer value 1)
0x78, 0x2a, // protocol op (0x78), 42 bytes
0x0a, 0x01, 0x00, // success result code (enumerated value 0)
0x04, 0x00, // no matched DN (0-byte octet string)
0x04, 0x00 // no diagnostic message (0-byte octet string)
]

View File

@ -0,0 +1,24 @@
'use strict'
module.exports.withId = [
0x30, 0x21, // sequence, 33 bytes
0x02, 0x01, 0x02, // message id (integer 2)
0x78, 0x1c, // protocol op (0x78), 28 bytes
0x0a, 0x01, 0x00, // status code, 0
0x04, 0x00, // no matched dn
0x04, 0x00, // no diagnostic message
0x8b, 0x13, // string response value
// "u:xxyyz@EXAMPLE.NET"
0x75, 0x3a, 0x78, 0x78, 0x79, 0x79, 0x7a, 0x40,
0x45, 0x58, 0x41, 0x4d, 0x50, 0x4c, 0x45, 0x2e,
0x4e, 0x45, 0x54
]
module.exports.withoutId = [
0x30, 0x0c, // sequence, 12 bytes
0x02, 0x01, 0x02, // message id (integer 2)
0x78, 0x07, // protocol op (0x78), 7 bytes
0x0a, 0x01, 0x00, // status code, 0
0x04, 0x00, // no matched dn
0x04, 0x00 // no diagnostic message
]

View File

@ -0,0 +1,33 @@
'use strict'
const { BerReader } = require('@ldapjs/asn1')
const ExtensionResponse = require('../extension-response')
/**
* Implements the password modify extension defined by
* https://www.rfc-editor.org/rfc/rfc3062.
*/
class PasswordModifyResponse extends ExtensionResponse {
/**
* Given a basic {@link ExtensionResponse} with a buffer string in
* `responseValue`, parse into a specific {@link PasswordModifyResponse}
* instance.
*
* @param {ExtensionResponse} response
*
* @returns {PasswordModifyResponse}
*/
static fromResponse (response) {
if (response.responseValue === undefined) {
return new PasswordModifyResponse()
}
const valueBuffer = Buffer.from(response.responseValue.substring(8), 'hex')
const reader = new BerReader(valueBuffer)
reader.readSequence()
const responseValue = reader.readString(0x80)
return new PasswordModifyResponse({ responseValue })
}
}
module.exports = PasswordModifyResponse

View File

@ -0,0 +1,28 @@
'use strict'
const tap = require('tap')
const { BerReader } = require('@ldapjs/asn1')
const LdapMessage = require('../../ldap-message')
const {
basicResponse,
withGeneratedPasswordBytes
} = require('./_fixtures/password-modify')
const PasswordModifyResponse = require('./password-modify')
tap.test('parses a response with a generated password', async t => {
const reader = new BerReader(Buffer.from(withGeneratedPasswordBytes))
let res = LdapMessage.parse(reader)
res = PasswordModifyResponse.fromResponse(res)
t.type(res, PasswordModifyResponse)
t.equal(res.responseName, undefined)
t.equal(res.responseValue, 'AdapterStevensonGrainPlaymate')
})
tap.test('parses a response with an empty value', async t => {
const reader = new BerReader(Buffer.from(basicResponse))
let res = LdapMessage.parse(reader)
res = PasswordModifyResponse.fromResponse(res)
t.type(res, PasswordModifyResponse)
t.equal(res.responseName, undefined)
t.equal(res.responseValue, undefined)
})

View File

@ -0,0 +1,30 @@
'use strict'
const ExtensionResponse = require('../extension-response')
/**
* Implements the "Who Am I" extension defined by
* https://www.rfc-editor.org/rfc/rfc4532.
*/
class WhoAmIResponse extends ExtensionResponse {
/**
* Given a basic {@link ExtensionResponse} with a buffer string in
* `responseValue`, parse into a specific {@link WhoAmIResponse}
* instance.
*
* @param {ExtensionResponse} response
*
* @returns {WhoAmIResponse}
*/
static fromResponse (response) {
if (response.responseValue === undefined) {
return new WhoAmIResponse()
}
const valueBuffer = Buffer.from(response.responseValue.substring(8), 'hex')
const responseValue = valueBuffer.toString('utf8')
return new WhoAmIResponse({ responseValue })
}
}
module.exports = WhoAmIResponse

View File

@ -0,0 +1,25 @@
'use strict'
const tap = require('tap')
const { BerReader } = require('@ldapjs/asn1')
const LdapMessage = require('../../ldap-message')
const { withId, withoutId } = require('./_fixtures/who-am-i')
const WhoAmIResponse = require('./who-am-i')
tap.test('parses a response with a returned id', async t => {
const reader = new BerReader(Buffer.from(withId))
let res = LdapMessage.parse(reader)
res = WhoAmIResponse.fromResponse(res)
t.type(res, WhoAmIResponse)
t.equal(res.responseName, undefined)
t.equal(res.responseValue, 'u:xxyyz@EXAMPLE.NET')
})
tap.test('parses a response with a returned id', async t => {
const reader = new BerReader(Buffer.from(withoutId))
let res = LdapMessage.parse(reader)
res = WhoAmIResponse.fromResponse(res)
t.type(res, WhoAmIResponse)
t.equal(res.responseName, undefined)
t.equal(res.responseValue, undefined)
})

View File

@ -0,0 +1,29 @@
'use strict'
const OIDS = new Map([
['CANCEL_REQUEST', '1.3.6.1.1.8'], // RFC 3909
['DISCONNECTION_NOTIFICATION', '1.3.6.1.4.1.1466.20036'], // RFC 4511
['PASSWORD_MODIFY', '1.3.6.1.4.1.4203.1.11.1'], // RFC 3062
['START_TLS', '1.3.6.1.4.1.1466.20037'], // RFC 4511
['WHO_AM_I', '1.3.6.1.4.1.4203.1.11.3'] // RFC 4532
])
Object.defineProperty(OIDS, 'lookupName', {
value: function (oid) {
for (const [key, value] of this.entries()) {
/* istanbul ignore else */
if (value === oid) return key
}
}
})
Object.defineProperty(OIDS, 'lookupOID', {
value: function (name) {
for (const [key, value] of this.entries()) {
/* istanbul ignore else */
if (key === name) return value
}
}
})
module.exports = OIDS

View File

@ -0,0 +1,14 @@
'use strict'
const tap = require('tap')
const RECOGNIZED_OIDS = require('./recognized-oids')
tap.test('lookupName returns correct name', async t => {
const name = RECOGNIZED_OIDS.lookupName('1.3.6.1.4.1.4203.1.11.1')
t.equal(name, 'PASSWORD_MODIFY')
})
tap.test('lookupOID returns correct OID', async t => {
const name = RECOGNIZED_OIDS.lookupOID('PASSWORD_MODIFY')
t.equal(name, '1.3.6.1.4.1.4203.1.11.1')
})

View File

@ -0,0 +1,189 @@
'use strict'
const LdapMessage = require('../ldap-message')
const { operations } = require('@ldapjs/protocol')
const partIsNotNumeric = part => /^\d+$/.test(part) === false
/**
* Determines if a passed in string is a dotted decimal string.
*
* Copied from `@ldapjs/dn`.
*
* @param {string} value
*
* @returns {boolean}
*/
function isDottedDecimal (value) {
if (typeof value !== 'string') return false
const parts = value.split('.')
const nonNumericParts = parts.filter(partIsNotNumeric)
return nonNumericParts.length === 0
}
/**
* Implements the intermediate response message as described in
* https://www.rfc-editor.org/rfc/rfc4511.html#section-4.13.
*
* TODO: actual implementations of this, e.g. RFC 4533 §2.5, seem to encode
* sequences in the responseValue. That means this needs a more robust
* implementation like is found in the ExtensionResponse implementation (i.e.
* detection of recognized OIDs and specific sub-implementations). As of now,
* this implementation follows the baseline spec without any sub-implementations.
*/
class IntermediateResponse extends LdapMessage {
#responseName
#responseValue
/**
* @typedef {LdapMessageOptions} IntermediateResponseOptions
* @property {string} responseName
* @property {string} responseValue
*/
/**
* @param {IntermediateResponseOptions} options
*/
constructor (options = {}) {
options.protocolOp = operations.LDAP_RES_INTERMEDIATE
super(options)
this.responseName = options.responseName ?? null
this.responseValue = options.responseValue ?? null
}
/**
* The name of the request type.
*
* @type {string}
*/
get type () {
return 'IntermediateResponse'
}
/**
* The numeric OID that identifies the type of intermediate response.
*
* @returns {string | undefined}
*/
get responseName () {
return this.#responseName
}
/**
* Define the numeric OID that identifies the type of intermediate response.
*
* @param {string | null} value
*
* @throws For an invalid value.
*/
set responseName (value) {
if (value === null) return
if (isDottedDecimal(value) === false) {
throw Error('responseName must be a numeric OID')
}
this.#responseName = value
}
/**
* The value for the intermidate response if any.
*
* @returns {string | undefined}
*/
get responseValue () {
return this.#responseValue
}
/**
* Define the value for the intermediate response.
*
* @param {string | null} value
*
* @throws For an invalid value.
*/
set responseValue (value) {
if (value === null) return
if (typeof value !== 'string') {
throw Error('responseValue must be a string')
}
this.#responseValue = value
}
/**
* Internal use only.
*
* @param {import('@ldapjs/asn1').BerWriter} ber
*
* @returns {import('@ldapjs/asn1').BerWriter}
*/
_toBer (ber) {
ber.startSequence(operations.LDAP_RES_INTERMEDIATE)
if (this.#responseName) {
ber.writeString(this.#responseName, 0x80)
}
if (this.#responseValue) {
ber.writeString(this.#responseValue, 0x81)
}
ber.endSequence()
return ber
}
/**
* Internal use only.
*
* @param {object}
*
* @returns {object}
*/
_pojo (obj = {}) {
obj.responseName = this.#responseName
obj.responseValue = this.#responseValue
return obj
}
/**
* Implements the standardized `parseToPojo` method.
*
* @see LdapMessage.parseToPojo
*
* @param {import('@ldapjs/asn1').BerReader} ber
*
* @returns {object}
*/
static parseToPojo (ber) {
const protocolOp = ber.readSequence()
if (protocolOp !== operations.LDAP_RES_INTERMEDIATE) {
const op = protocolOp.toString(16).padStart(2, '0')
throw Error(`found wrong protocol operation: 0x${op}`)
}
let responseName
let responseValue
let tag = ber.peek()
switch (tag) {
case 0x80: {
responseName = ber.readString(tag)
tag = ber.peek()
/* istanbul ignore else */
if (tag === 0x81) {
responseValue = ber.readString(tag)
}
break
}
case 0x81: {
responseValue = ber.readString(tag)
}
}
return { protocolOp, responseName, responseValue }
}
}
module.exports = IntermediateResponse

View File

@ -0,0 +1,191 @@
'use strict'
const tap = require('tap')
const { operations } = require('@ldapjs/protocol')
const { BerWriter, BerReader } = require('@ldapjs/asn1')
const IntermediateResponse = require('./intermediate-response')
const {
intermediateResponseBytes,
intermediateResponseNoValueBytes,
intermediateResponseNoNameBytes
} = require('./_fixtures/message-byte-arrays')
tap.test('basic', t => {
t.test('constructor no args', async t => {
const res = new IntermediateResponse()
t.strictSame(res.pojo, {
messageId: 1,
protocolOp: operations.LDAP_RES_INTERMEDIATE,
type: 'IntermediateResponse',
responseName: undefined,
responseValue: undefined,
controls: []
})
t.equal(res.type, 'IntermediateResponse')
})
t.test('constructor with args', async t => {
const res = new IntermediateResponse({
responseName: '1.2.3',
responseValue: 'foo'
})
t.strictSame(res.pojo, {
messageId: 1,
protocolOp: operations.LDAP_RES_INTERMEDIATE,
type: 'IntermediateResponse',
responseName: '1.2.3',
responseValue: 'foo',
controls: []
})
})
t.end()
})
tap.test('.responseName', t => {
t.test('sets/gets', async t => {
const res = new IntermediateResponse()
t.equal(res.responseName, undefined)
res.responseName = '1.2.3'
t.equal(res.responseName, '1.2.3')
})
t.test('rejects bad value', async t => {
const res = new IntermediateResponse()
t.throws(
() => {
res.responseName = 'foo bar'
},
'responseName must be a numeric OID'
)
t.throws(
() => {
res.responseName = 1.2
},
'responseName must be a numeric OID'
)
})
t.end()
})
tap.test('.responseValue', t => {
t.test('sets/gets', async t => {
const res = new IntermediateResponse()
t.equal(res.responseValue, undefined)
res.responseValue = '1.2.3'
t.equal(res.responseValue, '1.2.3')
})
t.test('rejects bad value', async t => {
const res = new IntermediateResponse()
t.throws(
() => {
res.responseValue = { foo: 'foo' }
},
'responseValue must be a string'
)
})
t.end()
})
tap.test('_toBer', t => {
t.test('converts instance to BER', async t => {
let res = new IntermediateResponse({
messageId: 2,
responseName: '1.2.3',
responseValue: 'foo'
})
let writer = new BerWriter()
res._toBer(writer)
t.equal(
Buffer.from(intermediateResponseBytes.slice(5)).compare(writer.buffer),
0
)
res = new IntermediateResponse({
messageId: 2,
responseName: '1.2.3'
})
writer = new BerWriter()
res._toBer(writer)
t.equal(
Buffer.from(intermediateResponseNoValueBytes.slice(5)).compare(writer.buffer),
0
)
res = new IntermediateResponse({
messageId: 2,
responseValue: 'foo'
})
writer = new BerWriter()
res._toBer(writer)
t.equal(
Buffer.from(intermediateResponseNoNameBytes.slice(5)).compare(writer.buffer),
0
)
})
t.end()
})
tap.test('_pojo', t => {
t.test('returns a pojo representation', async t => {
const req = new IntermediateResponse({
responseName: '1.2.3',
responseValue: 'foo'
})
t.strictSame(req._pojo(), {
responseName: '1.2.3',
responseValue: 'foo'
})
})
t.end()
})
tap.test('#parseToPojo', t => {
t.test('throws if operation incorrect', async t => {
const reqBuffer = Buffer.from(intermediateResponseBytes)
reqBuffer[5] = 0x61
const reader = new BerReader(reqBuffer)
reader.readSequence()
reader.readInt()
t.throws(
() => IntermediateResponse.parseToPojo(reader),
Error('found wrong protocol operation: 0x61')
)
})
t.test('returns a pojo representation', async t => {
let reqBuffer = Buffer.from(intermediateResponseBytes)
let reader = new BerReader(reqBuffer)
reader.readSequence()
reader.readInt()
let pojo = IntermediateResponse.parseToPojo(reader)
t.equal(pojo.protocolOp, operations.LDAP_RES_INTERMEDIATE)
t.equal(pojo.responseName, '1.2.3')
t.equal(pojo.responseValue, 'foo')
reqBuffer = Buffer.from(intermediateResponseNoNameBytes)
reader = new BerReader(reqBuffer)
reader.readSequence()
reader.readInt()
pojo = IntermediateResponse.parseToPojo(reader)
t.equal(pojo.responseName, undefined)
t.equal(pojo.responseValue, 'foo')
})
t.end()
})

View File

@ -0,0 +1,172 @@
'use strict'
const { operations } = require('@ldapjs/protocol')
const Change = require('@ldapjs/change')
const LdapMessage = require('../ldap-message')
/**
* Implements the MODIFY request message as described in
* https://www.rfc-editor.org/rfc/rfc4511.html#section-4.6.
*
* Changes should be in the order of operation as described in
* the spec. If sorting is desired, sort the array prior to
* adding it to the request.
*
* @example <caption>Sorting Changes</caption>
* const {ModifyRequest} = require('@ldapjs/messages')
* const Change = require('@ldapjs/change')
* const changes = someArrayOfChanges.sort(Change.sort)
* const req = new ModifyRequest({
* object: 'dn=foo,dc=example,dc=com',
* changes
* })
*/
class ModifyRequest extends LdapMessage {
#object
#changes
/**
* @typedef {LdapMessageOptions} ModifyRequestOptions
* @property {string|null} [object] The LDAP object (DN) to modify.
* @property {import('@ldapjs/change')[]} [changes] The set of changes to
* apply.
*/
/**
* @param {ModifyRequestOptions} [options]
*/
constructor (options = {}) {
options.protocolOp = operations.LDAP_REQ_MODIFY
super(options)
this.#object = options.object || null
this.changes = options.changes || []
}
/**
* A copy of the set of changes to be applied to the LDAP object.
*
* @returns {import('@ldapjs/change')[]}
*/
get changes () {
return this.#changes.slice(0)
}
/**
* Define the set of changes to apply to the LDAP object.
*
* @param {import('@ldapjs/change')[]} values
*
* @throws When `values` is not an array or contains any elements that
* are not changes.
*/
set changes (values) {
this.#changes = []
if (Array.isArray(values) === false) {
throw Error('changes must be an array')
}
for (let change of values) {
if (Change.isChange(change) === false) {
throw Error('change must be an instance of Change or a Change-like object')
}
if (Object.prototype.toString.call(change) !== '[object LdapChange]') {
change = new Change(change)
}
this.#changes.push(change)
}
}
/**
* The object (DN) to be modified.
*
* @returns {string}
*/
get object () {
return this.#object
}
/**
* Define the object (DN) to be modified.
*
* @param {string} value
*/
set object (value) {
this.#object = value
}
/**
* The name of the request type.
*
* @type {string}
*/
get type () {
return 'ModifyRequest'
}
get _dn () {
return this.#object
}
/**
* Internal use only.
*
* @param {import('@ldapjs/asn1').BerWriter} ber
*
* @returns {import('@ldapjs/asn1').BerWriter}
*/
_toBer (ber) {
ber.startSequence(operations.LDAP_REQ_MODIFY)
ber.writeString(this.#object.toString())
ber.startSequence()
for (const change of this.#changes) {
ber.appendBuffer(change.toBer().buffer)
}
ber.endSequence()
ber.endSequence()
return ber
}
/**
* Internal use only.
*
* @param {object}
*
* @returns {object}
*/
_pojo (obj = {}) {
obj.object = this.#object
obj.changes = this.#changes.map(c => c.pojo)
return obj
}
/**
* Implements the standardized `parseToPojo` method.
*
* @see LdapMessage.parseToPojo
*
* @param {import('@ldapjs/asn1').BerReader} ber
*/
static parseToPojo (ber) {
const protocolOp = ber.readSequence()
if (protocolOp !== operations.LDAP_REQ_MODIFY) {
const op = protocolOp.toString(16).padStart(2, '0')
throw Error(`found wrong protocol operation: 0x${op}`)
}
const object = ber.readString()
const changes = []
ber.readSequence()
const end = ber.offset + ber.length
while (ber.offset < end) {
const change = Change.fromBer(ber)
changes.push(change.pojo)
}
return { protocolOp, object, changes }
}
}
module.exports = ModifyRequest

View File

@ -0,0 +1,207 @@
'use strict'
const tap = require('tap')
const {
modifyRequestBytes
} = require('./_fixtures/message-byte-arrays')
const { operations } = require('@ldapjs/protocol')
const { BerReader } = require('@ldapjs/asn1')
const Attribute = require('@ldapjs/attribute')
const Change = require('@ldapjs/change')
const ModifyRequest = require('./modify-request')
tap.test('constructor', t => {
t.test('with empty params', async t => {
const req = new ModifyRequest()
t.strictSame(req.pojo, {
messageId: 1,
protocolOp: operations.LDAP_REQ_MODIFY,
type: 'ModifyRequest',
object: null,
changes: [],
controls: []
})
})
t.test('with invalid changes options', async t => {
t.throws(
() => new ModifyRequest({ changes: 'foo' }),
Error('changes must be an array')
)
})
t.end()
})
tap.test('.changes', t => {
t.test('gets a copy of the set of changes', async t => {
const changes = [new Change({
modification: new Attribute()
})]
const req = new ModifyRequest({ changes })
const found = req.changes
t.not(changes, found)
t.equal(changes.length, 1)
t.equal(Change.isChange(changes[0]), true)
})
t.test('set throws for non-array', async t => {
const req = new ModifyRequest()
t.throws(
() => {
req.changes = 42
},
Error('changes must be an array')
)
})
t.test('throws for non-change in array', async t => {
const req = new ModifyRequest()
t.throws(
() => {
req.changes = [42]
},
Error('change must be an instance of Change or a Change-like object')
)
})
t.test('converts change-likes to changes', async t => {
const req = new ModifyRequest()
req.changes = [{
operation: 'add',
modification: {
type: 'cn',
values: ['foo']
}
}]
t.equal(req.changes.length, 1)
t.equal(Object.prototype.toString.call(req.changes[0]), '[object LdapChange]')
})
t.end()
})
tap.test('.object', t => {
t.test('gets and sets', async t => {
const req = new ModifyRequest()
t.equal(req.object, null)
req.object = 'foo'
t.equal(req.object, 'foo')
t.equal(req.dn, 'foo')
})
t.end()
})
tap.test('_toBer', t => {
t.test('serializes to ber', async t => {
const req = new ModifyRequest({
messageId: 2,
object: 'uid=jdoe,ou=People,dc=example,dc=com',
changes: [
new Change({
operation: 'delete',
modification: new Attribute({
type: 'givenName',
values: ['John']
})
}),
new Change({
operation: 'add',
modification: new Attribute({
type: 'givenName',
values: ['Jonathan']
})
}),
new Change({
operation: 'replace',
modification: new Attribute({
type: 'cn',
values: ['Jonathan Doe']
})
})
]
})
const expected = Buffer.from(modifyRequestBytes)
const ber = req.toBer()
t.equal(expected.compare(ber.buffer), 0)
})
t.end()
})
tap.test('_pojo', t => {
t.test('serializes to plain object', async t => {
const req = new ModifyRequest({
object: 'foo',
changes: [
new Change({
modification: new Attribute({
type: 'cn',
values: ['bar']
})
})
]
})
t.strictSame(req._pojo(), {
object: 'foo',
changes: [{
operation: 'add',
modification: {
type: 'cn',
values: ['bar']
}
}]
})
})
t.end()
})
tap.test('#parseToPojo', t => {
t.test('throws for wrong op', async t => {
const bytes = Buffer.from(modifyRequestBytes.slice(6))
bytes[0] = 0x61
const reader = new BerReader(bytes)
t.throws(
() => ModifyRequest.parseToPojo(reader),
Error('found wrong protocol operation: 0x61')
)
})
t.test('parses bytes to an object', async t => {
const bytes = Buffer.from(modifyRequestBytes.slice(6))
const reader = new BerReader(bytes)
const pojo = ModifyRequest.parseToPojo(reader)
t.type(pojo, 'Object')
t.strictSame(pojo, {
protocolOp: operations.LDAP_REQ_MODIFY,
object: 'uid=jdoe,ou=People,dc=example,dc=com',
changes: [
{
operation: 'delete',
modification: {
type: 'givenName',
values: ['John']
}
},
{
operation: 'add',
modification: {
type: 'givenName',
values: ['Jonathan']
}
},
{
operation: 'replace',
modification: {
type: 'cn',
values: ['Jonathan Doe']
}
}
]
})
})
t.end()
})

View File

@ -0,0 +1,45 @@
'use strict'
const LdapResult = require('../ldap-result')
const { operations } = require('@ldapjs/protocol')
/**
* Implements the MODIFY response message as described in
* https://www.rfc-editor.org/rfc/rfc4511.html#section-4.6.
*/
class ModifyResponse extends LdapResult {
/**
* @param {LdapResultOptions} options
*/
constructor (options = {}) {
options.protocolOp = operations.LDAP_RES_MODIFY
super(options)
}
/**
* The name of the request type.
*
* @type {string}
*/
get type () {
return 'ModifyResponse'
}
/**
* Implements the standardized `parseToPojo` method.
*
* @see LdapMessage.parseToPojo
*
* @param {import('@ldapjs/asn1').BerReader} ber
*
* @returns {object}
*/
static parseToPojo (ber) {
return LdapResult._parseToPojo({
opCode: operations.LDAP_RES_MODIFY,
berReader: ber
})
}
}
module.exports = ModifyResponse

View File

@ -0,0 +1,55 @@
'use strict'
const tap = require('tap')
const { BerReader } = require('@ldapjs/asn1')
const { operations, resultCodes } = require('@ldapjs/protocol')
const {
modifyResponseBytes
} = require('./_fixtures/message-byte-arrays')
const ModifyResponse = require('./modify-response')
tap.test('basic', async t => {
const res = new ModifyResponse()
t.equal(res.protocolOp, operations.LDAP_RES_MODIFY)
t.equal(res.type, 'ModifyResponse')
})
tap.test('toBer', t => {
tap.test('returns basic bytes', async t => {
const res = new ModifyResponse({
messageId: 2,
status: resultCodes.SUCCESS
})
const ber = res.toBer()
const expected = Buffer.from(modifyResponseBytes)
t.equal(expected.compare(ber.buffer), 0)
})
t.end()
})
tap.test('#parseToPojo', t => {
t.test('parses a basic object', async t => {
const bytes = modifyResponseBytes.slice(5)
const reader = new BerReader(Buffer.from(bytes))
const pojo = ModifyResponse.parseToPojo(reader)
t.strictSame(pojo, {
status: resultCodes.SUCCESS,
matchedDN: '',
diagnosticMessage: '',
referrals: []
})
})
t.test('throws if protocol op is wrong', async t => {
const bytes = modifyResponseBytes.slice(5)
bytes[0] = 0x68
const reader = new BerReader(Buffer.from(bytes))
t.throws(
() => ModifyResponse.parseToPojo(reader),
Error('found wrong protocol operation: 0x68')
)
})
t.end()
})

View File

@ -0,0 +1,222 @@
'use strict'
const LdapMessage = require('../ldap-message')
const { operations } = require('@ldapjs/protocol')
const { DN } = require('@ldapjs/dn')
/**
* Implements the modifydn request message as described in
* https://www.rfc-editor.org/rfc/rfc4511.html#section-4.9.
*/
class ModifyDnRequest extends LdapMessage {
#entry
#newRdn
#deleteOldRdn
#newSuperior
/**
* @typedef {LdapMessageOptions} ModifyDnRequestOptions
* @property {string|null} [entry=null] The path to the LDAP object.
* @property {string|null} [newRdn=null] Path to the new object for the
* entry.
* @property {boolean} [deleteOldRdn=false] Indicates if attributes
* should be removed in the new RDN that were in the old RDN but not the
* new one.
* @property {string} [newSuperior] Path for the new parent for
* the RDN.
*/
/**
* @param {ModifyDnRequestOptions} [options]
*
* @throws When an option is invalid (e.g. `deleteOldRdn` is not a boolean
* value).
*/
constructor (options = {}) {
options.protocolOp = operations.LDAP_REQ_MODRDN
super(options)
this.entry = options.entry || ''
this.newRdn = options.newRdn || ''
this.deleteOldRdn = options.deleteOldRdn ?? false
this.newSuperior = options.newSuperior
}
/**
* The directory path to the object to modify.
*
* @type {import('@ldapjs/dn').DN}
*/
get entry () {
return this.#entry
}
/**
* Define the entry path to the LDAP object.
*
* @param {string | import('@ldapjs/dn').dn} value
*/
set entry (value) {
if (typeof value === 'string') {
this.#entry = DN.fromString(value)
} else if (Object.prototype.toString.call(value) === '[object LdapDn]') {
this.#entry = value
} else {
throw Error('entry must be a valid DN string or instance of LdapDn')
}
}
/**
* Alias of {@link entry}.
*
* @type {import('@ldapjs/dn').DN}
*/
get _dn () {
return this.#entry
}
/**
* The new directory path for the object.
*
* @returns {import('@ldapjs/dn').DN}
*/
get newRdn () {
return this.#newRdn
}
/**
* Define the new entry path to the LDAP object.
*
* @param {string | import('@ldapjs/dn').DN} value
*/
set newRdn (value) {
if (typeof value === 'string') {
this.#newRdn = DN.fromString(value)
} else if (Object.prototype.toString.call(value) === '[object LdapDn]') {
this.#newRdn = value
} else {
throw Error('newRdn must be a valid DN string or instance of LdapDn')
}
}
/**
* Indicates if the old RDN should be removed or not.
*
* @returns {boolean}
*/
get deleteOldRdn () {
return this.#deleteOldRdn
}
set deleteOldRdn (value) {
if (typeof value !== 'boolean') {
throw Error('deleteOldRdn must be a boolean value')
}
this.#deleteOldRdn = value
}
/**
* The new superior for the entry, if any is defined.
*
* @returns {undefined | import('@ldapjs/dn').DN}
*/
get newSuperior () {
return this.#newSuperior
}
/**
* Define the new superior path.
*
* @param {undefined | string | import('@ldapjs/dn').DN} value
*/
set newSuperior (value) {
if (value) {
if (typeof value === 'string') {
this.#newSuperior = DN.fromString(value)
} else if (Object.prototype.toString.call(value) === '[object LdapDn]') {
this.#newSuperior = value
} else {
throw Error('newSuperior must be a valid DN string or instance of LdapDn')
}
} else {
this.#newSuperior = undefined
}
}
/**
* The name of the request type.
*
* @type {string}
*/
get type () {
return 'ModifyDnRequest'
}
/**
* Internal use only.
*
* @param {import('@ldapjs/asn1').BerWriter} ber
*
* @returns {import('@ldapjs/asn1').BerWriter}
*/
_toBer (ber) {
ber.startSequence(operations.LDAP_REQ_MODRDN)
ber.writeString(this.#entry.toString())
ber.writeString(this.#newRdn.toString())
ber.writeBoolean(this.#deleteOldRdn)
/* istanbul ignore else */
if (this.#newSuperior !== undefined) {
ber.writeString(this.#newSuperior.toString(), 0x80)
}
ber.endSequence()
return ber
}
/**
* Internal use only.
*
* @param {object}
*
* @returns {object}
*/
_pojo (obj = {}) {
obj.entry = this.#entry.toString()
obj.newRdn = this.#newRdn.toString()
obj.deleteOldRdn = this.#deleteOldRdn
obj.newSuperior = this.#newSuperior ? this.#newSuperior.toString() : undefined
return obj
}
/**
* Implements the standardized `parseToPojo` method.
*
* @see LdapMessage.parseToPojo
*
* @param {import('@ldapjs/asn1').BerReader} ber
*
* @returns {object}
*/
static parseToPojo (ber) {
const protocolOp = ber.readSequence()
if (protocolOp !== operations.LDAP_REQ_MODRDN) {
const op = protocolOp.toString(16).padStart(2, '0')
throw Error(`found wrong protocol operation: 0x${op}`)
}
const entry = ber.readString()
const newRdn = ber.readString()
const deleteOldRdn = ber.readBoolean()
let newSuperior
/* istanbul ignore else */
if (ber.peek() === 0x80) {
newSuperior = ber.readString(0x80)
}
return { protocolOp, entry, newRdn, deleteOldRdn, newSuperior }
}
}
module.exports = ModifyDnRequest

View File

@ -0,0 +1,233 @@
'use strict'
const tap = require('tap')
const { operations } = require('@ldapjs/protocol')
const { BerReader, BerWriter } = require('@ldapjs/asn1')
const { DN } = require('@ldapjs/dn')
const ModifyDnRequest = require('./modifydn-request')
const {
modifyDnRequestBytes
} = require('./_fixtures/message-byte-arrays')
tap.test('basic', t => {
t.test('constructor no args', async t => {
const req = new ModifyDnRequest()
t.strictSame(req.pojo, {
messageId: 1,
protocolOp: operations.LDAP_REQ_MODRDN,
type: 'ModifyDnRequest',
entry: '',
newRdn: '',
deleteOldRdn: false,
newSuperior: undefined,
controls: []
})
})
t.test('constructor with args', async t => {
const req = new ModifyDnRequest({
entry: 'dc=to-move,dc=example,dc=com',
newRdn: 'dc=moved,dc=example,dc=com',
deleteOldRdn: true,
newSuperior: 'dc=example,dc=net'
})
t.strictSame(req.pojo, {
messageId: 1,
protocolOp: operations.LDAP_REQ_MODRDN,
type: 'ModifyDnRequest',
entry: 'dc=to-move,dc=example,dc=com',
newRdn: 'dc=moved,dc=example,dc=com',
deleteOldRdn: true,
newSuperior: 'dc=example,dc=net',
controls: []
})
})
t.test('.type', async t => {
const req = new ModifyDnRequest()
t.equal(req.type, 'ModifyDnRequest')
})
t.end()
})
tap.test('.entry', t => {
t.test('sets and gets', async t => {
const req = new ModifyDnRequest()
req.entry = 'foo=bar'
t.equal(Object.prototype.toString.call(req.entry), '[object LdapDn]')
t.equal(req.entry.toString(), 'foo=bar')
t.equal(req._dn.toString(), 'foo=bar')
req.entry = DN.fromString('cn=foo')
t.equal(req.entry.toString(), 'cn=foo')
})
t.test('throws for bad value', async t => {
const req = new ModifyDnRequest()
t.throws(
() => {
req.entry = { cn: 'foo' }
},
'entry must be a valid DN string or instance of LdapDN'
)
})
t.end()
})
tap.test('.newRdn', t => {
t.test('sets and gets', async t => {
const req = new ModifyDnRequest()
req.newRdn = 'foo=bar'
t.equal(Object.prototype.toString.call(req.newRdn), '[object LdapDn]')
t.equal(req.newRdn.toString(), 'foo=bar')
req.newRdn = DN.fromString('cn=foo')
t.equal(req.newRdn.toString(), 'cn=foo')
})
t.test('throws for bad value', async t => {
const req = new ModifyDnRequest()
t.throws(
() => {
req.newRdn = { cn: 'foo' }
},
'newRdn must be a valid DN string or instance of LdapDN'
)
})
t.end()
})
tap.test('.deleteOldRdn', t => {
t.test('throws for wrong type', async t => {
t.throws(
() => new ModifyDnRequest({ deleteOldRdn: 'false' }),
'deleteOldRdn must be a boolean value'
)
})
t.test('sets and gets', async t => {
const req = new ModifyDnRequest()
t.equal(req.deleteOldRdn, false)
req.deleteOldRdn = true
t.type(req.deleteOldRdn, 'boolean')
t.equal(req.deleteOldRdn, true)
})
t.end()
})
tap.test('.newSuperior', t => {
t.test('sets and gets', async t => {
const req = new ModifyDnRequest()
req.newSuperior = 'foo=bar'
t.equal(Object.prototype.toString.call(req.newSuperior), '[object LdapDn]')
t.equal(req.newSuperior.toString(), 'foo=bar')
req.newSuperior = null
t.equal(req.newSuperior, undefined)
req.newSuperior = DN.fromString('cn=foo')
t.equal(req.newSuperior.toString(), 'cn=foo')
})
t.test('throws for bad value', async t => {
const req = new ModifyDnRequest()
t.throws(
() => {
req.newSuperior = { cn: 'foo' }
},
'newSuperior must be a valid DN string or instance of LdapDN'
)
})
t.end()
})
tap.test('_toBer', async t => {
t.test('converts instance to BER', async t => {
const req = new ModifyDnRequest({
entry: 'uid=jdoe,ou=People,dc=example,dc=com',
newRdn: 'uid=john.doe',
deleteOldRdn: true,
newSuperior: 'ou=Users,dc=example,dc=com'
})
const writer = new BerWriter()
req._toBer(writer)
t.equal(
Buffer.from(modifyDnRequestBytes.slice(5)).compare(writer.buffer),
0
)
})
t.end()
})
tap.test('_pojo', t => {
t.test('returns a pojo representation', async t => {
let req = new ModifyDnRequest({
entry: 'cn=bar,dc=example,dc=com',
newRdn: 'cn=foo'
})
t.strictSame(req._pojo(), {
entry: 'cn=bar,dc=example,dc=com',
newRdn: 'cn=foo',
deleteOldRdn: false,
newSuperior: undefined
})
req = new ModifyDnRequest({
entry: 'cn=bar,dc=example,dc=com',
newRdn: 'cn=foo',
newSuperior: 'ou=people,dc=example,dc=com'
})
t.strictSame(req._pojo(), {
entry: 'cn=bar,dc=example,dc=com',
newRdn: 'cn=foo',
deleteOldRdn: false,
newSuperior: 'ou=people,dc=example,dc=com'
})
})
t.end()
})
tap.test('#parseToPojo', t => {
t.test('throws if operation incorrect', async t => {
const reqBuffer = Buffer.from(modifyDnRequestBytes)
reqBuffer[5] = 0x61
const reader = new BerReader(reqBuffer)
reader.readSequence()
reader.readInt()
t.throws(
() => ModifyDnRequest.parseToPojo(reader),
Error('found wrong protocol operation: 0x61')
)
})
t.test('returns a pojo representation', async t => {
const reqBuffer = Buffer.from(modifyDnRequestBytes)
const reader = new BerReader(reqBuffer)
reader.readSequence()
reader.readInt()
const pojo = ModifyDnRequest.parseToPojo(reader)
t.equal(pojo.protocolOp, operations.LDAP_REQ_MODRDN)
t.equal(pojo.entry, 'uid=jdoe,ou=People,dc=example,dc=com')
t.equal(pojo.newRdn, 'uid=john.doe')
t.equal(pojo.deleteOldRdn, true)
t.equal(pojo.newSuperior, 'ou=Users,dc=example,dc=com')
})
t.end()
})

View File

@ -0,0 +1,45 @@
'use strict'
const LdapResult = require('../ldap-result')
const { operations } = require('@ldapjs/protocol')
/**
* Implements the modifydn response message as described in
* https://www.rfc-editor.org/rfc/rfc4511.html#section-4.9.
*/
class ModifyDnResponse extends LdapResult {
/**
* @param {LdapResultOptions} options
*/
constructor (options = {}) {
options.protocolOp = operations.LDAP_RES_MODRDN
super(options)
}
/**
* The name of the request type.
*
* @type {string}
*/
get type () {
return 'ModifyDnResponse'
}
/**
* Implements the standardized `parseToPojo` method.
*
* @see LdapMessage.parseToPojo
*
* @param {import('@ldapjs/asn1').BerReader} ber
*
* @returns {object}
*/
static parseToPojo (ber) {
return LdapResult._parseToPojo({
opCode: operations.LDAP_RES_MODRDN,
berReader: ber
})
}
}
module.exports = ModifyDnResponse

View File

@ -0,0 +1,56 @@
'use strict'
const tap = require('tap')
const { BerReader } = require('@ldapjs/asn1')
const { operations } = require('@ldapjs/protocol')
const {
modifyDnResponseBytes
} = require('./_fixtures/message-byte-arrays')
const ModifyDnResponse = require('./modifydn-response')
tap.test('basic', async t => {
const res = new ModifyDnResponse()
t.equal(res.protocolOp, operations.LDAP_RES_MODRDN)
t.equal(res.type, 'ModifyDnResponse')
})
tap.test('toBer', t => {
tap.test('returns basic bytes', async t => {
const res = new ModifyDnResponse({ messageId: 2 })
const ber = res.toBer()
const expected = Buffer.from(modifyDnResponseBytes)
t.equal(expected.compare(ber.buffer), 0)
})
t.comment('see the LdapResult test suite for further tests')
t.end()
})
tap.test('#parseToPojo', t => {
t.test('parses a basic object', async t => {
const bytes = modifyDnResponseBytes.slice(5)
const reader = new BerReader(Buffer.from(bytes))
const pojo = ModifyDnResponse.parseToPojo(reader)
t.strictSame(pojo, {
status: 0,
matchedDN: '',
diagnosticMessage: '',
referrals: []
})
})
t.test('throws if protocol op is wrong', async t => {
const bytes = modifyDnResponseBytes.slice(5)
bytes[0] = 0x68
const reader = new BerReader(Buffer.from(bytes))
t.throws(
() => ModifyDnResponse.parseToPojo(reader),
Error('found wrong protocol operation: 0x68')
)
})
t.comment('see the LdapResult test suite for further tests')
t.end()
})

View File

@ -0,0 +1,515 @@
'use strict'
const LdapMessage = require('../ldap-message')
const { operations, search } = require('@ldapjs/protocol')
const { DN } = require('@ldapjs/dn')
const filter = require('@ldapjs/filter')
const { BerReader, BerTypes } = require('@ldapjs/asn1')
const warning = require('../deprecations')
const recognizedScopes = new Map([
['base', [search.SCOPE_BASE_OBJECT, 'base']],
['single', [search.SCOPE_ONE_LEVEL, 'single', 'one']],
['subtree', [search.SCOPE_SUBTREE, 'subtree', 'sub']]
])
const scopeAliasToScope = alias => {
alias = typeof alias === 'string' ? alias.toLowerCase() : alias
if (recognizedScopes.has(alias)) {
return recognizedScopes.get(alias)[0]
}
for (const value of recognizedScopes.values()) {
if (value.includes(alias)) {
return value[0]
}
}
return undefined
}
const isValidAttributeString = str => {
// special filter strings
if (['*', '1.1', '+'].includes(str) === true) {
return true
}
// "@<object_clas>"
if (/^@[a-zA-Z][\w\d.-]*$/.test(str) === true) {
return true
}
// ascii attribute names per RFC 4512 §2.5
if (/^[a-zA-Z][\w\d.;-]*$/.test(str) === true) {
return true
}
// Matches the non-standard `range=<low>-<high>` ActiveDirectory
// extension as described in §3.1.1.3.1.3.3 (revision 57.0) of
// https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-adts/d2435927-0999-4c62-8c6d-13ba31a52e1a.
if (/^[a-zA-Z][\w\d.-]*(;[\w\d.-]+)*;range=\d+-(\d+|\*)(;[\w\d.-]+)*$/.test(str) === true) {
return true
}
return false
}
/**
* Implements the add request message as described in
* https://www.rfc-editor.org/rfc/rfc4511.html#section-4.5.1.
*
* Various constants for searching and options can be used from the `search`
* object in the `@ldapjs/protocol` package. The same constants are exported
* here as static properties for convenience.
*/
class SearchRequest extends LdapMessage {
/**
* Limit searches to the specified {@link baseObject}.
*
* @type {number}
*/
static SCOPE_BASE = search.SCOPE_BASE_OBJECT
/**
* Limit searches to the immediate children of the specified
* {@link baseObject}.
*
* @type {number}
*/
static SCOPE_SINGLE = search.SCOPE_ONE_LEVEL
/**
* Limit searches to the {@link baseObject} and all descendents of that
* object.
*
* @type {number}
*/
static SCOPE_SUBTREE = search.SCOPE_SUBTREE
/**
* Do not perform any dereferencing of aliases at all.
*
* @type {number}
*/
static DEREF_ALIASES_NEVER = search.NEVER_DEREF_ALIASES
/**
* Dereference aliases in subordinate searches of the {@link baseObject}.
*
* @type {number}
*/
static DEREF_IN_SEARCHING = search.DEREF_IN_SEARCHING
/**
* Dereference aliases when finding the base object only.
*
* @type {number}
*/
static DEREF_BASE_OBJECT = search.DEREF_BASE_OBJECT
/**
* Dereference aliases when finding the base object and when searching
* subordinates.
*
* @type {number}
*/
static DEREF_ALWAYS = search.DEREF_ALWAYS
#baseObject
#scope
#derefAliases
#sizeLimit
#timeLimit
#typesOnly
#filter
#attributes = []
/**
* @typedef {LdapMessageOptions} SearchRequestOptions
* @property {string | import('@ldapjs/dn').DN} baseObject The path to the
* LDAP object that will serve as the basis of the search.
* @property {number | string} scope The type of search to be performed.
* May be one of {@link SCOPE_BASE}, {@link SCOPE_SINGLE},
* {@link SCOPE_SUBTREE}, `'base'`, `'single'` (`'one'`), or `'subtree'`
* (`'sub'`).
* @property {number} derefAliases Indicates if aliases should be dereferenced
* during searches. May be one of {@link DEREF_ALIASES_NEVER},
* {@link DEREF_BASE_OBJECT}, {@link DEREF_IN_SEARCHING}, or
* {@link DEREF_ALWAYS}.
* @property {number} sizeLimit The number of search results the server should
* limit the result set to. `0` indicates no desired limit.
* @property {number} timeLimit The number of seconds the server should work
* before aborting the search request. `0` indicates no desired limit.
* @property {boolean} typesOnly Indicates if only attribute names should
* be returned (`true`), or both names and values should be returned (`false`).
* @property {string | import('@ldapjs/filter').FilterString} filter The
* filter to apply when searching.
* @property {string[]} attributes A set of attribute filtering strings
* to apply. See the docs for the {@link attributes} setter.
*/
/**
* @param {SearchRequestOptions} options
*/
constructor (options = {}) {
options.protocolOp = operations.LDAP_REQ_SEARCH
super(options)
this.baseObject = options.baseObject ?? ''
this.scope = options.scope ?? search.SCOPE_BASE_OBJECT
this.derefAliases = options.derefAliases ?? search.NEVER_DEREF_ALIASES
this.sizeLimit = options.sizeLimit ?? 0
this.timeLimit = options.timeLimit ?? 0
this.typesOnly = options.typesOnly ?? false
this.filter = options.filter ?? new filter.PresenceFilter({ attribute: 'objectclass' })
this.attributes = options.attributes ?? []
}
/**
* Alias of {@link baseObject}.
*
* @type {import('@ldapjs/dn').DN}
*/
get _dn () {
return this.#baseObject
}
/**
* The name of the request type.
*
* @type {string}
*/
get type () {
return 'SearchRequest'
}
/**
* The list of attributes to match against.
*
* @returns {string[]}
*/
get attributes () {
return this.#attributes
}
/**
* Set the list of attributes to match against. Overwrites any existing
* attributes. The list is a set of spec defined strings. They are not
* instances of `@ldapjs/attribute`.
*
* See:
* + https://www.rfc-editor.org/rfc/rfc4511.html#section-4.5.1.8
* + https://www.rfc-editor.org/rfc/rfc3673.html
* + https://www.rfc-editor.org/rfc/rfc4529.html
*
* @param {string)[]} attrs
*/
set attributes (attrs) {
if (Array.isArray(attrs) === false) {
throw Error('attributes must be an array of attribute strings')
}
const newAttrs = []
for (const attr of attrs) {
if (typeof attr === 'string' && isValidAttributeString(attr) === true) {
newAttrs.push(attr)
} else if (typeof attr === 'string' && attr === '') {
// TODO: emit warning about spec violation via log and/or telemetry
warning.emit('LDAP_ATTRIBUTE_SPEC_ERR_001')
} else {
throw Error('attribute must be a valid string')
}
}
this.#attributes = newAttrs
}
/**
* The base LDAP object that the search will start from.
*
* @returns {import('@ldapjs/dn').DN}
*/
get baseObject () {
return this.#baseObject
}
/**
* Define the base LDAP object to start searches from.
*
* @param {string | import('@ldapjs/dn').DN} obj
*/
set baseObject (obj) {
if (typeof obj === 'string') {
this.#baseObject = DN.fromString(obj)
} else if (Object.prototype.toString.call(obj) === '[object LdapDn]') {
this.#baseObject = obj
} else {
throw Error('baseObject must be a DN string or DN instance')
}
}
/**
* The alias dereferencing method that will be provided to the server.
* May be one of {@link DEREF_ALIASES_NEVER}, {@link DEREF_IN_SEARCHING},
* {@link DEREF_BASE_OBJECT},or {@link DEREF_ALWAYS}.
*
* @returns {number}
*/
get derefAliases () {
return this.#derefAliases
}
/**
* Define the dereferencing method that will be provided to the server.
* May be one of {@link DEREF_ALIASES_NEVER}, {@link DEREF_IN_SEARCHING},
* {@link DEREF_BASE_OBJECT},or {@link DEREF_ALWAYS}.
*
* @param {number} value
*/
set derefAliases (value) {
if (Number.isInteger(value) === false) {
throw Error('derefAliases must be set to an integer')
}
this.#derefAliases = value
}
/**
* The filter that will be used in the search.
*
* @returns {import('@ldapjs/filter').FilterString}
*/
get filter () {
return this.#filter
}
/**
* Define the filter to use in the search.
*
* @param {string | import('@ldapjs/filter').FilterString} value
*/
set filter (value) {
if (
typeof value !== 'string' &&
Object.prototype.toString.call(value) !== '[object FilterString]'
) {
throw Error('filter must be a string or a FilterString instance')
}
if (typeof value === 'string') {
this.#filter = filter.parseString(value)
} else {
this.#filter = value
}
}
/**
* The current search scope value. Can be matched against the exported
* scope statics.
*
* @returns {number}
*
* @throws When the scope is set to an unrecognized scope constant.
*/
get scope () {
return this.#scope
}
/**
* Define the scope of the search.
*
* @param {number|string} value Accepts one of {@link SCOPE_BASE},
* {@link SCOPE_SINGLE}, or {@link SCOPE_SUBTREE}. Or, as a string, one of
* "base", "single", "one", "subtree", or "sub".
*
* @throws When the provided scope does not resolve to a recognized scope.
*/
set scope (value) {
const resolvedScope = scopeAliasToScope(value)
if (resolvedScope === undefined) {
throw Error(value + ' is an invalid search scope')
}
this.#scope = resolvedScope
}
/**
* The current search scope value as a string name.
*
* @returns {string} One of 'base', 'single', or 'subtree'.
*
* @throws When the scope is set to an unrecognized scope constant.
*/
get scopeName () {
switch (this.#scope) {
case search.SCOPE_BASE_OBJECT:
return 'base'
case search.SCOPE_ONE_LEVEL:
return 'single'
case search.SCOPE_SUBTREE:
return 'subtree'
}
}
/**
* The number of entries to limit search results to.
*
* @returns {number}
*/
get sizeLimit () {
return this.#sizeLimit
}
/**
* Define the number of entries to limit search results to.
*
* @param {number} value `0` indicates no restriction.
*/
set sizeLimit (value) {
if (Number.isInteger(value) === false) {
throw Error('sizeLimit must be an integer')
}
this.#sizeLimit = value
}
/**
* The number of seconds that the search should be limited to for execution.
* A value of `0` indicates a willingness to wait as long as the server is
* willing to work.
*
* @returns {number}
*/
get timeLimit () {
return this.#timeLimit
}
/**
* Define the number of seconds to wait for a search result before the server
* should abort the search.
*
* @param {number} value `0` indicates no time limit restriction.
*/
set timeLimit (value) {
if (Number.isInteger(value) === false) {
throw Error('timeLimit must be an integer')
}
this.#timeLimit = value
}
/**
* Indicates if only attribute names (`true`) should be returned, or if both
* attribute names and attribute values (`false`) should be returned.
*
* @returns {boolean}
*/
get typesOnly () {
return this.#typesOnly
}
/**
* Define if the search results should include only the attributes names
* or attribute names and attribute values.
*
* @param {boolean} value `false` for both names and values, `true` for
* names only.
*/
set typesOnly (value) {
if (typeof value !== 'boolean') {
throw Error('typesOnly must be set to a boolean value')
}
this.#typesOnly = value
}
/**
* Internal use only.
*
* @param {import('@ldapjs/asn1').BerWriter} ber
*
* @returns {import('@ldapjs/asn1').BerWriter}
*/
_toBer (ber) {
ber.startSequence(operations.LDAP_REQ_SEARCH)
ber.writeString(this.#baseObject.toString())
ber.writeEnumeration(this.#scope)
ber.writeEnumeration(this.#derefAliases)
ber.writeInt(this.#sizeLimit)
ber.writeInt(this.#timeLimit)
ber.writeBoolean(this.#typesOnly)
ber.appendBuffer(this.#filter.toBer().buffer)
ber.startSequence(BerTypes.Sequence | BerTypes.Constructor)
for (const attr of this.#attributes) {
ber.writeString(attr)
}
ber.endSequence()
ber.endSequence()
return ber
}
/**
* Internal use only.
*
* @param {object}
*
* @returns {object}
*/
_pojo (obj = {}) {
obj.baseObject = this.baseObject.toString()
obj.scope = this.scopeName
obj.derefAliases = this.derefAliases
obj.sizeLimit = this.sizeLimit
obj.timeLimit = this.timeLimit
obj.typesOnly = this.typesOnly
obj.filter = this.filter.toString()
obj.attributes = []
for (const attr of this.#attributes) {
obj.attributes.push(attr)
}
return obj
}
/**
* Implements the standardized `parseToPojo` method.
*
* @see LdapMessage.parseToPojo
*
* @param {import('@ldapjs/asn1').BerReader} ber
*
* @returns {object}
*/
static parseToPojo (ber) {
const protocolOp = ber.readSequence()
if (protocolOp !== operations.LDAP_REQ_SEARCH) {
const op = protocolOp.toString(16).padStart(2, '0')
throw Error(`found wrong protocol operation: 0x${op}`)
}
const baseObject = ber.readString()
const scope = ber.readEnumeration()
const derefAliases = ber.readEnumeration()
const sizeLimit = ber.readInt()
const timeLimit = ber.readInt()
const typesOnly = ber.readBoolean()
const filterTag = ber.peek()
const filterBuffer = ber.readRawBuffer(filterTag)
const parsedFilter = filter.parseBer(new BerReader(filterBuffer))
const attributes = []
// Advance to the first attribute sequence in the set
// of attribute sequences.
ber.readSequence()
const endOfAttributesPos = ber.offset + ber.length
while (ber.offset < endOfAttributesPos) {
const attribute = ber.readString()
attributes.push(attribute)
}
return {
protocolOp,
baseObject,
scope,
derefAliases,
sizeLimit,
timeLimit,
typesOnly,
filter: parsedFilter.toString(),
attributes
}
}
}
module.exports = SearchRequest

View File

@ -0,0 +1,438 @@
'use strict'
const tap = require('tap')
const { operations } = require('@ldapjs/protocol')
const filter = require('@ldapjs/filter')
const SearchRequest = require('./search-request')
const { DN } = require('@ldapjs/dn')
const { BerReader, BerWriter } = require('@ldapjs/asn1')
const warning = require('../deprecations')
// Silence the standard warning logs. We will test the messages explicitly.
process.removeAllListeners('warning')
const {
searchRequestBytes
} = require('./_fixtures/message-byte-arrays')
tap.test('basic', t => {
t.test('constructor no args', async t => {
const req = new SearchRequest()
const pojo = req.pojo
t.strictSame(pojo, {
messageId: 1,
protocolOp: operations.LDAP_REQ_SEARCH,
type: 'SearchRequest',
baseObject: '',
scope: 'base',
derefAliases: SearchRequest.DEREF_ALIASES_NEVER,
sizeLimit: 0,
timeLimit: 0,
typesOnly: false,
filter: '(objectclass=*)',
attributes: [],
controls: []
})
t.equal(req.type, 'SearchRequest')
})
t.test('constructor with args', async t => {
const req = new SearchRequest({
baseObject: 'cn=foo,dc=example,dc=com',
scope: SearchRequest.SCOPE_SUBTREE,
derefAliases: SearchRequest.DEREF_BASE_OBJECT,
sizeLimit: 1,
timeLimit: 1,
typesOnly: true,
filter: new filter.EqualityFilter({ attribute: 'cn', value: 'foo' }),
attributes: ['*']
})
const pojo = req.pojo
t.strictSame(pojo, {
messageId: 1,
protocolOp: operations.LDAP_REQ_SEARCH,
type: 'SearchRequest',
baseObject: 'cn=foo,dc=example,dc=com',
scope: 'subtree',
derefAliases: SearchRequest.DEREF_BASE_OBJECT,
sizeLimit: 1,
timeLimit: 1,
typesOnly: true,
filter: '(cn=foo)',
attributes: ['*'],
controls: []
})
})
t.end()
})
tap.test('.attributes', t => {
t.test('sets/gets', async t => {
const req = new SearchRequest()
t.strictSame(req.attributes, [])
req.attributes = ['*']
t.strictSame(req.attributes, ['*'])
})
t.test('set overwrites current list', async t => {
const req = new SearchRequest({
attributes: ['1.1']
})
req.attributes = ['1.1', '*', '@foo3-bar.2', 'cn', 'sn;lang-en']
t.strictSame(req.attributes, ['1.1', '*', '@foo3-bar.2', 'cn', 'sn;lang-en'])
})
t.test('throws if not an array', async t => {
t.throws(
() => new SearchRequest({ attributes: '*' }),
'attributes must be an array of attribute strings'
)
})
t.test('throws if array contains non-attribute', async t => {
const input = [
'*',
'not allowed'
]
t.throws(
() => new SearchRequest({ attributes: input }),
'attribute must be a valid string'
)
})
t.test('supports single character names (issue #2)', async t => {
const req = new SearchRequest({
attributes: ['a']
})
t.strictSame(req.attributes, ['a'])
})
t.test('supports multiple attribute options', async t => {
const req = new SearchRequest({
attributes: ['abc;lang-en;lang-es']
})
t.strictSame(req.attributes, ['abc;lang-en;lang-es'])
})
t.test('supports range options', async t => {
const req = new SearchRequest({
attributes: [
'a;range=0-*',
'abc;range=100-200',
'def;range=0-5;lang-en',
'ghi;lang-en;range=6-10',
'jkl;lang-en;range=11-15;lang-es'
]
})
t.strictSame(req.attributes, [
'a;range=0-*',
'abc;range=100-200',
'def;range=0-5;lang-en',
'ghi;lang-en;range=6-10',
'jkl;lang-en;range=11-15;lang-es'
])
})
t.test('throws if array contains an invalid range', async t => {
const input = ['a;range=*-100']
t.throws(
() => new SearchRequest({ attributes: input }),
'attribute must be a valid string'
)
})
t.test('skip empty attribute name', async t => {
process.on('warning', handler)
t.teardown(async () => {
process.removeListener('warning', handler)
warning.emitted.set('LDAP_ATTRIBUTE_SPEC_ERR_001', false)
})
const req = new SearchRequest({
attributes: ['abc', '']
})
t.strictSame(req.attributes, ['abc'])
function handler (error) {
t.equal(error.message, 'received attempt to define attribute with an empty name: attribute skipped.')
t.end()
}
})
t.end()
})
tap.test('.baseObject', t => {
t.test('sets/gets', async t => {
const req = new SearchRequest()
t.equal(req.baseObject.toString(), '')
req.baseObject = 'dc=example,dc=com'
t.equal(req.baseObject.toString(), 'dc=example,dc=com')
req.baseObject = DN.fromString('dc=example,dc=net')
t.equal(req.baseObject.toString(), 'dc=example,dc=net')
t.equal(req._dn.toString(), 'dc=example,dc=net')
})
t.test('throws for non-DN object', async t => {
const req = new SearchRequest()
t.throws(
() => {
req.baseObject = ['foo']
},
'baseObject must be a DN string or DN instance'
)
})
t.end()
})
tap.test('.derefAliases', t => {
t.test('sets/gets', async t => {
const req = new SearchRequest()
t.equal(req.derefAliases, SearchRequest.DEREF_ALIASES_NEVER)
req.derefAliases = SearchRequest.DEREF_ALWAYS
t.equal(req.derefAliases, SearchRequest.DEREF_ALWAYS)
})
t.test('throws for bad value', async t => {
const req = new SearchRequest()
t.throws(
() => {
req.derefAliases = '0'
},
'derefAliases must be set to an integer'
)
})
t.end()
})
tap.test('.filter', t => {
t.test('sets/gets', async t => {
const req = new SearchRequest()
t.equal(req.filter.toString(), '(objectclass=*)')
req.filter = '(cn=foo)'
t.equal(req.filter.toString(), '(cn=foo)')
req.filter = new filter.EqualityFilter({ attribute: 'sn', value: 'bar' })
t.equal(req.filter.toString(), '(sn=bar)')
})
t.test('throws for bad value', async t => {
const expected = 'filter must be a string or a FilterString instance'
const req = new SearchRequest()
t.throws(
() => {
req.filter = ['foo']
},
expected
)
t.throws(
() => {
req.filter = { attribute: 'cn', value: 'foo' }
},
expected
)
})
t.end()
})
tap.test('.scope', t => {
t.test('sets/gets', async t => {
const req = new SearchRequest()
t.equal(req.scopeName, 'base')
t.equal(req.scope, 0)
req.scope = SearchRequest.SCOPE_SINGLE
t.equal(req.scopeName, 'single')
t.equal(req.scope, 1)
req.scope = SearchRequest.SCOPE_SUBTREE
t.equal(req.scopeName, 'subtree')
t.equal(req.scope, 2)
req.scope = 'SUB'
t.equal(req.scopeName, 'subtree')
t.equal(req.scope, 2)
req.scope = 'base'
t.equal(req.scopeName, 'base')
t.equal(req.scope, 0)
})
t.test('throws for invalid value', async t => {
const expected = ' is an invalid search scope'
const req = new SearchRequest()
t.throws(
() => {
req.scope = 'nested'
},
'nested' + expected
)
t.throws(
() => {
req.scope = 42
},
42 + expected
)
})
t.end()
})
tap.test('.sizeLimit', t => {
t.test('sets/gets', async t => {
const req = new SearchRequest()
t.equal(req.sizeLimit, 0)
req.sizeLimit = 15
t.equal(req.sizeLimit, 15)
})
t.test('throws for bad value', async t => {
const req = new SearchRequest()
t.throws(
() => {
req.sizeLimit = 15.5
},
'sizeLimit must be an integer'
)
})
t.end()
})
tap.test('.timeLimit', t => {
t.test('sets/gets', async t => {
const req = new SearchRequest()
t.equal(req.timeLimit, 0)
req.timeLimit = 15
t.equal(req.timeLimit, 15)
})
t.test('throws for bad value', async t => {
const req = new SearchRequest()
t.throws(
() => {
req.timeLimit = 15.5
},
'timeLimit must be an integer'
)
})
t.end()
})
tap.test('.typesOnly', t => {
t.test('sets/gets', async t => {
const req = new SearchRequest()
t.equal(req.typesOnly, false)
req.typesOnly = true
t.equal(req.typesOnly, true)
})
t.test('throws for bad value', async t => {
const req = new SearchRequest()
t.throws(
() => {
req.typesOnly = 'true'
},
'typesOnly must be set to a boolean value'
)
})
t.end()
})
tap.test('_toBer', t => {
tap.test('converts instance to BER', async t => {
const req = new SearchRequest({
messageId: 2,
baseObject: 'dc=example,dc=com',
scope: 'subtree',
derefAliases: SearchRequest.DEREF_ALIASES_NEVER,
sizeLimit: 1000,
timeLimit: 30,
typesOnly: false,
filter: '(&(objectClass=person)(uid=jdoe))',
attributes: ['*', '+']
})
const writer = new BerWriter()
req._toBer(writer)
t.equal(
Buffer.from(searchRequestBytes.slice(5)).compare(writer.buffer),
0
)
})
t.end()
})
tap.test('_pojo', t => {
t.test('returns a pojo representation', async t => {
const req = new SearchRequest()
t.strictSame(req._pojo(), {
baseObject: '',
scope: 'base',
derefAliases: 0,
sizeLimit: 0,
timeLimit: 0,
typesOnly: false,
filter: '(objectclass=*)',
attributes: []
})
})
t.end()
})
tap.test('#parseToPojo', t => {
t.test('throws if operation incorrect', async t => {
const reqBuffer = Buffer.from(searchRequestBytes)
reqBuffer[5] = 0x61
const reader = new BerReader(reqBuffer)
reader.readSequence()
reader.readInt()
t.throws(
() => SearchRequest.parseToPojo(reader),
Error('found wrong protocol operation: 0x61')
)
})
t.test('returns a pojo representation', async t => {
const reqBuffer = Buffer.from(searchRequestBytes)
const reader = new BerReader(reqBuffer)
reader.readSequence()
reader.readInt()
const pojo = SearchRequest.parseToPojo(reader)
t.equal(pojo.protocolOp, operations.LDAP_REQ_SEARCH)
t.equal(pojo.baseObject, 'dc=example,dc=com')
t.equal(pojo.scope, SearchRequest.SCOPE_SUBTREE)
t.equal(pojo.derefAliases, SearchRequest.DEREF_ALIASES_NEVER)
t.equal(pojo.sizeLimit, 1000)
t.equal(pojo.timeLimit, 30)
t.equal(pojo.typesOnly, false)
t.equal(pojo.filter, '(&(objectClass=person)(uid=jdoe))')
t.strictSame(pojo.attributes, ['*', '+'])
})
t.end()
})

View File

@ -0,0 +1,54 @@
'use strict'
const LdapResult = require('../ldap-result')
const { operations } = require('@ldapjs/protocol')
/**
* Implements the search result done response message as described in
* https://www.rfc-editor.org/rfc/rfc4511.html#section-4.5.2.
*/
class SearchResultDone extends LdapResult {
#uri
/**
* @typedef {LdapResultOptions} SearchResultDoneOptions
* @property {string[]} [uri=[]] The set of reference URIs the message is
* providing.
* @property {string[]} [uris] An alias for uri.
*/
/**
* @param {SearchResultDoneOptions} options
*/
constructor (options = {}) {
options.protocolOp = operations.LDAP_RES_SEARCH_DONE
super(options)
}
/**
* The name of the request type.
*
* @type {string}
*/
get type () {
return 'SearchResultDone'
}
/**
* Implements the standardized `parseToPojo` method.
*
* @see LdapMessage.parseToPojo
*
* @param {import('@ldapjs/asn1').BerReader} ber
*
* @returns {object}
*/
static parseToPojo (ber) {
return LdapResult._parseToPojo({
opCode: operations.LDAP_RES_SEARCH_DONE,
berReader: ber
})
}
}
module.exports = SearchResultDone

View File

@ -0,0 +1,52 @@
'use strict'
const tap = require('tap')
const { BerReader } = require('@ldapjs/asn1')
const { operations } = require('@ldapjs/protocol')
const {
searchResultDoneBytes
} = require('./_fixtures/message-byte-arrays')
const SearchResultDone = require('./search-result-done')
tap.test('basic', async t => {
const res = new SearchResultDone()
t.equal(res.protocolOp, operations.LDAP_RES_SEARCH_DONE)
t.equal(res.type, 'SearchResultDone')
})
tap.test('toBer', t => {
tap.test('returns basic bytes', async t => {
const res = new SearchResultDone({ messageId: 2 })
const ber = res.toBer()
const expected = Buffer.from(searchResultDoneBytes)
t.equal(expected.compare(ber.buffer), 0)
})
t.end()
})
tap.test('#parseToPojo', t => {
t.test('parses a basic object', async t => {
const bytes = searchResultDoneBytes.slice(5)
const reader = new BerReader(Buffer.from(bytes))
const pojo = SearchResultDone.parseToPojo(reader)
t.strictSame(pojo, {
status: 0,
matchedDN: '',
diagnosticMessage: '',
referrals: []
})
})
t.test('throws if protocol op is wrong', async t => {
const bytes = searchResultDoneBytes.slice(5)
bytes[0] = 0x68
const reader = new BerReader(Buffer.from(bytes))
t.throws(
() => SearchResultDone.parseToPojo(reader),
Error('found wrong protocol operation: 0x68')
)
})
t.end()
})

View File

@ -0,0 +1,198 @@
'use strict'
const LdapMessage = require('../ldap-message')
const Attribute = require('@ldapjs/attribute')
const { operations } = require('@ldapjs/protocol')
const { DN } = require('@ldapjs/dn')
/**
* Implements the search result entry message as described in
* https://www.rfc-editor.org/rfc/rfc4511.html#section-4.5.2.
*/
class SearchResultEntry extends LdapMessage {
/**
* Path to the LDAP object.
*
* @type {import('@ldapjs/dn').DN}
*/
#objectName
/**
* A set of attribute objects.
*
* @type {import('@ldapjs/attribute')[]}
*/
#attributes = []
/**
* @typedef {LdapMessageOptions} SearchResultEntryOptions
* @property {string | import('@ldapjs/dn').DN} [objectName=''] The path to
* the LDAP object.
* @property {import('@ldapjs/attribute')[]} attributes A set of attributes
* to store at the `entry` path.
*/
/**
* @param {SearchResultEntryOptions} [options]
*
* @throws When the provided attributes list is invalid or the object name
* is not a valid LdapDn object or DN string.
*/
constructor (options = {}) {
options.protocolOp = operations.LDAP_RES_SEARCH_ENTRY
super(options)
this.objectName = options.objectName ?? ''
this.attributes = options.attributes ?? []
}
/**
* Alias of {@link objectName}.
*
* @type {string}
*/
get _dn () {
return this.#objectName
}
/**
* The name of the request type.
*
* @type {string}
*/
get type () {
return 'SearchResultEntry'
}
/**
* Get a copy of the attributes associated with the request.
*
* @returns {import('@ldapjs/attribute')[]}
*/
get attributes () {
return this.#attributes.slice(0)
}
/**
* Set the attributes to be added to the entry. Replaces any existing
* attributes.
*
* @param {object[] | import('@ldapjs/attribute')[]} attrs
*
* @throws If the input is not an array, or any element is not an
* {@link Attribute} or attribute-like object.
*/
set attributes (attrs) {
if (Array.isArray(attrs) === false) {
throw Error('attrs must be an array')
}
const newAttrs = []
for (const attr of attrs) {
if (Attribute.isAttribute(attr) === false) {
throw Error('attr must be an Attribute instance or Attribute-like object')
}
if (Object.prototype.toString.call(attr) !== '[object LdapAttribute]') {
newAttrs.push(new Attribute(attr))
continue
}
newAttrs.push(attr)
}
this.#attributes = newAttrs
}
/**
* The path to the LDAP entry that matched the search.
*
* @returns {import('@ldapjs/dn').DN}
*/
get objectName () {
return this.#objectName
}
/**
* Set the path to the LDAP entry that matched the search.
*
* @param {string | import('@ldapjs/dn').DN} value
*
* @throws When the input is invalid.
*/
set objectName (value) {
if (typeof value === 'string') {
this.#objectName = DN.fromString(value)
} else if (Object.prototype.toString.call(value) === '[object LdapDn]') {
this.#objectName = value
} else {
throw Error('objectName must be a DN string or an instance of LdapDn')
}
}
/**
* Internal use only.
*
* @param {import('@ldapjs/asn1').BerWriter} ber
*
* @returns {import('@ldapjs/asn1').BerWriter}
*/
_toBer (ber) {
ber.startSequence(operations.LDAP_RES_SEARCH_ENTRY)
ber.writeString(this.#objectName.toString())
ber.startSequence()
for (const attr of this.#attributes) {
const attrBer = attr.toBer()
ber.appendBuffer(attrBer.buffer)
}
ber.endSequence()
ber.endSequence()
return ber
}
/**
* Internal use only.
*
* @param {object}
*
* @returns {object}
*/
_pojo (obj = {}) {
obj.objectName = this.#objectName.toString()
obj.attributes = []
for (const attr of this.#attributes) {
obj.attributes.push(attr.pojo)
}
return obj
}
/**
* Implements the standardized `parseToPojo` method.
*
* @see LdapMessage.parseToPojo
*
* @param {import('@ldapjs/asn1').BerReader} ber
*
* @returns {object}
*/
static parseToPojo (ber) {
const protocolOp = ber.readSequence()
if (protocolOp !== operations.LDAP_RES_SEARCH_ENTRY) {
const op = protocolOp.toString(16).padStart(2, '0')
throw Error(`found wrong protocol operation: 0x${op}`)
}
const objectName = ber.readString()
const attributes = []
// Advance to the first attribute sequence in the set
// of attribute sequences.
ber.readSequence()
const endOfAttributesPos = ber.offset + ber.length
while (ber.offset < endOfAttributesPos) {
const attribute = Attribute.fromBer(ber)
attributes.push(attribute)
}
return { protocolOp, objectName, attributes }
}
}
module.exports = SearchResultEntry

View File

@ -0,0 +1,195 @@
'use strict'
const tap = require('tap')
const { operations } = require('@ldapjs/protocol')
const Attribute = require('@ldapjs/attribute')
const { DN } = require('@ldapjs/dn')
const { BerWriter, BerReader } = require('@ldapjs/asn1')
const SearchResultEntry = require('./search-result-entry')
const {
searchResultEntryBytes,
searchResultEntryNoValuesBytes
} = require('./_fixtures/message-byte-arrays')
tap.test('basic', t => {
t.test('constructor no args', async t => {
const res = new SearchResultEntry()
t.strictSame(res.pojo, {
messageId: 1,
protocolOp: operations.LDAP_RES_SEARCH_ENTRY,
type: 'SearchResultEntry',
objectName: '',
attributes: [],
controls: []
})
t.equal(res.type, 'SearchResultEntry')
})
t.test('constructor with args', async t => {
const res = new SearchResultEntry({
objectName: 'dc=example,dc=com',
attributes: [{ type: 'cn', values: ['foo'] }]
})
t.strictSame(res.pojo, {
messageId: 1,
protocolOp: operations.LDAP_RES_SEARCH_ENTRY,
type: 'SearchResultEntry',
objectName: 'dc=example,dc=com',
attributes: [{ type: 'cn', values: ['foo'] }],
controls: []
})
})
t.end()
})
tap.test('.attributes', t => {
t.test('sets/gets', async t => {
const res = new SearchResultEntry()
t.strictSame(res.attributes, [])
res.attributes = [new Attribute({ type: 'cn', values: 'foo' })]
t.strictSame(res.attributes, [new Attribute({ type: 'cn', values: 'foo' })])
})
t.test('rejects non-array', async t => {
const res = new SearchResultEntry()
t.throws(
() => {
res.attributes = { type: 'cn', values: ['foo'] }
},
'attrs must be an array'
)
})
t.test('rejects non-attribute objects', async t => {
const res = new SearchResultEntry()
t.throws(
() => {
res.attributes = [{ foo: 'bar' }]
},
'attr must be an Attribute instance or Attribute-like object'
)
})
t.end()
})
tap.test('.objectName', t => {
t.test('sets/gets', async t => {
const res = new SearchResultEntry()
t.equal(res.objectName.toString(), '')
t.equal(res._dn.toString(), '')
res.objectName = 'cn=foo'
t.equal(res.objectName.toString(), 'cn=foo')
res.objectName = DN.fromString('sn=bar')
t.equal(res.objectName.toString(), 'sn=bar')
})
t.test('throws for invalid value', async t => {
const res = new SearchResultEntry()
t.throws(
() => {
res.objectName = ['invalid input']
},
'objectName must be a DN string or instance of LdapDn'
)
})
t.end()
})
tap.test('_toBer', t => {
t.test('converts instance to BER', async t => {
let res = new SearchResultEntry({
objectName: 'dc=example,dc=com',
attributes: [
{ type: 'objectClass', values: ['top', 'domain'] },
{ type: 'dc', values: ['example'] }
]
})
let writer = new BerWriter()
res._toBer(writer)
t.equal(
Buffer.from(searchResultEntryBytes.slice(5)).compare(writer.buffer),
0
)
res = new SearchResultEntry({
objectName: 'dc=example,dc=com',
attributes: [
{ type: 'objectClass', values: [] },
{ type: 'dc', values: [] }
]
})
writer = new BerWriter()
res._toBer(writer)
t.equal(
Buffer.from(searchResultEntryNoValuesBytes.slice(5)).compare(writer.buffer),
0
)
})
t.end()
})
tap.test('_pojo', t => {
t.test('returns a pojo representation', async t => {
const req = new SearchResultEntry({
objectName: 'cn=foo',
attributes: [{ type: 'bar', values: ['baz'] }]
})
t.strictSame(req._pojo(), {
objectName: 'cn=foo',
attributes: [{
type: 'bar',
values: ['baz']
}]
})
})
t.end()
})
tap.test('#parseToPojo', t => {
t.test('throws if operation incorrect', async t => {
const reqBuffer = Buffer.from(searchResultEntryBytes)
reqBuffer[5] = 0x61
const reader = new BerReader(reqBuffer)
reader.readSequence()
reader.readInt()
t.throws(
() => SearchResultEntry.parseToPojo(reader),
Error('found wrong protocol operation: 0x61')
)
})
t.test('returns a pojo representation', async t => {
const reqBuffer = Buffer.from(searchResultEntryBytes)
const reader = new BerReader(reqBuffer)
reader.readSequence()
reader.readInt()
const pojo = SearchResultEntry.parseToPojo(reader)
t.equal(pojo.protocolOp, operations.LDAP_RES_SEARCH_ENTRY)
t.equal(pojo.objectName, 'dc=example,dc=com')
t.strictSame(pojo.attributes[0].pojo, {
type: 'objectClass',
values: ['top', 'domain']
})
t.strictSame(pojo.attributes[1].pojo, {
type: 'dc',
values: ['example']
})
})
t.end()
})

View File

@ -0,0 +1,144 @@
'use strict'
const LdapMessage = require('../ldap-message')
const { operations } = require('@ldapjs/protocol')
/**
* Implements the search result reference response message as described in
* https://www.rfc-editor.org/rfc/rfc4511.html#section-4.5.2.
*/
class SearchResultReference extends LdapMessage {
#uri
/**
* @typedef {LdapMessageOptions} SearchResultReferenceOptions
* @property {string[]} [uri=[]] The set of reference URIs the message is
* providing.
* @property {string[]} [uris] An alias for uri.
*/
/**
* @param {SearchResultReferenceOptions} options
*/
constructor (options = {}) {
options.protocolOp = operations.LDAP_RES_SEARCH_REF
super(options)
this.uri = (options.uri || options.uris) ?? []
}
/**
* The name of the request type.
*
* @type {string}
*/
get type () {
return 'SearchResultReference'
}
/**
* The list of reference URIs associated with the message.
*
* @returns {string[]}
*/
get uri () {
return this.#uri.slice(0)
}
/**
* Define the list of reference URIs associated with the message.
*
* @param {string[]} value
*
* @throws When the value is not an array or contains a non-string element.
*/
set uri (value) {
if (
Array.isArray(value) === false ||
value.some(v => typeof v !== 'string')
) {
throw Error('uri must be an array of strings')
}
this.#uri = value.slice(0)
}
/**
* Alias of {@link uri}.
*
* @returns {string[]}
*/
get uris () {
return this.uri
}
/**
* Alias of {@link uri} setter.
*
* @param {string[]} value
*/
set uris (value) {
this.uri = value
}
/**
* Internal use only.
*
* @param {import('@ldapjs/asn1').BerWriter} ber
*
* @returns {import('@ldapjs/asn1').BerWriter}
*/
_toBer (ber) {
ber.startSequence(operations.LDAP_RES_SEARCH_REF)
for (const uri of this.#uri) {
ber.writeString(uri)
}
ber.endSequence()
return ber
}
/**
* Internal use only.
*
* @param {object}
*
* @returns {object}
*/
_pojo (obj = {}) {
obj.uri = []
for (const uri of this.#uri) {
obj.uri.push(uri)
}
return obj
}
/**
* Implements the standardized `parseToPojo` method.
*
* @see LdapMessage.parseToPojo
*
* @param {import('@ldapjs/asn1').BerReader} ber
*
* @returns {object}
*/
static parseToPojo (ber) {
const protocolOp = ber.readSequence()
if (protocolOp !== operations.LDAP_RES_SEARCH_REF) {
const op = protocolOp.toString(16).padStart(2, '0')
throw Error(`found wrong protocol operation: 0x${op}`)
}
const uri = []
const endOfMessagePos = ber.offset + ber.length
while (ber.offset < endOfMessagePos) {
const u = ber.readString()
uri.push(u)
}
return { protocolOp, uri }
}
}
module.exports = SearchResultReference

View File

@ -0,0 +1,161 @@
'use strict'
const tap = require('tap')
const { operations } = require('@ldapjs/protocol')
const { BerReader, BerWriter } = require('@ldapjs/asn1')
const SearchResultReference = require('./search-result-reference')
const {
searchResultReferenceBytes
} = require('./_fixtures/message-byte-arrays')
tap.test('basic', t => {
t.test('constructor no args', async t => {
const expected = {
messageId: 1,
protocolOp: operations.LDAP_RES_SEARCH_REF,
type: 'SearchResultReference',
uri: [],
controls: []
}
const res = new SearchResultReference()
t.strictSame(res.pojo, expected)
t.equal(res.type, 'SearchResultReference')
})
t.test('constructor with args', async t => {
const expected = {
messageId: 1,
protocolOp: operations.LDAP_RES_SEARCH_REF,
type: 'SearchResultReference',
uri: ['ldap://foo', 'ldap://bar'],
controls: []
}
let res = new SearchResultReference({
uri: ['ldap://foo', 'ldap://bar']
})
t.strictSame(res.pojo, expected)
res = new SearchResultReference({
uris: ['ldap://foo', 'ldap://bar']
})
t.strictSame(res.pojo, expected)
})
t.end()
})
tap.test('.uri/.uris', t => {
t.test('sets/gets', async t => {
const res = new SearchResultReference()
t.strictSame(res.uri, [])
t.strictSame(res.uris, [])
res.uri = ['ldap://foo']
t.strictSame(res.uri, ['ldap://foo'])
res.uris = ['ldap://bar']
t.strictSame(res.uris, ['ldap://bar'])
t.strictSame(res.uri, ['ldap://bar'])
})
t.test('throws for bad input', async t => {
const res = new SearchResultReference()
const expected = Error('uri must be an array of strings')
t.throws(
() => {
res.uri = 'ldap://foo'
},
expected
)
t.throws(
() => {
res.uris = 'ldap://foo'
},
expected
)
t.throws(
() => {
res.uri = ['ldap://foo', { foo: 'foo' }, 'ldap://bar']
},
expected
)
})
t.end()
})
tap.test('_toBer', t => {
tap.test('converts instance to BER', async t => {
const res = new SearchResultReference({
uri: [
'ldap://ds1.example.com:389/dc=example,dc=com??sub?',
'ldap://ds2.example.com:389/dc=example,dc=com??sub?'
]
})
const writer = new BerWriter()
res._toBer(writer)
t.equal(
Buffer.from(searchResultReferenceBytes.slice(5)).compare(writer.buffer),
0
)
})
t.end()
})
tap.test('_pojo', t => {
t.test('returns a pojo representation', async t => {
const res = new SearchResultReference({
uri: [
'ldap://ds1.example.com:389/dc=example,dc=com??sub?',
'ldap://ds2.example.com:389/dc=example,dc=com??sub?'
]
})
t.strictSame(res._pojo(), {
uri: [
'ldap://ds1.example.com:389/dc=example,dc=com??sub?',
'ldap://ds2.example.com:389/dc=example,dc=com??sub?'
]
})
})
t.end()
})
tap.test('#parseToPojo', t => {
t.test('throws if operation incorrect', async t => {
const reqBuffer = Buffer.from(searchResultReferenceBytes)
reqBuffer[5] = 0x61
const reader = new BerReader(reqBuffer)
reader.readSequence()
reader.readInt()
t.throws(
() => SearchResultReference.parseToPojo(reader),
Error('found wrong protocol operation: 0x61')
)
})
t.test('returns a pojo representation', async t => {
const reqBuffer = Buffer.from(searchResultReferenceBytes)
const reader = new BerReader(reqBuffer)
reader.readSequence()
reader.readInt()
const pojo = SearchResultReference.parseToPojo(reader)
t.equal(pojo.protocolOp, operations.LDAP_RES_SEARCH_REF)
t.equal(pojo.uri[0], 'ldap://ds1.example.com:389/dc=example,dc=com??sub?')
t.equal(pojo.uri[1], 'ldap://ds2.example.com:389/dc=example,dc=com??sub?')
})
t.end()
})

View File

@ -0,0 +1,69 @@
'use strict'
const LdapMessage = require('../ldap-message')
const { operations } = require('@ldapjs/protocol')
/**
* Implements the unbind request message as described in
* https://www.rfc-editor.org/rfc/rfc4511.html#section-4.3.
*/
class UnbindRequest extends LdapMessage {
/**
* @param {LdapMessageOptions} options
*/
constructor (options = {}) {
options.protocolOp = operations.LDAP_REQ_UNBIND
super(options)
}
/**
* The name of the request type.
*
* @type {string}
*/
get type () {
return 'UnbindRequest'
}
/**
* Internal use only.
*
* @param {import('@ldapjs/asn1').BerWriter} ber
*
* @returns {import('@ldapjs/asn1').BerWriter}
*/
_toBer (ber) {
ber.writeString('', operations.LDAP_REQ_UNBIND)
return ber
}
/**
* Internal use only.
*
* @param {object}
*
* @returns {object}
*/
_pojo (obj = {}) {
return obj
}
/**
* Implements the standardized `parseToPojo` method.
*
* @see LdapMessage.parseToPojo
*
* @param {import('@ldapjs/asn1').BerReader} ber
*/
static parseToPojo (ber) {
const protocolOp = ber.readSequence()
if (protocolOp !== operations.LDAP_REQ_UNBIND) {
const op = protocolOp.toString(16).padStart(2, '0')
throw Error(`found wrong protocol operation: 0x${op}`)
}
return { protocolOp }
}
}
module.exports = UnbindRequest

View File

@ -0,0 +1,75 @@
'use strict'
const tap = require('tap')
const { operations } = require('@ldapjs/protocol')
const UnbindRequest = require('./unbind-request')
const { unbindRequestBytes } = require('./_fixtures/message-byte-arrays')
const { BerReader, BerWriter } = require('@ldapjs/asn1')
tap.test('basic', t => {
t.test('constructor no args', async t => {
const req = new UnbindRequest()
t.strictSame(req.pojo, {
messageId: 1,
protocolOp: operations.LDAP_REQ_UNBIND,
type: 'UnbindRequest',
controls: []
})
t.equal(req.type, 'UnbindRequest')
})
t.end()
})
tap.test('_toBer', t => {
tap.test('converts instance to BER', async t => {
const req = new UnbindRequest()
const writer = new BerWriter()
req._toBer(writer)
t.equal(
Buffer.from(unbindRequestBytes.slice(5)).compare(writer.buffer),
0
)
})
t.end()
})
tap.test('_pojo', t => {
t.test('returns a pojo representation', async t => {
const req = new UnbindRequest()
t.strictSame(req._pojo(), {})
})
t.end()
})
tap.test('#parseToPojo', t => {
t.test('throws if operation incorrect', async t => {
const reqBuffer = Buffer.from(unbindRequestBytes)
reqBuffer[5] = 0x61
const reader = new BerReader(reqBuffer)
reader.readSequence()
reader.readInt()
t.throws(
() => UnbindRequest.parseToPojo(reader),
Error('found wrong protocol operation: 0x61')
)
})
t.test('returns a pojo representation', async t => {
const reqBuffer = Buffer.from(unbindRequestBytes)
const reader = new BerReader(reqBuffer)
reader.readSequence()
reader.readInt()
const pojo = UnbindRequest.parseToPojo(reader)
t.equal(pojo.protocolOp, operations.LDAP_REQ_UNBIND)
})
t.end()
})

205
node_modules/@ldapjs/messages/lib/parse-to-message.js generated vendored Normal file
View File

@ -0,0 +1,205 @@
'use strict'
const { operations } = require('@ldapjs/protocol')
const { getControl } = require('@ldapjs/controls')
const messageClasses = {
AbandonRequest: require('./messages/abandon-request'),
AddRequest: require('./messages/add-request'),
BindRequest: require('./messages/bind-request'),
CompareRequest: require('./messages/compare-request'),
DeleteRequest: require('./messages/delete-request'),
ExtensionRequest: require('./messages/extension-request'),
ModifyRequest: require('./messages/modify-request'),
ModifyDnRequest: require('./messages/modifydn-request'),
SearchRequest: require('./messages/search-request'),
UnbindRequest: require('./messages/unbind-request'),
AbandonResponse: require('./messages/abandon-response'),
AddResponse: require('./messages/add-response'),
BindResponse: require('./messages/bind-response'),
CompareResponse: require('./messages/compare-response'),
DeleteResponse: require('./messages/delete-response'),
ExtensionResponse: require('./messages/extension-response'),
ModifyResponse: require('./messages/modify-response'),
ModifyDnResponse: require('./messages/modifydn-response'),
// Search result messages.
SearchResultEntry: require('./messages/search-result-entry'),
SearchResultReference: require('./messages/search-result-reference'),
SearchResultDone: require('./messages/search-result-done'),
// Miscellaneous messages.
IntermediateResponse: require('./messages/intermediate-response')
}
/**
* Utility function that inspects a BER object and parses it into an instance
* of a specific LDAP message.
*
* @param {import('@ldapjs/asn1').BerReader} ber An object that represents a
* full LDAP Message sequence as described in
* https://www.rfc-editor.org/rfc/rfc4511.html#section-4.1.1.
*
* @returns {LdapMessage} Some specific instance of the base LDAP Message
* type.
*
* @throws When the input data is malformed.
*/
module.exports = function parseToMessage (ber) {
const inputType = Object.prototype.toString.apply(ber)
if (inputType !== '[object BerReader]') {
throw TypeError(`Expected BerReader but got ${inputType}.`)
}
ber.readSequence()
const messageId = ber.readInt()
const messageType = identifyType(ber)
const MessageClass = messageClasses[messageType]
const pojoMessage = MessageClass.parseToPojo(ber)
const message = new MessageClass({
messageId,
...pojoMessage
})
// Look for controls
if (ber.peek() === 0xa0) {
ber.readSequence()
const end = ber.offset + ber.length
while (ber.offset < end) {
const c = getControl(ber)
/* istanbul ignore else */
if (c) {
message.addControl(c)
}
}
}
return message
}
/**
* Determines the type of LDAP message the BER represents, e.g. a "Bind Request"
* message.
*
* @param {BerReader} ber
*
* @returns {string}
*/
function identifyType (ber) {
let result
switch (ber.peek()) {
case operations.LDAP_REQ_ABANDON: {
result = 'AbandonRequest'
break
}
case 0x00: {
result = 'AbandonResponse'
break
}
case operations.LDAP_REQ_ADD: {
result = 'AddRequest'
break
}
case operations.LDAP_RES_ADD: {
result = 'AddResponse'
break
}
case operations.LDAP_REQ_BIND: {
result = 'BindRequest'
break
}
case operations.LDAP_RES_BIND: {
result = 'BindResponse'
break
}
case operations.LDAP_REQ_COMPARE: {
result = 'CompareRequest'
break
}
case operations.LDAP_RES_COMPARE: {
result = 'CompareResponse'
break
}
case operations.LDAP_REQ_DELETE: {
result = 'DeleteRequest'
break
}
case operations.LDAP_RES_DELETE: {
result = 'DeleteResponse'
break
}
case operations.LDAP_REQ_EXTENSION: {
result = 'ExtensionRequest'
break
}
case operations.LDAP_RES_EXTENSION: {
result = 'ExtensionResponse'
break
}
case operations.LDAP_REQ_MODIFY: {
result = 'ModifyRequest'
break
}
case operations.LDAP_RES_MODIFY: {
result = 'ModifyResponse'
break
}
case operations.LDAP_REQ_MODRDN: {
result = 'ModifyDnRequest'
break
}
case operations.LDAP_RES_MODRDN: {
result = 'ModifyDnResponse'
break
}
case operations.LDAP_REQ_SEARCH: {
result = 'SearchRequest'
break
}
case operations.LDAP_RES_SEARCH_ENTRY: {
result = 'SearchResultEntry'
break
}
case operations.LDAP_RES_SEARCH_REF: {
result = 'SearchResultReference'
break
}
case operations.LDAP_RES_SEARCH_DONE: {
result = 'SearchResultDone'
break
}
case operations.LDAP_REQ_UNBIND: {
result = 'UnbindRequest'
break
}
case operations.LDAP_RES_INTERMEDIATE: {
result = 'IntermediateResponse'
break
}
}
return result
}

View File

@ -0,0 +1,54 @@
'use strict'
const tap = require('tap')
const { BerReader } = require('@ldapjs/asn1')
const parseToMessage = require('./parse-to-message')
const messageBytesArrays = require('./messages/_fixtures/message-byte-arrays')
tap.test('throws if input not a BerReader', async t => {
const input = Buffer.from([0x30, 0x01, 0x64])
t.throws(
() => parseToMessage(input),
Error('Expected BerReader but got [object Uint8Array]')
)
})
tap.test('throws if sequence is invalid', async t => {
const input = new BerReader(Buffer.from([0x0a, 0x01, 0x64]))
t.throws(
() => parseToMessage(input),
Error('Expected 0x02: got 0x64')
)
})
tap.test('parses messages correctly', async t => {
for (const [name, messageBytes] of Object.entries(messageBytesArrays)) {
t.comment(`verifying message bytes: ${name}`)
const expected = Buffer.from(messageBytes)
const reader = new BerReader(expected)
const message = parseToMessage(reader)
const found = message.toBer().buffer
const isEqual = t.equal(expected.compare(found), 0, `${name} comparison`)
if (isEqual === false) {
const diff = {}
for (let i = 0; i < expected.length; i += 1) {
if (expected[i] !== found[i]) {
diff[i] = {
expected: Number(expected[i]).toString(16),
found: Number(found[i]).toString(16)
}
}
}
t.fail(`${name} differs`, diff)
}
}
})
tap.test('parses search req with evolution filter', async t => {
const messageBytes = require('./_fixtures/evolution-filter-req')
const messageBuffer = Buffer.from(messageBytes)
const message = parseToMessage(new BerReader(messageBuffer))
t.equal(messageBuffer.compare(message.toBer().buffer), 0)
})

44
node_modules/@ldapjs/messages/package.json generated vendored Normal file
View File

@ -0,0 +1,44 @@
{
"name": "@ldapjs/messages",
"homepage": "https://github.com/ldapjs/messages",
"description": "API for creating and parsing LDAP messages",
"version": "1.3.0",
"license": "MIT",
"repository": {
"type": "git",
"url": "git@github.com:ldapjs/messages.git"
},
"main": "index.js",
"dependencies": {
"@ldapjs/asn1": "^2.0.0",
"@ldapjs/attribute": "^1.0.0",
"@ldapjs/change": "^1.0.0",
"@ldapjs/controls": "^2.1.0",
"@ldapjs/dn": "^1.1.0",
"@ldapjs/filter": "^2.1.1",
"@ldapjs/protocol": "^1.2.1",
"process-warning": "^2.2.0"
},
"devDependencies": {
"@fastify/pre-commit": "^2.0.2",
"eslint": "^8.47.0",
"eslint-config-standard": "^17.0.0",
"eslint-plugin-import": "^2.28.0",
"eslint-plugin-n": "^16.0.1",
"eslint-plugin-node": "^11.1.0",
"eslint-plugin-promise": "^6.1.1",
"tap": "^16.3.8"
},
"scripts": {
"lint": "eslint .",
"lint:ci": "eslint .",
"test": "tap --no-coverage-report",
"test:cov": "tap",
"test:cov:html": "tap --coverage-report=html",
"test:watch": "tap -w --no-coverage-report"
},
"precommit": [
"lint",
"test"
]
}