Compare commits

..

No commits in common. "master" and "v2.3.2" have entirely different histories.

135 changed files with 8830 additions and 1581 deletions

View File

@ -4,9 +4,6 @@ module.exports = {
es2021: true, es2021: true,
node: true node: true
}, },
parserOptions: {
ecmaVersion: 'latest'
},
extends: [ extends: [
'standard' 'standard'
], ],

View File

@ -10,10 +10,10 @@ jobs:
name: Update Docs name: Update Docs
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v2
- uses: actions/setup-node@v3 - uses: actions/setup-node@v2.5.1
with: with:
node-version: '18' node-version: '14'
- name: Install Packages - name: Install Packages
run: npm install run: npm install
- name: Build Docs - name: Build Docs

View File

@ -4,34 +4,35 @@ name: 'Integration Tests'
# https://github.community/t5/GitHub-Actions/Github-Actions-services-not-reachable/m-p/30739/highlight/true#M538 # https://github.community/t5/GitHub-Actions/Github-Actions-services-not-reachable/m-p/30739/highlight/true#M538
on: on:
push:
branches:
- master
- next
pull_request: pull_request:
branches: branches:
- master - master
- next
jobs: jobs:
baseline: baseline:
name: Baseline Tests name: Baseline Tests
runs-on: ubuntu-latest runs-on: ubuntu-latest
services: # services:
openldap: # openldap:
image: ghcr.io/ldapjs/docker-test-openldap/openldap:2023-10-30 # image: docker.pkg.github.com/ldapjs/docker-test-openldap/openldap:1.0
ports: # ports:
- 389:389 # - 389:389
- 636:636 # - 636:636
options: >
--health-cmd "ldapsearch -Y EXTERNAL -Q -H ldapi:// -b ou=people,dc=planetexpress,dc=com -LLL '(cn=Turanga Leela)' cn"
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v2
- uses: actions/setup-node@v3 - uses: actions/setup-node@v2.5.1
with:
node-version: 'lts/*' # Hack way to start service since GitHub doesn't integrate with its own services
- name: Docker login
run: docker login docker.pkg.github.com -u ${GITHUB_ACTOR} -p ${GITHUB_TOKEN}
env:
GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}}
- name: Pull Docker image
run: docker pull "docker.pkg.github.com/ldapjs/docker-test-openldap/openldap:latest"
- name: Start OpenLDAP service
run: docker run -it -d --name openldap -p 389:389 -p 636:636 docker.pkg.github.com/ldapjs/docker-test-openldap/openldap:latest
- name: Install Packages - name: Install Packages
run: npm install run: npm install

View File

@ -4,21 +4,17 @@ on:
push: push:
branches: branches:
- master - master
- next
pull_request: pull_request:
branches: branches:
- master - master
- next
jobs: jobs:
lint: lint:
name: Lint Check name: Lint Check
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v2
- uses: actions/setup-node@v3 - uses: actions/setup-node@v2.5.1
with:
node-version: 'lts/*'
- name: Install Packages - name: Install Packages
run: npm install run: npm install
- name: Lint Code - name: Lint Code
@ -32,16 +28,27 @@ jobs:
- ubuntu-latest - ubuntu-latest
- windows-latest - windows-latest
node: node:
- 16 - 10.13.0
- 18 - 10.x
- 20 - 12.x
- 14.x
runs-on: ${{ matrix.os }} runs-on: ${{ matrix.os }}
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v2
- uses: actions/setup-node@v3 - uses: actions/setup-node@v2.5.1
with: with:
node-version: ${{ matrix.node }} node-version: ${{ matrix.node }}
- name: Install Packages - name: Install Packages
run: npm install run: npm install
- name: Run Tests - name: Run Tests
run: npm run test:ci run: npm run test:ci
- name: Coveralls Parallel
uses: coverallsapp/github-action@1.1.3
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
parallel: true
- name: Coveralls Finished
uses: coverallsapp/github-action@1.1.3
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
parallel-finished: true

3
.npmrc
View File

@ -1,3 +0,0 @@
# npm general settings
package-lock=false
legacy-peer-deps=true

4
.taprc Normal file
View File

@ -0,0 +1,4 @@
check-coverage: false
files:
- 'test/**/*.test.js'

View File

@ -1,10 +0,0 @@
# With PR #834 the code in this code base has been reduced significantly.
# As a result, the coverage percentages changed, and are much lower than
# previously. So we are reducing the requirements accordingly
branches: 50
functions: 50
lines: 50
statements: 50
files:
- 'test/**/*.test.js'

View File

@ -1,34 +1,54 @@
# Project Decomissioned # LDAPjs
This project has been decomissioned. I, James Sumners, took it on when it was [![Build Status](https://github.com/ldapjs/node-ldapjs/workflows/Lint%20And%20Test/badge.svg)](https://github.com/ldapjs/node-ldapjs/actions)
languishing without any maintenance as it filled a need in the ecosystem and [![Coverage Status](https://coveralls.io/repos/github/ldapjs/node-ldapjs/badge.svg)](https://coveralls.io/github/ldapjs/node-ldapjs/)
I had built things at a prior organization that depended upon this project.
I spent a lot of time triaging issues and reworking things toward a path
that could be more easily maintained by a community of volunteers. But I have
not had the time to dedicate to this project in quite a while. There are
outstanding issues that would take me at least a week of dedicated development
time to solve, and I cannot afford to take time off of work to do that.
Particularly considering that the aforementioned organization was two
jobs ago, and it is extremely unlikely that I will transition to a role again
that will need this project.
So, why am I just now deciding to decomission this project? Because today, LDAPjs makes the LDAP protocol a first class citizen in Node.js.
2024-05-14, I received the following email:
![Abusive email](dt.png) ## Usage
I will not tolerate abuse, and I especially will not tolerate tacit death For full docs, head on over to <http://ldapjs.org>.
threats, over a hobby. You can thank the author of that email for the
decomissioning on this project.
My recommendation to you in regard to LDAP operations: write a gateway in a ```javascript
language that is more suited to these types of operations. I'd suggest var ldap = require('ldapjs');
[Go](https://go.dev).
👋 var server = ldap.createServer();
P.S.: if I ever do need this project again, I might revive it. But I'd fight server.search('dc=example', function(req, res, next) {
hard for my suggestion above. Also, I will consider turning it over to an var obj = {
interested party, but I will require at least one recommendation from a dn: req.dn.toString(),
Node.js core contributor that I can vet with the people that I know on that attributes: {
team. objectclass: ['organization', 'top'],
o: 'example'
}
};
if (req.filter.matches(obj.attributes))
res.send(obj);
res.end();
});
server.listen(1389, function() {
console.log('ldapjs listening at ' + server.url);
});
```
To run that, assuming you've got the [OpenLDAP](http://www.openldap.org/)
client on your system:
ldapsearch -H ldap://localhost:1389 -x -b dc=example objectclass=*
## Installation
npm install ldapjs
DTrace support is included in ldapjs. To enable it, `npm install dtrace-provider`.
## License
MIT.
## Bugs
See <https://github.com/ldapjs/node-ldapjs/issues>.

View File

@ -1,10 +1,8 @@
version: '3'
services: services:
openldap: openldap:
image: ghcr.io/ldapjs/docker-test-openldap/openldap:2023-10-30 image: docker.pkg.github.com/ldapjs/docker-test-openldap/openldap:latest
ports: ports:
- 389:389 - 389:389
- 636:636 - 636:636
healthcheck:
start_period: 3s
test: >
/usr/bin/ldapsearch -Y EXTERNAL -Q -H ldapi:// -b ou=people,dc=planetexpress,dc=com -LLL '(cn=Turanga Leela)' cn 1>/dev/null

View File

@ -22,7 +22,7 @@ const client = ldap.createClient({
url: ['ldap://127.0.0.1:1389', 'ldap://127.0.0.2:1389'] url: ['ldap://127.0.0.1:1389', 'ldap://127.0.0.2:1389']
}); });
client.on('connectError', (err) => { client.on('error', (err) => {
// handle connection error // handle connection error
}) })
``` ```
@ -41,6 +41,7 @@ client is:
|connectTimeout |Milliseconds client should wait before timing out on TCP connections (Default: OS default)| |connectTimeout |Milliseconds client should wait before timing out on TCP connections (Default: OS default)|
|tlsOptions |Additional options passed to TLS connection layer when connecting via `ldaps://` (See: The TLS docs for node.js)| |tlsOptions |Additional options passed to TLS connection layer when connecting via `ldaps://` (See: The TLS docs for node.js)|
|idleTimeout |Milliseconds after last activity before client emits idle event| |idleTimeout |Milliseconds after last activity before client emits idle event|
|strictDN |Force strict DN parsing for client methods (Default is true)|
|reconnect |Try to reconnect when the connection gets lost (Default is false)| |reconnect |Try to reconnect when the connection gets lost (Default is false)|
### url ### url
@ -213,8 +214,7 @@ Example:
const change = new ldap.Change({ const change = new ldap.Change({
operation: 'add', operation: 'add',
modification: { modification: {
type: 'pets', pets: ['cat', 'dog']
values: ['cat', 'dog']
} }
}); });
@ -235,13 +235,7 @@ must be one of:
| add | Adds the attribute value(s) referenced in `modification`. The attribute may or may not already exist. | | add | Adds the attribute value(s) referenced in `modification`. The attribute may or may not already exist. |
| delete | Deletes the attribute (and all values) referenced in `modification`. | | delete | Deletes the attribute (and all values) referenced in `modification`. |
`modification` is just a plain old JS object with the required type and values you want. `modification` is just a plain old JS object with the values you want.
| Operation | Description |
|-----------|-------------|
| type | String that defines the attribute type for the modification. |
| values | Defines the values for modification. |
# modifyDN # modifyDN
`modifyDN(dn, newDN, controls, callback)` `modifyDN(dn, newDN, controls, callback)`
@ -293,7 +287,7 @@ Responses inside callback of the `search` method are an `EventEmitter` where you
each `searchEntry` that comes back from the server. You will additionally be able to listen for a `searchRequest` each `searchEntry` that comes back from the server. You will additionally be able to listen for a `searchRequest`
, `searchReference`, `error` and `end` event. , `searchReference`, `error` and `end` event.
`searchRequest` is emitted immediately after every `SearchRequest` is sent with a `SearchRequest` parameter. You can do operations `searchRequest` is emitted immediately after every `SearchRequest` is sent with a `SearchRequest` parameter. You can do operations
like `client.abandon` with `searchRequest.messageId` to abandon this search request. Note that the `error` event will like `client.abandon` with `searchRequest.messageID` to abandon this search request. Note that the `error` event will
only be for client/TCP errors, not LDAP error codes like the other APIs. You'll want to check the LDAP status code only be for client/TCP errors, not LDAP error codes like the other APIs. You'll want to check the LDAP status code
(likely for `0`) on the `end` event to assert success. LDAP search results can give you a lot of status codes, such as (likely for `0`) on the `end` event to assert success. LDAP search results can give you a lot of status codes, such as
time or size exceeded, busy, inappropriate matching, etc., which is why this method doesn't try to wrap up the code time or size exceeded, busy, inappropriate matching, etc., which is why this method doesn't try to wrap up the code
@ -312,10 +306,10 @@ client.search('o=example', opts, (err, res) => {
assert.ifError(err); assert.ifError(err);
res.on('searchRequest', (searchRequest) => { res.on('searchRequest', (searchRequest) => {
console.log('searchRequest: ', searchRequest.messageId); console.log('searchRequest: ', searchRequest.messageID);
}); });
res.on('searchEntry', (entry) => { res.on('searchEntry', (entry) => {
console.log('entry: ' + JSON.stringify(entry.pojo)); console.log('entry: ' + JSON.stringify(entry.object));
}); });
res.on('searchReference', (referral) => { res.on('searchReference', (referral) => {
console.log('referral: ' + referral.uris.join()); console.log('referral: ' + referral.uris.join());

View File

@ -80,15 +80,8 @@ Example:
`listen(port, [host], [callback])` `listen(port, [host], [callback])`
Begin accepting connections on the specified port and host. If the host is Begin accepting connections on the specified port and host. If the host is
omitted, the server will accept connections directed to the IPv4 address omitted, the server will accept connections directed to any IPv4 address
`127.0.0.1`. To listen on any other address, supply said address as the `host` (INADDR\_ANY).
parameter. For example, to listen on all available IPv6 addresses supply
`::` as the `host` (note, this _may_ also result in listening on all
available IPv4 addresses, depending on operating system behavior).
We highly recommend being as explicit as possible with the `host` parameter.
Listening on all available addresses (through `::` or `0.0.0.0`) can lead
to potential security issues.
This function is asynchronous. The last parameter callback will be called when This function is asynchronous. The last parameter callback will be called when
the server has been bound. the server has been bound.
@ -204,7 +197,7 @@ authenticated as on this connection. If the client didn't bind, then a DN object
will be there defaulted to `cn=anonymous`. will be there defaulted to `cn=anonymous`.
Additionally, request will have a `logId` parameter you can use to uniquely Additionally, request will have a `logId` parameter you can use to uniquely
identify the request/connection pair in logs (includes the LDAP messageId). identify the request/connection pair in logs (includes the LDAP messageID).
## Common Response Elements ## Common Response Elements

BIN
dt.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 155 KiB

View File

@ -130,7 +130,7 @@ server.search(SUFFIX, authorize, function (req, res, next) {
case 'base': case 'base':
if (req.filter.matches(db[dn])) { if (req.filter.matches(db[dn])) {
res.send({ res.send({
dn, dn: dn,
attributes: db[dn] attributes: db[dn]
}) })
} }

24
examples/snoopldap.d Executable file
View File

@ -0,0 +1,24 @@
#!/usr/sbin/dtrace -s
#pragma D option quiet
BEGIN
{
printf("%-8s %-8s %-16s %-15s %-15s %s\n",
"LATENCY", "OPTYPE", "REMOTE IP", "BIND DN", "REQ DN",
"STATUS");
}
ldapjs*:::server-*-start
{
starts[arg0] = timestamp;
}
ldapjs*:::server-*-done
/starts[arg0]/
{
printf("%6dms %-8s %-16s %-15s %-15s %d\n",
(timestamp - starts[arg0]) / 1000000, strtok(probename + 7, "-"),
copyinstr(arg1), copyinstr(arg2), copyinstr(arg3), arg4);
starts[arg0] = 0;
}

54
lib/assert.js Normal file
View File

@ -0,0 +1,54 @@
// Copyright 2015 Joyent, Inc.
const assert = require('assert')
const util = require('util')
const isDN = require('./dn').DN.isDN
const isAttribute = require('./attribute').isAttribute
/// --- Helpers
// Copied from mcavage/node-assert-plus
function _assert (arg, type, name) {
name = name || type
throw new assert.AssertionError({
message: util.format('%s (%s) required', name, type),
actual: typeof (arg),
expected: type,
operator: '===',
stackStartFunction: _assert.caller
})
}
/// --- API
function stringDN (input, name) {
if (isDN(input) || typeof (input) === 'string') { return }
_assert(input, 'DN or string', name)
}
function optionalStringDN (input, name) {
if (input === undefined || isDN(input) || typeof (input) === 'string') { return }
_assert(input, 'DN or string', name)
}
function optionalDN (input, name) {
if (input !== undefined && !isDN(input)) { _assert(input, 'DN', name) }
}
function optionalArrayOfAttribute (input, name) {
if (input === undefined) { return }
if (!Array.isArray(input) ||
input.some(function (v) { return !isAttribute(v) })) {
_assert(input, 'array of Attribute', name)
}
}
/// --- Exports
module.exports = {
stringDN: stringDN,
optionalStringDN: optionalStringDN,
optionalDN: optionalDN,
optionalArrayOfAttribute: optionalArrayOfAttribute
}

160
lib/attribute.js Normal file
View File

@ -0,0 +1,160 @@
// Copyright 2011 Mark Cavage, Inc. All rights reserved.
const assert = require('assert')
const asn1 = require('asn1')
const Protocol = require('./protocol')
/// --- API
function Attribute (options) {
if (options) {
if (typeof (options) !== 'object') { throw new TypeError('options must be an object') }
if (options.type && typeof (options.type) !== 'string') { throw new TypeError('options.type must be a string') }
} else {
options = {}
}
this.type = options.type || ''
this._vals = []
if (options.vals !== undefined && options.vals !== null) { this.vals = options.vals }
}
module.exports = Attribute
Object.defineProperties(Attribute.prototype, {
buffers: {
get: function getBuffers () {
return this._vals
},
configurable: false
},
json: {
get: function getJson () {
return {
type: this.type,
vals: this.vals
}
},
configurable: false
},
vals: {
get: function getVals () {
const eType = _bufferEncoding(this.type)
return this._vals.map(function (v) {
return v.toString(eType)
})
},
set: function setVals (vals) {
const self = this
this._vals = []
if (Array.isArray(vals)) {
vals.forEach(function (v) {
self.addValue(v)
})
} else {
self.addValue(vals)
}
},
configurable: false
}
})
Attribute.prototype.addValue = function addValue (val) {
if (Buffer.isBuffer(val)) {
this._vals.push(val)
} else {
this._vals.push(Buffer.from(val + '', _bufferEncoding(this.type)))
}
}
/* BEGIN JSSTYLED */
Attribute.compare = function compare (a, b) {
if (!(Attribute.isAttribute(a)) || !(Attribute.isAttribute(b))) {
throw new TypeError('can only compare Attributes')
}
if (a.type < b.type) return -1
if (a.type > b.type) return 1
if (a.vals.length < b.vals.length) return -1
if (a.vals.length > b.vals.length) return 1
for (let i = 0; i < a.vals.length; i++) {
if (a.vals[i] < b.vals[i]) return -1
if (a.vals[i] > b.vals[i]) return 1
}
return 0
}
/* END JSSTYLED */
Attribute.prototype.parse = function parse (ber) {
assert.ok(ber)
ber.readSequence()
this.type = ber.readString()
if (ber.peek() === Protocol.LBER_SET) {
if (ber.readSequence(Protocol.LBER_SET)) {
const end = ber.offset + ber.length
while (ber.offset < end) { this._vals.push(ber.readString(asn1.Ber.OctetString, true)) }
}
}
return true
}
Attribute.prototype.toBer = function toBer (ber) {
assert.ok(ber)
ber.startSequence()
ber.writeString(this.type)
ber.startSequence(Protocol.LBER_SET)
if (this._vals.length) {
this._vals.forEach(function (b) {
ber.writeByte(asn1.Ber.OctetString)
ber.writeLength(b.length)
for (let i = 0; i < b.length; i++) { ber.writeByte(b[i]) }
})
} else {
ber.writeStringArray([])
}
ber.endSequence()
ber.endSequence()
return ber
}
Attribute.prototype.toString = function () {
return JSON.stringify(this.json)
}
Attribute.toBer = function (attr, ber) {
return Attribute.prototype.toBer.call(attr, ber)
}
Attribute.isAttribute = function isAttribute (attr) {
if (!attr || typeof (attr) !== 'object') {
return false
}
if (attr instanceof Attribute) {
return true
}
if ((typeof (attr.toBer) === 'function') &&
(typeof (attr.type) === 'string') &&
(Array.isArray(attr.vals)) &&
(attr.vals.filter(function (item) {
return (typeof (item) === 'string' ||
Buffer.isBuffer(item))
}).length === attr.vals.length)) {
return true
}
return false
}
function _bufferEncoding (type) {
/* JSSTYLED */
return /;binary$/.test(type) ? 'base64' : 'utf8'
}

213
lib/change.js Normal file
View File

@ -0,0 +1,213 @@
// Copyright 2011 Mark Cavage, Inc. All rights reserved.
const assert = require('assert-plus')
const Attribute = require('./attribute')
// var Protocol = require('./protocol')
/// --- API
function Change (options) {
if (options) {
assert.object(options)
assert.optionalString(options.operation)
} else {
options = {}
}
this._modification = false
this.operation = options.operation || options.type || 'add'
this.modification = options.modification || {}
}
Object.defineProperties(Change.prototype, {
operation: {
get: function getOperation () {
switch (this._operation) {
case 0x00: return 'add'
case 0x01: return 'delete'
case 0x02: return 'replace'
default:
throw new Error('0x' + this._operation.toString(16) + ' is invalid')
}
},
set: function setOperation (val) {
assert.string(val)
switch (val.toLowerCase()) {
case 'add':
this._operation = 0x00
break
case 'delete':
this._operation = 0x01
break
case 'replace':
this._operation = 0x02
break
default:
throw new Error('Invalid operation type: 0x' + val.toString(16))
}
},
configurable: false
},
modification: {
get: function getModification () {
return this._modification
},
set: function setModification (val) {
if (Attribute.isAttribute(val)) {
this._modification = val
return
}
// Does it have an attribute-like structure
if (Object.keys(val).length === 2 &&
typeof (val.type) === 'string' &&
Array.isArray(val.vals)) {
this._modification = new Attribute({
type: val.type,
vals: val.vals
})
return
}
const keys = Object.keys(val)
if (keys.length > 1) {
throw new Error('Only one attribute per Change allowed')
} else if (keys.length === 0) {
return
}
const k = keys[0]
const _attr = new Attribute({ type: k })
if (Array.isArray(val[k])) {
val[k].forEach(function (v) {
_attr.addValue(v.toString())
})
} else if (Buffer.isBuffer(val[k])) {
_attr.addValue(val[k])
} else if (val[k] !== undefined && val[k] !== null) {
_attr.addValue(val[k].toString())
}
this._modification = _attr
},
configurable: false
},
json: {
get: function getJSON () {
return {
operation: this.operation,
modification: this._modification ? this._modification.json : {}
}
},
configurable: false
}
})
Change.isChange = function isChange (change) {
if (!change || typeof (change) !== 'object') {
return false
}
if ((change instanceof Change) ||
((typeof (change.toBer) === 'function') &&
(change.modification !== undefined) &&
(change.operation !== undefined))) {
return true
}
return false
}
Change.compare = function (a, b) {
if (!Change.isChange(a) || !Change.isChange(b)) { throw new TypeError('can only compare Changes') }
if (a.operation < b.operation) { return -1 }
if (a.operation > b.operation) { return 1 }
return Attribute.compare(a.modification, b.modification)
}
/**
* Apply a Change to properties of an object.
*
* @param {Object} change the change to apply.
* @param {Object} obj the object to apply it to.
* @param {Boolean} scalar convert single-item arrays to scalars. Default: false
*/
Change.apply = function apply (change, obj, scalar) {
assert.string(change.operation)
assert.string(change.modification.type)
assert.ok(Array.isArray(change.modification.vals))
assert.object(obj)
const type = change.modification.type
const vals = change.modification.vals
let data = obj[type]
if (data !== undefined) {
if (!Array.isArray(data)) {
data = [data]
}
} else {
data = []
}
switch (change.operation) {
case 'replace':
if (vals.length === 0) {
// replace empty is a delete
delete obj[type]
return obj
} else {
data = vals
}
break
case 'add': {
// add only new unique entries
const newValues = vals.filter(function (entry) {
return (data.indexOf(entry) === -1)
})
data = data.concat(newValues)
break
}
case 'delete':
data = data.filter(function (entry) {
return (vals.indexOf(entry) === -1)
})
if (data.length === 0) {
// Erase the attribute if empty
delete obj[type]
return obj
}
break
default:
break
}
if (scalar && data.length === 1) {
// store single-value outputs as scalars, if requested
obj[type] = data[0]
} else {
obj[type] = data
}
return obj
}
Change.prototype.parse = function (ber) {
assert.ok(ber)
ber.readSequence()
this._operation = ber.readEnumeration()
this._modification = new Attribute()
this._modification.parse(ber)
return true
}
Change.prototype.toBer = function (ber) {
assert.ok(ber)
ber.startSequence()
ber.writeEnumeration(this._operation)
ber = this._modification.toBer(ber)
ber.endSequence()
return ber
}
/// --- Exports
module.exports = Change

View File

@ -15,37 +15,37 @@ const vasync = require('vasync')
const assert = require('assert-plus') const assert = require('assert-plus')
const VError = require('verror').VError const VError = require('verror').VError
const Attribute = require('@ldapjs/attribute') const Attribute = require('../attribute')
const Change = require('@ldapjs/change') const Change = require('../change')
const Control = require('../controls/index').Control const Control = require('../controls/index').Control
const { Control: LdapControl } = require('@ldapjs/controls')
const SearchPager = require('./search_pager') const SearchPager = require('./search_pager')
const Protocol = require('@ldapjs/protocol') const Protocol = require('../protocol')
const { DN } = require('@ldapjs/dn') const dn = require('../dn')
const errors = require('../errors') const errors = require('../errors')
const filters = require('@ldapjs/filter') const filters = require('../filters')
const Parser = require('../messages/parser') const messages = require('../messages')
const url = require('../url') const url = require('../url')
const CorkedEmitter = require('../corked_emitter') const CorkedEmitter = require('../corked_emitter')
/// --- Globals /// --- Globals
const messages = require('@ldapjs/messages') const AbandonRequest = messages.AbandonRequest
const { const AddRequest = messages.AddRequest
AbandonRequest, const BindRequest = messages.BindRequest
AddRequest, const CompareRequest = messages.CompareRequest
BindRequest, const DeleteRequest = messages.DeleteRequest
CompareRequest, const ExtendedRequest = messages.ExtendedRequest
DeleteRequest, const ModifyRequest = messages.ModifyRequest
ExtensionRequest: ExtendedRequest, const ModifyDNRequest = messages.ModifyDNRequest
ModifyRequest, const SearchRequest = messages.SearchRequest
ModifyDnRequest: ModifyDNRequest, const UnbindRequest = messages.UnbindRequest
SearchRequest, const UnbindResponse = messages.UnbindResponse
UnbindRequest,
LdapResult: LDAPResult, const LDAPResult = messages.LDAPResult
SearchResultEntry: SearchEntry, const SearchEntry = messages.SearchEntry
SearchResultReference: SearchReference const SearchReference = messages.SearchReference
} = messages // var SearchResponse = messages.SearchResponse
const Parser = messages.Parser
const PresenceFilter = filters.PresenceFilter const PresenceFilter = filters.PresenceFilter
@ -67,9 +67,9 @@ function nextClientId () {
function validateControls (controls) { function validateControls (controls) {
if (Array.isArray(controls)) { if (Array.isArray(controls)) {
controls.forEach(function (c) { controls.forEach(function (c) {
if (!(c instanceof Control) && !(c instanceof LdapControl)) { throw new TypeError('controls must be [Control]') } if (!(c instanceof Control)) { throw new TypeError('controls must be [Control]') }
}) })
} else if (controls instanceof Control || controls instanceof LdapControl) { } else if (controls instanceof Control) {
controls = [controls] controls = [controls]
} else { } else {
throw new TypeError('controls must be [Control]') throw new TypeError('controls must be [Control]')
@ -78,11 +78,13 @@ function validateControls (controls) {
return controls return controls
} }
function ensureDN (input) { function ensureDN (input, strict) {
if (DN.isDn(input)) { if (dn.DN.isDN(input)) {
return input return dn
} else if (strict) {
return dn.parse(input)
} else if (typeof (input) === 'string') { } else if (typeof (input) === 'string') {
return DN.fromString(input) return input
} else { } else {
throw new Error('invalid DN') throw new Error('invalid DN')
} }
@ -134,6 +136,7 @@ function Client (options) {
failAfter: parseInt(rOpts.failAfter, 10) || Infinity failAfter: parseInt(rOpts.failAfter, 10) || Infinity
} }
} }
this.strictDN = (options.strictDN !== undefined) ? options.strictDN : true
this.queue = requestQueueFactory({ this.queue = requestQueueFactory({
size: parseInt((options.queueSize || 0), 10), size: parseInt((options.queueSize || 0), 10),
@ -174,13 +177,13 @@ module.exports = Client
* The callback will be invoked as soon as the data is flushed out to the * The callback will be invoked as soon as the data is flushed out to the
* network, as there is never a response from abandon. * network, as there is never a response from abandon.
* *
* @param {Number} messageId the messageId to abandon. * @param {Number} messageID the messageID to abandon.
* @param {Control} controls (optional) either a Control or [Control]. * @param {Control} controls (optional) either a Control or [Control].
* @param {Function} callback of the form f(err). * @param {Function} callback of the form f(err).
* @throws {TypeError} on invalid input. * @throws {TypeError} on invalid input.
*/ */
Client.prototype.abandon = function abandon (messageId, controls, callback) { Client.prototype.abandon = function abandon (messageID, controls, callback) {
assert.number(messageId, 'messageId') assert.number(messageID, 'messageID')
if (typeof (controls) === 'function') { if (typeof (controls) === 'function') {
callback = controls callback = controls
controls = [] controls = []
@ -190,8 +193,8 @@ Client.prototype.abandon = function abandon (messageId, controls, callback) {
assert.func(callback, 'callback') assert.func(callback, 'callback')
const req = new AbandonRequest({ const req = new AbandonRequest({
abandonId: messageId, abandonID: messageID,
controls controls: controls
}) })
return this._send(req, 'abandon', null, callback) return this._send(req, 'abandon', null, callback)
@ -245,9 +248,9 @@ Client.prototype.add = function add (name, entry, controls, callback) {
} }
const req = new AddRequest({ const req = new AddRequest({
entry: ensureDN(name), entry: ensureDN(name, this.strictDN),
attributes: entry, attributes: entry,
controls controls: controls
}) })
return this._send(req, [errors.LDAP_SUCCESS], null, callback) return this._send(req, [errors.LDAP_SUCCESS], null, callback)
@ -267,12 +270,7 @@ Client.prototype.bind = function bind (name,
controls, controls,
callback, callback,
_bypass) { _bypass) {
if ( if (typeof (name) !== 'string' && !(name instanceof dn.DN)) { throw new TypeError('name (string) required') }
typeof (name) !== 'string' &&
Object.prototype.toString.call(name) !== '[object LdapDn]'
) {
throw new TypeError('name (string) required')
}
assert.optionalString(credentials, 'credentials') assert.optionalString(credentials, 'credentials')
if (typeof (controls) === 'function') { if (typeof (controls) === 'function') {
callback = controls callback = controls
@ -286,7 +284,7 @@ Client.prototype.bind = function bind (name,
name: name || '', name: name || '',
authentication: 'Simple', authentication: 'Simple',
credentials: credentials || '', credentials: credentials || '',
controls controls: controls
}) })
// Connection errors will be reported to the bind callback too (useful when the LDAP server is not available) // Connection errors will be reported to the bind callback too (useful when the LDAP server is not available)
@ -327,10 +325,10 @@ Client.prototype.compare = function compare (name,
assert.func(callback, 'callback') assert.func(callback, 'callback')
const req = new CompareRequest({ const req = new CompareRequest({
entry: ensureDN(name), entry: ensureDN(name, this.strictDN),
attribute: attr, attribute: attr,
value, value: value,
controls controls: controls
}) })
return this._send(req, CMP_EXPECT, null, function (err, res) { return this._send(req, CMP_EXPECT, null, function (err, res) {
@ -359,8 +357,8 @@ Client.prototype.del = function del (name, controls, callback) {
assert.func(callback, 'callback') assert.func(callback, 'callback')
const req = new DeleteRequest({ const req = new DeleteRequest({
entry: ensureDN(name), entry: ensureDN(name, this.strictDN),
controls controls: controls
}) })
return this._send(req, [errors.LDAP_SUCCESS], null, callback) return this._send(req, [errors.LDAP_SUCCESS], null, callback)
@ -397,7 +395,7 @@ Client.prototype.exop = function exop (name, value, controls, callback) {
const req = new ExtendedRequest({ const req = new ExtendedRequest({
requestName: name, requestName: name,
requestValue: value, requestValue: value,
controls controls: controls
}) })
return this._send(req, [errors.LDAP_SUCCESS], null, function (err, res) { return this._send(req, [errors.LDAP_SUCCESS], null, function (err, res) {
@ -470,9 +468,9 @@ Client.prototype.modify = function modify (name, change, controls, callback) {
assert.func(callback, 'callback') assert.func(callback, 'callback')
const req = new ModifyRequest({ const req = new ModifyRequest({
object: ensureDN(name), object: ensureDN(name, this.strictDN),
changes, changes: changes,
controls controls: controls
}) })
return this._send(req, [errors.LDAP_SUCCESS], null, callback) return this._send(req, [errors.LDAP_SUCCESS], null, callback)
@ -506,16 +504,18 @@ Client.prototype.modifyDN = function modifyDN (name,
} }
assert.func(callback) assert.func(callback)
const newDN = DN.fromString(newName) const DN = ensureDN(name)
// TODO: is non-strict handling desired here?
const newDN = dn.parse(newName)
const req = new ModifyDNRequest({ const req = new ModifyDNRequest({
entry: DN.fromString(name), entry: DN,
deleteOldRdn: true, deleteOldRdn: true,
controls controls: controls
}) })
if (newDN.length !== 1) { if (newDN.length !== 1) {
req.newRdn = DN.fromString(newDN.shift().toString()) req.newRdn = dn.parse(newDN.rdns.shift().toString())
req.newSuperior = newDN req.newSuperior = newDN
} else { } else {
req.newRdn = newDN req.newRdn = newDN
@ -571,7 +571,7 @@ Client.prototype.search = function search (base,
options.filter = filters.parseString(options.filter) options.filter = filters.parseString(options.filter)
} else if (!options.filter) { } else if (!options.filter) {
options.filter = new PresenceFilter({ attribute: 'objectclass' }) options.filter = new PresenceFilter({ attribute: 'objectclass' })
} else if (Object.prototype.toString.call(options.filter) !== '[object FilterString]') { } else if (!filters.isFilter(options.filter)) {
throw new TypeError('options.filter (Filter) required') throw new TypeError('options.filter (Filter) required')
} }
if (typeof (controls) === 'function') { if (typeof (controls) === 'function') {
@ -593,14 +593,14 @@ Client.prototype.search = function search (base,
} }
const self = this const self = this
const baseDN = ensureDN(base) const baseDN = ensureDN(base, this.strictDN)
function sendRequest (ctrls, emitter, cb) { function sendRequest (ctrls, emitter, cb) {
const req = new SearchRequest({ const req = new SearchRequest({
baseObject: baseDN, baseObject: baseDN,
scope: options.scope || 'base', scope: options.scope || 'base',
filter: options.filter, filter: options.filter,
derefAliases: options.derefAliases || Protocol.search.NEVER_DEREF_ALIASES, derefAliases: options.derefAliases || Protocol.NEVER_DEREF_ALIASES,
sizeLimit: options.sizeLimit || 0, sizeLimit: options.sizeLimit || 0,
timeLimit: options.timeLimit || 10, timeLimit: options.timeLimit || 10,
typesOnly: options.typesOnly || false, typesOnly: options.typesOnly || false,
@ -629,11 +629,11 @@ Client.prototype.search = function search (base,
} }
const pager = new SearchPager({ const pager = new SearchPager({
callback, callback: callback,
controls, controls: controls,
pageSize: size, pageSize: size,
pagePause: pageOpts.pagePause, pagePause: pageOpts.pagePause,
sendRequest sendRequest: sendRequest
}) })
pager.begin() pager.begin()
} else { } else {
@ -723,7 +723,7 @@ Client.prototype.starttls = function starttls (options,
self._tracker.parser.write(data) self._tracker.parser.write(data)
}) })
secure.on('error', function (err) { secure.on('error', function (err) {
self.log.trace({ err }, 'error event: %s', new Error().stack) self.log.trace({ err: err }, 'error event: %s', new Error().stack)
self.emit('error', err) self.emit('error', err)
sock.destroy() sock.destroy()
@ -744,7 +744,7 @@ Client.prototype.starttls = function starttls (options,
const req = new ExtendedRequest({ const req = new ExtendedRequest({
requestName: '1.3.6.1.4.1.1466.20037', requestName: '1.3.6.1.4.1.1466.20037',
requestValue: null, requestValue: null,
controls controls: controls
}) })
return this._send(req, return this._send(req,
@ -857,7 +857,7 @@ Client.prototype.connect = function connect () {
function initSocket (server) { function initSocket (server) {
tracker = messageTrackerFactory({ tracker = messageTrackerFactory({
id: server ? server.href : self.socketPath, id: server ? server.href : self.socketPath,
parser: new Parser({ log }) parser: new Parser({ log: log })
}) })
// This won't be set on TLS. So. Very. Annoying. // This won't be set on TLS. So. Very. Annoying.
@ -876,52 +876,15 @@ Client.prototype.connect = function connect () {
}) })
// The "router" // The "router"
//
// This is invoked after the incoming BER has been parsed into a JavaScript
// object.
tracker.parser.on('message', function onMessage (message) { tracker.parser.on('message', function onMessage (message) {
message.connection = self._socket message.connection = self._socket
const trackedObject = tracker.fetch(message.messageId) const callback = tracker.fetch(message.messageID)
if (!trackedObject) {
log.error({ message: message.pojo }, 'unmatched server message received')
return false
}
const { message: trackedMessage, callback } = trackedObject
if (!callback) { if (!callback) {
log.error({ message: message.pojo }, 'unsolicited message') log.error({ message: message.json }, 'unsolicited message')
return false return false
} }
// Some message types have narrower implementations and require extra
// parsing to be complete. In particular, ExtensionRequest messages will
// return responses that do not identify the request that generated them.
// Therefore, we have to match the response to the request and handle
// the extra processing accordingly.
switch (trackedMessage.type) {
case 'ExtensionRequest': {
const extensionType = ExtendedRequest.recognizedOIDs().lookupName(trackedMessage.requestName)
switch (extensionType) {
case 'PASSWORD_MODIFY': {
message = messages.PasswordModifyResponse.fromResponse(message)
break
}
case 'WHO_AM_I': {
message = messages.WhoAmIResponse.fromResponse(message)
break
}
default:
}
break
}
default:
}
return callback(message) return callback(message)
}) })
@ -1002,7 +965,7 @@ Client.prototype.connect = function connect () {
socket.end() socket.end()
}) })
socket.on('error', function onSocketError (err) { socket.on('error', function onSocketError (err) {
log.trace({ err }, 'error event: %s', new Error().stack) log.trace({ err: err }, 'error event: %s', new Error().stack)
self.emit('error', err) self.emit('error', err)
socket.destroy() socket.destroy()
@ -1120,16 +1083,8 @@ Client.prototype._onClose = function _onClose (closeError) {
return cb(new ConnectionError(tracker.id + ' closed')) return cb(new ConnectionError(tracker.id + ' closed'))
} else { } else {
// Unbinds will be communicated as a success since we're closed // Unbinds will be communicated as a success since we're closed
// TODO: we are faking this "UnbindResponse" object in order to make const unbind = new UnbindResponse({ messageID: msgid })
// tests pass. There is no such thing as an "unbind response" in the LDAP unbind.status = 'unbind'
// protocol. When the client is revamped, this logic should be removed.
// ~ jsumners 2023-02-16
const Unbind = class extends LDAPResult {
messageID = msgid
messageId = msgid
status = 'unbind'
}
const unbind = new Unbind()
return cb(unbind) return cb(unbind)
} }
}) })
@ -1247,23 +1202,21 @@ Client.prototype._sendSocket = function _sendSocket (message,
function messageCallback (msg) { function messageCallback (msg) {
if (timer) { clearTimeout(timer) } if (timer) { clearTimeout(timer) }
log.trace({ msg: msg ? msg.pojo : null }, 'response received') log.trace({ msg: msg ? msg.json : null }, 'response received')
if (expect === 'abandon') { return sendResult('end', null) } if (expect === 'abandon') { return sendResult('end', null) }
if (msg instanceof SearchEntry || msg instanceof SearchReference) { if (msg instanceof SearchEntry || msg instanceof SearchReference) {
let event = msg.constructor.name let event = msg.constructor.name
// Generate the event name for the event emitter, i.e. "searchEntry" event = event[0].toLowerCase() + event.slice(1)
// and "searchReference".
event = (event[0].toLowerCase() + event.slice(1)).replaceAll('Result', '')
return sendResult(event, msg) return sendResult(event, msg)
} else { } else {
tracker.remove(message.messageId) tracker.remove(message.messageID)
// Potentially mark client as idle // Potentially mark client as idle
self._updateIdle() self._updateIdle()
if (msg instanceof LDAPResult) { if (msg instanceof LDAPResult) {
if (msg.status !== 0 && expect.indexOf(msg.status) === -1) { if (expect.indexOf(msg.status) === -1) {
return sendResult('error', errors.getError(msg)) return sendResult('error', errors.getError(msg))
} }
return sendResult('end', msg) return sendResult('end', msg)
@ -1277,7 +1230,7 @@ Client.prototype._sendSocket = function _sendSocket (message,
function onRequestTimeout () { function onRequestTimeout () {
self.emit('timeout', message) self.emit('timeout', message)
const { callback: cb } = tracker.fetch(message.messageId) const cb = tracker.fetch(message.messageID)
if (cb) { if (cb) {
// FIXME: the timed-out request should be abandoned // FIXME: the timed-out request should be abandoned
cb(new errors.TimeoutError('request timeout (client interrupt)')) cb(new errors.TimeoutError('request timeout (client interrupt)'))
@ -1286,8 +1239,8 @@ Client.prototype._sendSocket = function _sendSocket (message,
function writeCallback () { function writeCallback () {
if (expect === 'abandon') { if (expect === 'abandon') {
// Mark the messageId specified as abandoned // Mark the messageID specified as abandoned
tracker.abandon(message.abandonId) tracker.abandon(message.abandonID)
// No need to track the abandon request itself // No need to track the abandon request itself
tracker.remove(message.id) tracker.remove(message.id)
return callback(null) return callback(null)
@ -1319,11 +1272,10 @@ Client.prototype._sendSocket = function _sendSocket (message,
timer = setTimeout(onRequestTimeout, self.timeout) timer = setTimeout(onRequestTimeout, self.timeout)
} }
log.trace('sending request %j', message.pojo) log.trace('sending request %j', message.json)
try { try {
const messageBer = message.toBer() return conn.write(message.toBer(), writeCallback)
return conn.write(messageBer.buffer, writeCallback)
} catch (e) { } catch (e) {
if (timer) { clearTimeout(timer) } if (timer) { clearTimeout(timer) }

View File

@ -4,7 +4,7 @@ const logger = require('../logger')
const Client = require('./client') const Client = require('./client')
module.exports = { module.exports = {
Client, Client: Client,
createClient: function createClient (options) { createClient: function createClient (options) {
if (isObject(options) === false) throw TypeError('options (object) required') if (isObject(options) === false) throw TypeError('options (object) required')
if (options.url && typeof options.url !== 'string' && !Array.isArray(options.url)) throw TypeError('options.url (string|array) required') if (options.url && typeof options.url !== 'string' && !Array.isArray(options.url)) throw TypeError('options.url (string|array) required')

View File

@ -62,23 +62,13 @@ module.exports = function messageTrackerFactory (options) {
*/ */
tracker.abandon = function abandonMessage (msgID) { tracker.abandon = function abandonMessage (msgID) {
if (messages.has(msgID) === false) return false if (messages.has(msgID) === false) return false
const toAbandon = messages.get(msgID)
abandoned.set(msgID, { abandoned.set(msgID, {
age: currentID, age: currentID,
message: toAbandon.message, cb: messages.get(msgID)
cb: toAbandon.callback
}) })
return messages.delete(msgID) return messages.delete(msgID)
} }
/**
* @typedef {object} Tracked
* @property {object} message The tracked message. Usually the outgoing
* request object.
* @property {Function} callback The handler to use when receiving a
* response to the tracked message.
*/
/** /**
* Retrieves the message handler for a message. Removes abandoned messages * Retrieves the message handler for a message. Removes abandoned messages
* that have been given time to be resolved. * that have been given time to be resolved.
@ -89,10 +79,10 @@ module.exports = function messageTrackerFactory (options) {
* @method fetch * @method fetch
*/ */
tracker.fetch = function fetchMessage (msgID) { tracker.fetch = function fetchMessage (msgID) {
const tracked = messages.get(msgID) const messageCB = messages.get(msgID)
if (tracked) { if (messageCB) {
purgeAbandoned(msgID, abandoned) purgeAbandoned(msgID, abandoned)
return tracked return messageCB
} }
// We sent an abandon request but the server either wasn't able to process // We sent an abandon request but the server either wasn't able to process
@ -101,7 +91,7 @@ module.exports = function messageTrackerFactory (options) {
// to be processed normally. // to be processed normally.
const abandonedMsg = abandoned.get(msgID) const abandonedMsg = abandoned.get(msgID)
if (abandonedMsg) { if (abandonedMsg) {
return { message: abandonedMsg, callback: abandonedMsg.cb } return abandonedMsg.cb
} }
return null return null
@ -120,7 +110,7 @@ module.exports = function messageTrackerFactory (options) {
messages.forEach((val, key) => { messages.forEach((val, key) => {
purgeAbandoned(key, abandoned) purgeAbandoned(key, abandoned)
tracker.remove(key) tracker.remove(key)
cb(key, val.callback) cb(key, val)
}) })
} }
@ -142,7 +132,7 @@ module.exports = function messageTrackerFactory (options) {
* Add a message handler to be tracked. * Add a message handler to be tracked.
* *
* @param {object} message The message object to be tracked. This object will * @param {object} message The message object to be tracked. This object will
* have a new property added to it: `messageId`. * have a new property added to it: `messageID`.
* @param {function} callback The handler for the message. * @param {function} callback The handler for the message.
* *
* @memberof MessageTracker * @memberof MessageTracker
@ -153,8 +143,8 @@ module.exports = function messageTrackerFactory (options) {
// This side effect is not ideal but the client doesn't attach the tracker // This side effect is not ideal but the client doesn't attach the tracker
// to itself until after the `.connect` method has fired. If this can be // to itself until after the `.connect` method has fired. If this can be
// refactored later, then we can possibly get rid of this side effect. // refactored later, then we can possibly get rid of this side effect.
message.messageId = currentID message.messageID = currentID
messages.set(currentID, { callback, message }) messages.set(currentID, callback)
} }
return tracker return tracker

View File

@ -14,7 +14,7 @@
* is not accepting any requests. * is not accepting any requests.
*/ */
module.exports = function enqueue (message, expect, emitter, cb) { module.exports = function enqueue (message, expect, emitter, cb) {
if (this._queue.size >= this.size || this._frozen) { if (this._queue.length >= this.size || this._frozen) {
return false return false
} }

View File

@ -2,8 +2,14 @@
const EventEmitter = require('events').EventEmitter const EventEmitter = require('events').EventEmitter
const util = require('util') const util = require('util')
const assert = require('assert-plus') const assert = require('assert-plus')
const { PagedResultsControl } = require('@ldapjs/controls')
// var dn = require('../dn')
// var messages = require('../messages/index')
// var Protocol = require('../protocol')
const PagedControl = require('../controls/paged_results_control.js')
const CorkedEmitter = require('../corked_emitter.js') const CorkedEmitter = require('../corked_emitter.js')
/// --- API /// --- API
@ -41,7 +47,7 @@ function SearchPager (opts) {
this.sendRequest = opts.sendRequest this.sendRequest = opts.sendRequest
this.controls.forEach(function (control) { this.controls.forEach(function (control) {
if (control.type === PagedResultsControl.OID) { if (control.type === PagedControl.OID) {
// The point of using SearchPager is not having to do this. // The point of using SearchPager is not having to do this.
// Toss an error if the pagedResultsControl is present // Toss an error if the pagedResultsControl is present
throw new Error('redundant pagedResultControl') throw new Error('redundant pagedResultControl')
@ -73,7 +79,7 @@ SearchPager.prototype._onEnd = function _onEnd (res) {
const self = this const self = this
let cookie = null let cookie = null
res.controls.forEach(function (control) { res.controls.forEach(function (control) {
if (control.type === PagedResultsControl.OID) { if (control.type === PagedControl.OID) {
cookie = control.value.cookie cookie = control.value.cookie
} }
}) })
@ -89,13 +95,13 @@ SearchPager.prototype._onEnd = function _onEnd (res) {
if (this.listeners('pageError').length > 0) { if (this.listeners('pageError').length > 0) {
this.emit('pageError', err) this.emit('pageError', err)
// If the consumer as subscribed to pageError, SearchPager is absolved // If the consumer as subscribed to pageError, SearchPager is absolved
// from delivering the fault via the 'error' event. Emitting an 'end' // from deliverying the fault via the 'error' event. Emitting an 'end'
// event after 'error' breaks the contract that the standard client // event after 'error' breaks the contract that the standard client
// provides, so it's only a possibility if 'pageError' is used instead. // provides, so it's only a possibility if 'pageError' is used instead.
this.emit('end', res) this.emit('end', res)
} else { } else {
this.emit('error', err) this.emit('error', err)
// No end event possible per explanation above. // No end event possible per explaination above.
} }
return return
} }
@ -134,10 +140,10 @@ SearchPager.prototype._onError = function _onError (err) {
*/ */
SearchPager.prototype._nextPage = function _nextPage (cookie) { SearchPager.prototype._nextPage = function _nextPage (cookie) {
const controls = this.controls.slice(0) const controls = this.controls.slice(0)
controls.push(new PagedResultsControl({ controls.push(new PagedControl({
value: { value: {
size: this.pageSize, size: this.pageSize,
cookie cookie: cookie
} }
})) }))

61
lib/controls/control.js Normal file
View File

@ -0,0 +1,61 @@
// Copyright 2011 Mark Cavage, Inc. All rights reserved.
const assert = require('assert-plus')
// var asn1 = require('asn1')
// var Protocol = require('../protocol')
/// --- Globals
// var Ber = asn1.Ber
/// --- API
function Control (options) {
assert.optionalObject(options)
options = options || {}
assert.optionalString(options.type)
assert.optionalBool(options.criticality)
if (options.value) {
assert.buffer(options.value)
}
this.type = options.type || ''
this.criticality = options.critical || options.criticality || false
this.value = options.value || null
}
Object.defineProperties(Control.prototype, {
json: {
get: function getJson () {
const obj = {
controlType: this.type,
criticality: this.criticality,
controlValue: this.value
}
return (typeof (this._json) === 'function' ? this._json(obj) : obj)
}
}
})
Control.prototype.toBer = function toBer (ber) {
assert.ok(ber)
ber.startSequence()
ber.writeString(this.type || '')
ber.writeBoolean(this.criticality)
if (typeof (this._toBer) === 'function') {
this._toBer(ber)
} else {
if (this.value) { ber.writeString(this.value) }
}
ber.endSequence()
}
Control.prototype.toString = function toString () {
return this.json
}
/// --- Exports
module.exports = Control

View File

@ -0,0 +1,83 @@
const assert = require('assert-plus')
const util = require('util')
const asn1 = require('asn1')
const Control = require('./control')
/// --- Globals
const BerReader = asn1.BerReader
const BerWriter = asn1.BerWriter
/// --- API
function EntryChangeNotificationControl (options) {
assert.optionalObject(options)
options = options || {}
options.type = EntryChangeNotificationControl.OID
if (options.value) {
if (Buffer.isBuffer(options.value)) {
this.parse(options.value)
} else if (typeof (options.value) === 'object') {
this._value = options.value
} else {
throw new TypeError('options.value must be a Buffer or Object')
}
options.value = null
}
Control.call(this, options)
}
util.inherits(EntryChangeNotificationControl, Control)
Object.defineProperties(EntryChangeNotificationControl.prototype, {
value: {
get: function () { return this._value || {} },
configurable: false
}
})
EntryChangeNotificationControl.prototype.parse = function parse (buffer) {
assert.ok(buffer)
const ber = new BerReader(buffer)
if (ber.readSequence()) {
this._value = {
changeType: ber.readInt()
}
// if the operation was moddn, then parse the optional previousDN attr
if (this._value.changeType === 8) { this._value.previousDN = ber.readString() }
this._value.changeNumber = ber.readInt()
return true
}
return false
}
EntryChangeNotificationControl.prototype._toBer = function (ber) {
assert.ok(ber)
if (!this._value) { return }
const writer = new BerWriter()
writer.startSequence()
writer.writeInt(this.value.changeType)
if (this.value.previousDN) { writer.writeString(this.value.previousDN) }
writer.writeInt(parseInt(this.value.changeNumber, 10))
writer.endSequence()
ber.writeBuffer(writer.buffer, 0x04)
}
EntryChangeNotificationControl.prototype._json = function (obj) {
obj.controlValue = this.value
return obj
}
EntryChangeNotificationControl.OID = '2.16.840.1.113730.3.4.7'
/// --- Exports
module.exports = EntryChangeNotificationControl

View File

@ -1,4 +1,86 @@
// Copyright 2011 Mark Cavage, Inc. All rights reserved. // Copyright 2011 Mark Cavage, Inc. All rights reserved.
const controls = require('@ldapjs/controls') const assert = require('assert')
module.exports = controls const Ber = require('asn1').Ber
const Control = require('./control')
const EntryChangeNotificationControl =
require('./entry_change_notification_control')
const PersistentSearchControl = require('./persistent_search_control')
const PagedResultsControl = require('./paged_results_control')
const ServerSideSortingRequestControl =
require('./server_side_sorting_request_control.js')
const ServerSideSortingResponseControl =
require('./server_side_sorting_response_control.js')
const VirtualListViewRequestControl =
require('./virtual_list_view_request_control.js')
const VirtualListViewResponseControl =
require('./virtual_list_view_response_control.js')
/// --- API
module.exports = {
getControl: function getControl (ber) {
assert.ok(ber)
if (ber.readSequence() === null) { return null }
let type
const opts = {
criticality: false,
value: null
}
if (ber.length) {
const end = ber.offset + ber.length
type = ber.readString()
if (ber.offset < end) {
if (ber.peek() === Ber.Boolean) { opts.criticality = ber.readBoolean() }
}
if (ber.offset < end) { opts.value = ber.readString(Ber.OctetString, true) }
}
let control
switch (type) {
case PersistentSearchControl.OID:
control = new PersistentSearchControl(opts)
break
case EntryChangeNotificationControl.OID:
control = new EntryChangeNotificationControl(opts)
break
case PagedResultsControl.OID:
control = new PagedResultsControl(opts)
break
case ServerSideSortingRequestControl.OID:
control = new ServerSideSortingRequestControl(opts)
break
case ServerSideSortingResponseControl.OID:
control = new ServerSideSortingResponseControl(opts)
break
case VirtualListViewRequestControl.OID:
control = new VirtualListViewRequestControl(opts)
break
case VirtualListViewResponseControl.OID:
control = new VirtualListViewResponseControl(opts)
break
default:
opts.type = type
control = new Control(opts)
break
}
return control
},
Control: Control,
EntryChangeNotificationControl: EntryChangeNotificationControl,
PagedResultsControl: PagedResultsControl,
PersistentSearchControl: PersistentSearchControl,
ServerSideSortingRequestControl: ServerSideSortingRequestControl,
ServerSideSortingResponseControl: ServerSideSortingResponseControl,
VirtualListViewRequestControl: VirtualListViewRequestControl,
VirtualListViewResponseControl: VirtualListViewResponseControl
}

View File

@ -0,0 +1,82 @@
const assert = require('assert-plus')
const util = require('util')
const asn1 = require('asn1')
const Control = require('./control')
/// --- Globals
const BerReader = asn1.BerReader
const BerWriter = asn1.BerWriter
/// --- API
function PagedResultsControl (options) {
assert.optionalObject(options)
options = options || {}
options.type = PagedResultsControl.OID
if (options.value) {
if (Buffer.isBuffer(options.value)) {
this.parse(options.value)
} else if (typeof (options.value) === 'object') {
this._value = options.value
} else {
throw new TypeError('options.value must be a Buffer or Object')
}
options.value = null
}
Control.call(this, options)
}
util.inherits(PagedResultsControl, Control)
Object.defineProperties(PagedResultsControl.prototype, {
value: {
get: function () { return this._value || {} },
configurable: false
}
})
PagedResultsControl.prototype.parse = function parse (buffer) {
assert.ok(buffer)
const ber = new BerReader(buffer)
if (ber.readSequence()) {
this._value = {}
this._value.size = ber.readInt()
this._value.cookie = ber.readString(asn1.Ber.OctetString, true)
// readString returns '' instead of a zero-length buffer
if (!this._value.cookie) { this._value.cookie = Buffer.alloc(0) }
return true
}
return false
}
PagedResultsControl.prototype._toBer = function (ber) {
assert.ok(ber)
if (!this._value) { return }
const writer = new BerWriter()
writer.startSequence()
writer.writeInt(this.value.size)
if (this.value.cookie && this.value.cookie.length > 0) {
writer.writeBuffer(this.value.cookie, asn1.Ber.OctetString)
} else {
writer.writeString('') // writeBuffer rejects zero-length buffers
}
writer.endSequence()
ber.writeBuffer(writer.buffer, 0x04)
}
PagedResultsControl.prototype._json = function (obj) {
obj.controlValue = this.value
return obj
}
PagedResultsControl.OID = '1.2.840.113556.1.4.319'
/// --- Exports
module.exports = PagedResultsControl

View File

@ -0,0 +1,82 @@
// Copyright 2011 Mark Cavage, Inc. All rights reserved.
const assert = require('assert-plus')
const util = require('util')
const asn1 = require('asn1')
const Control = require('./control')
/// --- Globals
const BerReader = asn1.BerReader
const BerWriter = asn1.BerWriter
/// --- API
function PersistentSearchControl (options) {
assert.optionalObject(options)
options = options || {}
options.type = PersistentSearchControl.OID
if (options.value) {
if (Buffer.isBuffer(options.value)) {
this.parse(options.value)
} else if (typeof (options.value) === 'object') {
this._value = options.value
} else {
throw new TypeError('options.value must be a Buffer or Object')
}
options.value = null
}
Control.call(this, options)
}
util.inherits(PersistentSearchControl, Control)
Object.defineProperties(PersistentSearchControl.prototype, {
value: {
get: function () { return this._value || {} },
configurable: false
}
})
PersistentSearchControl.prototype.parse = function parse (buffer) {
assert.ok(buffer)
const ber = new BerReader(buffer)
if (ber.readSequence()) {
this._value = {
changeTypes: ber.readInt(),
changesOnly: ber.readBoolean(),
returnECs: ber.readBoolean()
}
return true
}
return false
}
PersistentSearchControl.prototype._toBer = function (ber) {
assert.ok(ber)
if (!this._value) { return }
const writer = new BerWriter()
writer.startSequence()
writer.writeInt(this.value.changeTypes)
writer.writeBoolean(this.value.changesOnly)
writer.writeBoolean(this.value.returnECs)
writer.endSequence()
ber.writeBuffer(writer.buffer, 0x04)
}
PersistentSearchControl.prototype._json = function (obj) {
obj.controlValue = this.value
return obj
}
PersistentSearchControl.OID = '2.16.840.1.113730.3.4.3'
/// --- Exports
module.exports = PersistentSearchControl

View File

@ -0,0 +1,108 @@
const assert = require('assert-plus')
const util = require('util')
const asn1 = require('asn1')
const Control = require('./control')
/// --- Globals
const BerReader = asn1.BerReader
const BerWriter = asn1.BerWriter
/// --- API
function ServerSideSortingRequestControl (options) {
assert.optionalObject(options)
options = options || {}
options.type = ServerSideSortingRequestControl.OID
if (options.value) {
if (Buffer.isBuffer(options.value)) {
this.parse(options.value)
} else if (Array.isArray(options.value)) {
assert.arrayOfObject(options.value, 'options.value must be Objects')
for (let i = 0; i < options.value.length; i++) {
if (Object.prototype.hasOwnProperty.call(options.value[i], 'attributeType') === false) {
throw new Error('Missing required key: attributeType')
}
}
this._value = options.value
} else if (typeof (options.value) === 'object') {
if (Object.prototype.hasOwnProperty.call(options.value, 'attributeType') === false) {
throw new Error('Missing required key: attributeType')
}
this._value = [options.value]
} else {
throw new TypeError('options.value must be a Buffer, Array or Object')
}
options.value = null
}
Control.call(this, options)
}
util.inherits(ServerSideSortingRequestControl, Control)
Object.defineProperties(ServerSideSortingRequestControl.prototype, {
value: {
get: function () { return this._value || [] },
configurable: false
}
})
ServerSideSortingRequestControl.prototype.parse = function parse (buffer) {
assert.ok(buffer)
const ber = new BerReader(buffer)
let item
if (ber.readSequence(0x30)) {
this._value = []
while (ber.readSequence(0x30)) {
item = {}
item.attributeType = ber.readString(asn1.Ber.OctetString)
if (ber.peek() === 0x80) {
item.orderingRule = ber.readString(0x80)
}
if (ber.peek() === 0x81) {
item.reverseOrder = (ber._readTag(0x81) !== 0)
}
this._value.push(item)
}
return true
}
return false
}
ServerSideSortingRequestControl.prototype._toBer = function (ber) {
assert.ok(ber)
if (!this._value || this.value.length === 0) { return }
const writer = new BerWriter()
writer.startSequence(0x30)
for (let i = 0; i < this.value.length; i++) {
const item = this.value[i]
writer.startSequence(0x30)
if (item.attributeType) {
writer.writeString(item.attributeType, asn1.Ber.OctetString)
}
if (item.orderingRule) {
writer.writeString(item.orderingRule, 0x80)
}
if (item.reverseOrder) {
writer.writeBoolean(item.reverseOrder, 0x81)
}
writer.endSequence()
}
writer.endSequence()
ber.writeBuffer(writer.buffer, 0x04)
}
ServerSideSortingRequestControl.prototype._json = function (obj) {
obj.controlValue = this.value
return obj
}
ServerSideSortingRequestControl.OID = '1.2.840.113556.1.4.473'
/// ---Exports
module.exports = ServerSideSortingRequestControl

View File

@ -0,0 +1,100 @@
const assert = require('assert-plus')
const util = require('util')
const asn1 = require('asn1')
const Control = require('./control')
const CODES = require('../errors/codes')
/// --- Globals
const BerReader = asn1.BerReader
const BerWriter = asn1.BerWriter
const VALID_CODES = [
CODES.LDAP_SUCCESS,
CODES.LDAP_OPERATIONS_ERROR,
CODES.LDAP_TIME_LIMIT_EXCEEDED,
CODES.LDAP_STRONG_AUTH_REQUIRED,
CODES.LDAP_ADMIN_LIMIT_EXCEEDED,
CODES.LDAP_NO_SUCH_ATTRIBUTE,
CODES.LDAP_INAPPROPRIATE_MATCHING,
CODES.LDAP_INSUFFICIENT_ACCESS_RIGHTS,
CODES.LDAP_BUSY,
CODES.LDAP_UNWILLING_TO_PERFORM,
CODES.LDAP_OTHER
]
function ServerSideSortingResponseControl (options) {
assert.optionalObject(options)
options = options || {}
options.type = ServerSideSortingResponseControl.OID
options.criticality = false
if (options.value) {
if (Buffer.isBuffer(options.value)) {
this.parse(options.value)
} else if (typeof (options.value) === 'object') {
if (VALID_CODES.indexOf(options.value.result) === -1) {
throw new Error('Invalid result code')
}
if (options.value.failedAttribute &&
typeof (options.value.failedAttribute) !== 'string') {
throw new Error('failedAttribute must be String')
}
this._value = options.value
} else {
throw new TypeError('options.value must be a Buffer or Object')
}
options.value = null
}
Control.call(this, options)
}
util.inherits(ServerSideSortingResponseControl, Control)
Object.defineProperties(ServerSideSortingResponseControl.prototype, {
value: {
get: function () { return this._value || {} },
configurable: false
}
})
ServerSideSortingResponseControl.prototype.parse = function parse (buffer) {
assert.ok(buffer)
const ber = new BerReader(buffer)
if (ber.readSequence(0x30)) {
this._value = {}
this._value.result = ber.readEnumeration()
if (ber.peek() === 0x80) {
this._value.failedAttribute = ber.readString(0x80)
}
return true
}
return false
}
ServerSideSortingResponseControl.prototype._toBer = function (ber) {
assert.ok(ber)
if (!this._value || this.value.length === 0) { return }
const writer = new BerWriter()
writer.startSequence(0x30)
writer.writeEnumeration(this.value.result)
if (this.value.result !== CODES.LDAP_SUCCESS && this.value.failedAttribute) {
writer.writeString(this.value.failedAttribute, 0x80)
}
writer.endSequence()
ber.writeBuffer(writer.buffer, 0x04)
}
ServerSideSortingResponseControl.prototype._json = function (obj) {
obj.controlValue = this.value
return obj
}
ServerSideSortingResponseControl.OID = '1.2.840.113556.1.4.474'
/// --- Exports
module.exports = ServerSideSortingResponseControl

View File

@ -0,0 +1,94 @@
const assert = require('assert-plus')
const util = require('util')
const asn1 = require('asn1')
const Control = require('./control')
/// --- Globals
const BerReader = asn1.BerReader
const BerWriter = asn1.BerWriter
/// --- API
function VirtualListViewControl (options) {
assert.optionalObject(options)
options = options || {}
options.type = VirtualListViewControl.OID
if (options.value) {
if (Buffer.isBuffer(options.value)) {
this.parse(options.value)
} else if (typeof (options.value) === 'object') {
if (Object.prototype.hasOwnProperty.call(options.value, 'beforeCount') === false) {
throw new Error('Missing required key: beforeCount')
}
if (Object.prototype.hasOwnProperty.call(options.value, 'afterCount') === false) {
throw new Error('Missing required key: afterCount')
}
this._value = options.value
} else {
throw new TypeError('options.value must be a Buffer or Object')
}
options.value = null
}
Control.call(this, options)
}
util.inherits(VirtualListViewControl, Control)
Object.defineProperties(VirtualListViewControl.prototype, {
value: {
get: function () { return this._value || [] },
configurable: false
}
})
VirtualListViewControl.prototype.parse = function parse (buffer) {
assert.ok(buffer)
const ber = new BerReader(buffer)
if (ber.readSequence()) {
this._value = {}
this._value.beforeCount = ber.readInt()
this._value.afterCount = ber.readInt()
if (ber.peek() === 0xa0) {
if (ber.readSequence(0xa0)) {
this._value.targetOffset = ber.readInt()
this._value.contentCount = ber.readInt()
}
}
if (ber.peek() === 0x81) {
this._value.greaterThanOrEqual = ber.readString(0x81)
}
return true
}
return false
}
VirtualListViewControl.prototype._toBer = function (ber) {
assert.ok(ber)
if (!this._value || this.value.length === 0) {
return
}
const writer = new BerWriter()
writer.startSequence(0x30)
writer.writeInt(this.value.beforeCount)
writer.writeInt(this.value.afterCount)
if (this.value.targetOffset !== undefined) {
writer.startSequence(0xa0)
writer.writeInt(this.value.targetOffset)
writer.writeInt(this.value.contentCount)
writer.endSequence()
} else if (this.value.greaterThanOrEqual !== undefined) {
writer.writeString(this.value.greaterThanOrEqual, 0x81)
}
writer.endSequence()
ber.writeBuffer(writer.buffer, 0x04)
}
VirtualListViewControl.prototype._json = function (obj) {
obj.controlValue = this.value
return obj
}
VirtualListViewControl.OID = '2.16.840.1.113730.3.4.9'
/// ---Exports
module.exports = VirtualListViewControl

View File

@ -0,0 +1,112 @@
const assert = require('assert-plus')
const util = require('util')
const asn1 = require('asn1')
const Control = require('./control')
const CODES = require('../errors/codes')
/// --- Globals
const BerReader = asn1.BerReader
const BerWriter = asn1.BerWriter
const VALID_CODES = [
CODES.LDAP_SUCCESS,
CODES.LDAP_OPERATIONS_ERROR,
CODES.LDAP_UNWILLING_TO_PERFORM,
CODES.LDAP_INSUFFICIENT_ACCESS_RIGHTS,
CODES.LDAP_BUSY,
CODES.LDAP_TIME_LIMIT_EXCEEDED,
CODES.LDAP_ADMIN_LIMIT_EXCEEDED,
CODES.LDAP_SORT_CONTROL_MISSING,
CODES.LDAP_INDEX_RANGE_ERROR,
CODES.LDAP_CONTROL_ERROR,
CODES.LDAP_OTHER
]
function VirtualListViewResponseControl (options) {
assert.optionalObject(options)
options = options || {}
options.type = VirtualListViewResponseControl.OID
options.criticality = false
if (options.value) {
if (Buffer.isBuffer(options.value)) {
this.parse(options.value)
} else if (typeof (options.value) === 'object') {
if (VALID_CODES.indexOf(options.value.result) === -1) {
throw new Error('Invalid result code')
}
this._value = options.value
} else {
throw new TypeError('options.value must be a Buffer or Object')
}
options.value = null
}
Control.call(this, options)
}
util.inherits(VirtualListViewResponseControl, Control)
Object.defineProperties(VirtualListViewResponseControl.prototype, {
value: {
get: function () { return this._value || {} },
configurable: false
}
})
VirtualListViewResponseControl.prototype.parse = function parse (buffer) {
assert.ok(buffer)
const ber = new BerReader(buffer)
if (ber.readSequence()) {
this._value = {}
if (ber.peek(0x02)) {
this._value.targetPosition = ber.readInt()
}
if (ber.peek(0x02)) {
this._value.contentCount = ber.readInt()
}
this._value.result = ber.readEnumeration()
this._value.cookie = ber.readString(asn1.Ber.OctetString, true)
// readString returns '' instead of a zero-length buffer
if (!this._value.cookie) {
this._value.cookie = Buffer.alloc(0)
}
return true
}
return false
}
VirtualListViewResponseControl.prototype._toBer = function (ber) {
assert.ok(ber)
if (!this._value || this.value.length === 0) {
return
}
const writer = new BerWriter()
writer.startSequence()
if (this.value.targetPosition !== undefined) {
writer.writeInt(this.value.targetPosition)
}
if (this.value.contentCount !== undefined) {
writer.writeInt(this.value.contentCount)
}
writer.writeEnumeration(this.value.result)
if (this.value.cookie && this.value.cookie.length > 0) {
writer.writeBuffer(this.value.cookie, asn1.Ber.OctetString)
} else {
writer.writeString('') // writeBuffer rejects zero-length buffers
}
writer.endSequence()
ber.writeBuffer(writer.buffer, 0x04)
}
VirtualListViewResponseControl.prototype._json = function (obj) {
obj.controlValue = this.value
return obj
}
VirtualListViewResponseControl.OID = '2.16.840.1.113730.3.4.10'
/// --- Exports
module.exports = VirtualListViewResponseControl

473
lib/dn.js Normal file
View File

@ -0,0 +1,473 @@
// Copyright 2011 Mark Cavage, Inc. All rights reserved.
const assert = require('assert-plus')
/// --- Helpers
function invalidDN (name) {
const e = new Error()
e.name = 'InvalidDistinguishedNameError'
e.message = name
return e
}
function isAlphaNumeric (c) {
const re = /[A-Za-z0-9]/
return re.test(c)
}
function isWhitespace (c) {
const re = /\s/
return re.test(c)
}
function repeatChar (c, n) {
let out = ''
const max = n || 0
for (let i = 0; i < max; i++) { out += c }
return out
}
/// --- API
function RDN (obj) {
const self = this
this.attrs = {}
if (obj) {
Object.keys(obj).forEach(function (k) {
self.set(k, obj[k])
})
}
}
RDN.prototype.set = function rdnSet (name, value, opts) {
assert.string(name, 'name (string) required')
assert.string(value, 'value (string) required')
const self = this
const lname = name.toLowerCase()
this.attrs[lname] = {
value: value,
name: name
}
if (opts && typeof (opts) === 'object') {
Object.keys(opts).forEach(function (k) {
if (k !== 'value') { self.attrs[lname][k] = opts[k] }
})
}
}
RDN.prototype.equals = function rdnEquals (rdn) {
if (typeof (rdn) !== 'object') { return false }
const ourKeys = Object.keys(this.attrs)
const theirKeys = Object.keys(rdn.attrs)
if (ourKeys.length !== theirKeys.length) { return false }
ourKeys.sort()
theirKeys.sort()
for (let i = 0; i < ourKeys.length; i++) {
if (ourKeys[i] !== theirKeys[i]) { return false }
if (this.attrs[ourKeys[i]].value !== rdn.attrs[ourKeys[i]].value) { return false }
}
return true
}
/**
* Convert RDN to string according to specified formatting options.
* (see: DN.format for option details)
*/
RDN.prototype.format = function rdnFormat (options) {
assert.optionalObject(options, 'options must be an object')
options = options || {}
const self = this
let str = ''
function escapeValue (val, forceQuote) {
let out = ''
let cur = 0
const len = val.length
let quoted = false
/* BEGIN JSSTYLED */
// TODO: figure out what this regex is actually trying to test for and
// fix it to appease the linter.
/* eslint-disable-next-line no-useless-escape */
const escaped = /[\\\"]/
const special = /[,=+<>#;]/
/* END JSSTYLED */
if (len > 0) {
// Wrap strings with trailing or leading spaces in quotes
quoted = forceQuote || (val[0] === ' ' || val[len - 1] === ' ')
}
while (cur < len) {
if (escaped.test(val[cur]) || (!quoted && special.test(val[cur]))) {
out += '\\'
}
out += val[cur++]
}
if (quoted) { out = '"' + out + '"' }
return out
}
function sortParsed (a, b) {
return self.attrs[a].order - self.attrs[b].order
}
function sortStandard (a, b) {
const nameCompare = a.localeCompare(b)
if (nameCompare === 0) {
// TODO: Handle binary values
return self.attrs[a].value.localeCompare(self.attrs[b].value)
} else {
return nameCompare
}
}
const keys = Object.keys(this.attrs)
if (options.keepOrder) {
keys.sort(sortParsed)
} else {
keys.sort(sortStandard)
}
keys.forEach(function (key) {
const attr = self.attrs[key]
if (str.length) { str += '+' }
if (options.keepCase) {
str += attr.name
} else {
if (options.upperName) { str += key.toUpperCase() } else { str += key }
}
str += '=' + escapeValue(attr.value, (options.keepQuote && attr.quoted))
})
return str
}
RDN.prototype.toString = function rdnToString () {
return this.format()
}
// Thank you OpenJDK!
function parse (name) {
if (typeof (name) !== 'string') { throw new TypeError('name (string) required') }
let cur = 0
const len = name.length
function parseRdn () {
const rdn = new RDN()
let order = 0
rdn.spLead = trim()
while (cur < len) {
const opts = {
order: order
}
const attr = parseAttrType()
trim()
if (cur >= len || name[cur++] !== '=') { throw invalidDN(name) }
trim()
// Parameters about RDN value are set in 'opts' by parseAttrValue
const value = parseAttrValue(opts)
rdn.set(attr, value, opts)
rdn.spTrail = trim()
if (cur >= len || name[cur] !== '+') { break }
++cur
++order
}
return rdn
}
function trim () {
let count = 0
while ((cur < len) && isWhitespace(name[cur])) {
++cur
count++
}
return count
}
function parseAttrType () {
const beg = cur
while (cur < len) {
const c = name[cur]
if (isAlphaNumeric(c) ||
c === '.' ||
c === '-' ||
c === ' ') {
++cur
} else {
break
}
}
// Back out any trailing spaces.
while ((cur > beg) && (name[cur - 1] === ' ')) { --cur }
if (beg === cur) { throw invalidDN(name) }
return name.slice(beg, cur)
}
function parseAttrValue (opts) {
if (cur < len && name[cur] === '#') {
opts.binary = true
return parseBinaryAttrValue()
} else if (cur < len && name[cur] === '"') {
opts.quoted = true
return parseQuotedAttrValue()
} else {
return parseStringAttrValue()
}
}
function parseBinaryAttrValue () {
const beg = cur++
while (cur < len && isAlphaNumeric(name[cur])) { ++cur }
return name.slice(beg, cur)
}
function parseQuotedAttrValue () {
let str = ''
++cur // Consume the first quote
while ((cur < len) && name[cur] !== '"') {
if (name[cur] === '\\') { cur++ }
str += name[cur++]
}
if (cur++ >= len) {
// no closing quote
throw invalidDN(name)
}
return str
}
function parseStringAttrValue () {
const beg = cur
let str = ''
let esc = -1
while ((cur < len) && !atTerminator()) {
if (name[cur] === '\\') {
// Consume the backslash and mark its place just in case it's escaping
// whitespace which needs to be preserved.
esc = cur++
}
if (cur === len) {
// backslash followed by nothing
throw invalidDN(name)
}
str += name[cur++]
}
// Trim off (unescaped) trailing whitespace and rewind cursor to the end of
// the AttrValue to record whitespace length.
for (; cur > beg; cur--) {
if (!isWhitespace(name[cur - 1]) || (esc === (cur - 1))) { break }
}
return str.slice(0, cur - beg)
}
function atTerminator () {
return (cur < len &&
(name[cur] === ',' ||
name[cur] === ';' ||
name[cur] === '+'))
}
const rdns = []
// Short-circuit for empty DNs
if (len === 0) { return new DN(rdns) }
rdns.push(parseRdn())
while (cur < len) {
if (name[cur] === ',' || name[cur] === ';') {
++cur
rdns.push(parseRdn())
} else {
throw invalidDN(name)
}
}
return new DN(rdns)
}
function DN (rdns) {
assert.optionalArrayOfObject(rdns, '[object] required')
this.rdns = rdns ? rdns.slice() : []
this._format = {}
}
Object.defineProperties(DN.prototype, {
length: {
get: function getLength () { return this.rdns.length },
configurable: false
}
})
/**
* Convert DN to string according to specified formatting options.
*
* Parameters:
* - options: formatting parameters (optional, details below)
*
* Options are divided into two types:
* - Preservation options: Using data recorded during parsing, details of the
* original DN are preserved when converting back into a string.
* - Modification options: Alter string formatting defaults.
*
* Preservation options _always_ take precedence over modification options.
*
* Preservation Options:
* - keepOrder: Order of multi-value RDNs.
* - keepQuote: RDN values which were quoted will remain so.
* - keepSpace: Leading/trailing spaces will be output.
* - keepCase: Parsed attr name will be output instead of lowercased version.
*
* Modification Options:
* - upperName: RDN names will be uppercased instead of lowercased.
* - skipSpace: Disable trailing space after RDN separators
*/
DN.prototype.format = function dnFormat (options) {
assert.optionalObject(options, 'options must be an object')
options = options || this._format
let str = ''
this.rdns.forEach(function (rdn) {
const rdnString = rdn.format(options)
if (str.length !== 0) {
str += ','
}
if (options.keepSpace) {
str += (repeatChar(' ', rdn.spLead) +
rdnString + repeatChar(' ', rdn.spTrail))
} else if (options.skipSpace === true || str.length === 0) {
str += rdnString
} else {
str += ' ' + rdnString
}
})
return str
}
/**
* Set default string formatting options.
*/
DN.prototype.setFormat = function setFormat (options) {
assert.object(options, 'options must be an object')
this._format = options
}
DN.prototype.toString = function dnToString () {
return this.format()
}
DN.prototype.parentOf = function parentOf (dn) {
if (typeof (dn) !== 'object') { dn = parse(dn) }
if (this.rdns.length >= dn.rdns.length) { return false }
const diff = dn.rdns.length - this.rdns.length
for (let i = this.rdns.length - 1; i >= 0; i--) {
const myRDN = this.rdns[i]
const theirRDN = dn.rdns[i + diff]
if (!myRDN.equals(theirRDN)) { return false }
}
return true
}
DN.prototype.childOf = function childOf (dn) {
if (typeof (dn) !== 'object') { dn = parse(dn) }
return dn.parentOf(this)
}
DN.prototype.isEmpty = function isEmpty () {
return (this.rdns.length === 0)
}
DN.prototype.equals = function dnEquals (dn) {
if (typeof (dn) !== 'object') { dn = parse(dn) }
if (this.rdns.length !== dn.rdns.length) { return false }
for (let i = 0; i < this.rdns.length; i++) {
if (!this.rdns[i].equals(dn.rdns[i])) { return false }
}
return true
}
DN.prototype.parent = function dnParent () {
if (this.rdns.length !== 0) {
const save = this.rdns.shift()
const dn = new DN(this.rdns)
this.rdns.unshift(save)
return dn
}
return null
}
DN.prototype.clone = function dnClone () {
const dn = new DN(this.rdns)
dn._format = this._format
return dn
}
DN.prototype.reverse = function dnReverse () {
this.rdns.reverse()
return this
}
DN.prototype.pop = function dnPop () {
return this.rdns.pop()
}
DN.prototype.push = function dnPush (rdn) {
assert.object(rdn, 'rdn (RDN) required')
return this.rdns.push(rdn)
}
DN.prototype.shift = function dnShift () {
return this.rdns.shift()
}
DN.prototype.unshift = function dnUnshift (rdn) {
assert.object(rdn, 'rdn (RDN) required')
return this.rdns.unshift(rdn)
}
DN.isDN = function isDN (dn) {
if (!dn || typeof (dn) !== 'object') {
return false
}
if (dn instanceof DN) {
return true
}
if (Array.isArray(dn.rdns)) {
// Really simple duck-typing for now
return true
}
return false
}
/// --- Exports
module.exports = {
parse: parse,
DN: DN,
RDN: RDN
}

120
lib/dtrace.js Normal file
View File

@ -0,0 +1,120 @@
// Copyright 2011 Mark Cavage, Inc. All rights reserved.s
/// --- Globals
let SERVER_PROVIDER
let DTRACE_ID = 0
const MAX_INT = 4294967295
/*
* Args:
* server-*-start:
* 0 -> id
* 1 -> remoteIP
* 2 -> bindDN
* 3 -> req.dn
* 4,5 -> op specific
*
* server-*-done:
* 0 -> id
* 1 -> remoteIp
* 2 -> bindDN
* 3 -> requsetDN
* 4 -> status
* 5 -> errorMessage
*
*/
const SERVER_PROBES = {
// 4: attributes.length
'server-add-start': ['int', 'char *', 'char *', 'char *', 'int'],
'server-add-done': ['int', 'char *', 'char *', 'char *', 'int', 'char *'],
'server-bind-start': ['int', 'char *', 'char *', 'char *'],
'server-bind-done': ['int', 'char *', 'char *', 'char *', 'int', 'char *'],
// 4: attribute, 5: value
'server-compare-start': ['int', 'char *', 'char *', 'char *',
'char *', 'char *'],
'server-compare-done': ['int', 'char *', 'char *', 'char *', 'int', 'char *'],
'server-delete-start': ['int', 'char *', 'char *', 'char *'],
'server-delete-done': ['int', 'char *', 'char *', 'char *', 'int', 'char *'],
// 4: requestName, 5: requestValue
'server-exop-start': ['int', 'char *', 'char *', 'char *', 'char *',
'char *'],
'server-exop-done': ['int', 'char *', 'char *', 'char *', 'int', 'char *'],
// 4: changes.length
'server-modify-start': ['int', 'char *', 'char *', 'char *', 'int'],
'server-modify-done': ['int', 'char *', 'char *', 'char *', 'int', 'char *'],
// 4: newRdn, 5: newSuperior
'server-modifydn-start': ['int', 'char *', 'char *', 'char *', 'char *',
'char *'],
'server-modifydn-done': ['int', 'char *', 'char *', 'char *', 'int',
'char *'],
// 4: scope, 5: filter
'server-search-start': ['int', 'char *', 'char *', 'char *', 'char *',
'char *'],
'server-search-done': ['int', 'char *', 'char *', 'char *', 'int', 'char *'],
// Last two are searchEntry.DN and seachEntry.attributes.length
'server-search-entry': ['int', 'char *', 'char *', 'char *', 'char *', 'int'],
'server-unbind-start': ['int', 'char *', 'char *', 'char *'],
'server-unbind-done': ['int', 'char *', 'char *', 'char *', 'int', 'char *'],
'server-abandon-start': ['int', 'char *', 'char *', 'char *'],
'server-abandon-done': ['int', 'char *', 'char *', 'char *', 'int', 'char *'],
// remote IP
'server-connection': ['char *']
}
/// --- API
module.exports = (function () {
if (!SERVER_PROVIDER) {
try {
const dtrace = require('dtrace-provider')
SERVER_PROVIDER = dtrace.createDTraceProvider('ldapjs')
Object.keys(SERVER_PROBES).forEach(function (p) {
const args = SERVER_PROBES[p].splice(0)
args.unshift(p)
dtrace.DTraceProvider.prototype.addProbe.apply(SERVER_PROVIDER, args)
})
} catch (e) {
SERVER_PROVIDER = {
fire: function () {
},
enable: function () {
},
addProbe: function () {
const p = {
fire: function () {
}
}
return (p)
},
removeProbe: function () {
},
disable: function () {
}
}
}
SERVER_PROVIDER.enable()
SERVER_PROVIDER._nextId = function () {
if (DTRACE_ID === MAX_INT) { DTRACE_ID = 0 }
return ++DTRACE_ID
}
}
return SERVER_PROVIDER
}())

View File

@ -86,7 +86,7 @@ Object.keys(CODES).forEach(function (code) {
}) })
ERRORS[CODES[code]] = { ERRORS[CODES[code]] = {
err, err: err,
message: msg message: msg
} }
}) })

27
lib/filters/and_filter.js Normal file
View File

@ -0,0 +1,27 @@
// Copyright 2011 Mark Cavage, Inc. All rights reserved.
const assert = require('assert')
const util = require('util')
const parents = require('ldap-filter')
const Filter = require('./filter')
/// --- API
function AndFilter (options) {
parents.AndFilter.call(this, options)
}
util.inherits(AndFilter, parents.AndFilter)
Filter.mixin(AndFilter)
module.exports = AndFilter
AndFilter.prototype._toBer = function (ber) {
assert.ok(ber)
this.filters.forEach(function (f) {
ber = f.toBer(ber)
})
return ber
}

View File

@ -0,0 +1,35 @@
// Copyright 2011 Mark Cavage, Inc. All rights reserved.
const assert = require('assert')
const util = require('util')
const parents = require('ldap-filter')
const Filter = require('./filter')
/// --- API
function ApproximateFilter (options) {
parents.ApproximateFilter.call(this, options)
}
util.inherits(ApproximateFilter, parents.ApproximateFilter)
Filter.mixin(ApproximateFilter)
module.exports = ApproximateFilter
ApproximateFilter.prototype.parse = function (ber) {
assert.ok(ber)
this.attribute = ber.readString().toLowerCase()
this.value = ber.readString()
return true
}
ApproximateFilter.prototype._toBer = function (ber) {
assert.ok(ber)
ber.writeString(this.attribute)
ber.writeString(this.value)
return ber
}

View File

@ -0,0 +1,60 @@
// Copyright 2011 Mark Cavage, Inc. All rights reserved.
const assert = require('assert-plus')
const util = require('util')
const ASN1 = require('asn1').Ber
const parents = require('ldap-filter')
const Filter = require('./filter')
/// --- API
function EqualityFilter (options) {
parents.EqualityFilter.call(this, options)
}
util.inherits(EqualityFilter, parents.EqualityFilter)
Filter.mixin(EqualityFilter)
module.exports = EqualityFilter
EqualityFilter.prototype.matches = function (target, strictAttrCase) {
assert.object(target, 'target')
const tv = parents.getAttrValue(target, this.attribute, strictAttrCase)
let value = this.value
if (this.attribute.toLowerCase() === 'objectclass') {
/*
* Perform case-insensitive match for objectClass since nearly every LDAP
* implementation behaves in this manner.
*/
value = value.toLowerCase()
return parents.testValues(function (v) {
return value === v.toLowerCase()
}, tv)
} else {
return parents.testValues(function (v) {
return value === v
}, tv)
}
}
EqualityFilter.prototype.parse = function (ber) {
assert.ok(ber)
this.attribute = ber.readString().toLowerCase()
this.value = ber.readString(ASN1.OctetString, true)
if (this.attribute === 'objectclass') { this.value = this.value.toLowerCase() }
return true
}
EqualityFilter.prototype._toBer = function (ber) {
assert.ok(ber)
ber.writeString(this.attribute)
ber.writeBuffer(this.raw, ASN1.OctetString)
return ber
}

44
lib/filters/escape.js Normal file
View File

@ -0,0 +1,44 @@
// Copyright 2011 Mark Cavage, Inc. All rights reserved.
/**
* RFC 2254 Escaping of filter strings
*
* Raw Escaped
* (o=Parens (R Us)) (o=Parens \28R Us\29)
* (cn=star*) (cn=star\2A)
* (filename=C:\MyFile) (filename=C:\5cMyFile)
*
* Use substr_filter to avoid having * ecsaped.
*
* @author [Austin King](https://github.com/ozten)
*/
exports.escape = function (inp) {
if (typeof (inp) === 'string') {
let esc = ''
for (let i = 0; i < inp.length; i++) {
switch (inp[i]) {
case '*':
esc += '\\2a'
break
case '(':
esc += '\\28'
break
case ')':
esc += '\\29'
break
case '\\':
esc += '\\5c'
break
case '\0':
esc += '\\00'
break
default:
esc += inp[i]
break
}
}
return esc
} else {
return inp
}
}

59
lib/filters/ext_filter.js Normal file
View File

@ -0,0 +1,59 @@
// Copyright 2011 Mark Cavage, Inc. All rights reserved.
const assert = require('assert')
const util = require('util')
const parents = require('ldap-filter')
const Filter = require('./filter')
// THIS IS A STUB!
//
// ldapjs does not support server side extensible matching.
// This class exists only for the client to send them.
/// --- API
function ExtensibleFilter (options) {
parents.ExtensibleFilter.call(this, options)
}
util.inherits(ExtensibleFilter, parents.ExtensibleFilter)
Filter.mixin(ExtensibleFilter)
module.exports = ExtensibleFilter
ExtensibleFilter.prototype.parse = function (ber) {
const end = ber.offset + ber.length
while (ber.offset < end) {
const tag = ber.peek()
switch (tag) {
case 0x81:
this.rule = ber.readString(tag)
break
case 0x82:
this.matchType = ber.readString(tag)
break
case 0x83:
this.value = ber.readString(tag)
break
case 0x84:
this.dnAttributes = ber.readBoolean(tag)
break
default:
throw new Error('Invalid ext_match filter type: 0x' + tag.toString(16))
}
}
return true
}
ExtensibleFilter.prototype._toBer = function (ber) {
assert.ok(ber)
if (this.rule) { ber.writeString(this.rule, 0x81) }
if (this.matchType) { ber.writeString(this.matchType, 0x82) }
ber.writeString(this.value, 0x83)
if (this.dnAttributes) { ber.writeBoolean(this.dnAttributes, 0x84) }
return ber
}

60
lib/filters/filter.js Normal file
View File

@ -0,0 +1,60 @@
// Copyright 2011 Mark Cavage, Inc. All rights reserved.
// var assert = require('assert')
const Protocol = require('../protocol')
/// --- Globals
const TYPES = {
and: Protocol.FILTER_AND,
or: Protocol.FILTER_OR,
not: Protocol.FILTER_NOT,
equal: Protocol.FILTER_EQUALITY,
substring: Protocol.FILTER_SUBSTRINGS,
ge: Protocol.FILTER_GE,
le: Protocol.FILTER_LE,
present: Protocol.FILTER_PRESENT,
approx: Protocol.FILTER_APPROX,
ext: Protocol.FILTER_EXT
}
/// --- API
function isFilter (filter) {
if (!filter || typeof (filter) !== 'object') {
return false
}
// Do our best to duck-type it
if (typeof (filter.toBer) === 'function' &&
typeof (filter.matches) === 'function' &&
TYPES[filter.type] !== undefined) {
return true
}
return false
}
function isBerWriter (ber) {
return Boolean(
ber &&
typeof (ber) === 'object' &&
typeof (ber.startSequence) === 'function' &&
typeof (ber.endSequence) === 'function'
)
}
function mixin (target) {
target.prototype.toBer = function toBer (ber) {
if (isBerWriter(ber) === false) { throw new TypeError('ber (BerWriter) required') }
ber.startSequence(TYPES[this.type])
ber = this._toBer(ber)
ber.endSequence()
return ber
}
}
module.exports = {
isFilter: isFilter,
mixin: mixin
}

35
lib/filters/ge_filter.js Normal file
View File

@ -0,0 +1,35 @@
// Copyright 2011 Mark Cavage, Inc. All rights reserved.
const assert = require('assert')
const util = require('util')
const parents = require('ldap-filter')
const Filter = require('./filter')
/// --- API
function GreaterThanEqualsFilter (options) {
parents.GreaterThanEqualsFilter.call(this, options)
}
util.inherits(GreaterThanEqualsFilter, parents.GreaterThanEqualsFilter)
Filter.mixin(GreaterThanEqualsFilter)
module.exports = GreaterThanEqualsFilter
GreaterThanEqualsFilter.prototype.parse = function (ber) {
assert.ok(ber)
this.attribute = ber.readString().toLowerCase()
this.value = ber.readString()
return true
}
GreaterThanEqualsFilter.prototype._toBer = function (ber) {
assert.ok(ber)
ber.writeString(this.attribute)
ber.writeString(this.value)
return ber
}

208
lib/filters/index.js Normal file
View File

@ -0,0 +1,208 @@
// Copyright 2011 Mark Cavage, Inc. All rights reserved.
const assert = require('assert')
const asn1 = require('asn1')
const parents = require('ldap-filter')
const Protocol = require('../protocol')
const Filter = require('./filter')
const AndFilter = require('./and_filter')
const ApproximateFilter = require('./approx_filter')
const EqualityFilter = require('./equality_filter')
const ExtensibleFilter = require('./ext_filter')
const GreaterThanEqualsFilter = require('./ge_filter')
const LessThanEqualsFilter = require('./le_filter')
const NotFilter = require('./not_filter')
const OrFilter = require('./or_filter')
const PresenceFilter = require('./presence_filter')
const SubstringFilter = require('./substr_filter')
/// --- Globals
const BerReader = asn1.BerReader
/// --- Internal Parsers
/*
* A filter looks like this coming in:
* Filter ::= CHOICE {
* and [0] SET OF Filter,
* or [1] SET OF Filter,
* not [2] Filter,
* equalityMatch [3] AttributeValueAssertion,
* substrings [4] SubstringFilter,
* greaterOrEqual [5] AttributeValueAssertion,
* lessOrEqual [6] AttributeValueAssertion,
* present [7] AttributeType,
* approxMatch [8] AttributeValueAssertion,
* extensibleMatch [9] MatchingRuleAssertion --v3 only
* }
*
* SubstringFilter ::= SEQUENCE {
* type AttributeType,
* SEQUENCE OF CHOICE {
* initial [0] IA5String,
* any [1] IA5String,
* final [2] IA5String
* }
* }
*
* The extensibleMatch was added in LDAPv3:
*
* MatchingRuleAssertion ::= SEQUENCE {
* matchingRule [1] MatchingRuleID OPTIONAL,
* type [2] AttributeDescription OPTIONAL,
* matchValue [3] AssertionValue,
* dnAttributes [4] BOOLEAN DEFAULT FALSE
* }
*/
function _parse (ber) {
assert.ok(ber)
function parseSet (f) {
const end = ber.offset + ber.length
while (ber.offset < end) { f.addFilter(_parse(ber)) }
}
let f
const type = ber.readSequence()
switch (type) {
case Protocol.FILTER_AND:
f = new AndFilter()
parseSet(f)
break
case Protocol.FILTER_APPROX:
f = new ApproximateFilter()
f.parse(ber)
break
case Protocol.FILTER_EQUALITY:
f = new EqualityFilter()
f.parse(ber)
return f
case Protocol.FILTER_EXT:
f = new ExtensibleFilter()
f.parse(ber)
return f
case Protocol.FILTER_GE:
f = new GreaterThanEqualsFilter()
f.parse(ber)
return f
case Protocol.FILTER_LE:
f = new LessThanEqualsFilter()
f.parse(ber)
return f
case Protocol.FILTER_NOT:
f = new NotFilter({
filter: _parse(ber)
})
break
case Protocol.FILTER_OR:
f = new OrFilter()
parseSet(f)
break
case Protocol.FILTER_PRESENT:
f = new PresenceFilter()
f.parse(ber)
break
case Protocol.FILTER_SUBSTRINGS:
f = new SubstringFilter()
f.parse(ber)
break
default:
throw new Error('Invalid search filter type: 0x' + type.toString(16))
}
assert.ok(f)
return f
}
function cloneFilter (input) {
let child
if (input.type === 'and' || input.type === 'or') {
child = input.filters.map(cloneFilter)
} else if (input.type === 'not') {
child = cloneFilter(input.filter)
}
switch (input.type) {
case 'and':
return new AndFilter({ filters: child })
case 'or':
return new OrFilter({ filters: child })
case 'not':
return new NotFilter({ filter: child })
case 'equal':
return new EqualityFilter(input)
case 'substring':
return new SubstringFilter(input)
case 'ge':
return new GreaterThanEqualsFilter(input)
case 'le':
return new LessThanEqualsFilter(input)
case 'present':
return new PresenceFilter(input)
case 'approx':
return new ApproximateFilter(input)
case 'ext':
return new ExtensibleFilter(input)
default:
throw new Error('invalid filter type:' + input.type)
}
}
function escapedToHex (str) {
return str.replace(/\\([0-9a-f](?![0-9a-f])|[^0-9a-f]|$)/gi, function (match, p1) {
if (!p1) {
return '\\5c'
}
const hexCode = p1.charCodeAt(0).toString(16)
return '\\' + hexCode
})
}
function parseString (str) {
const hexStr = escapedToHex(str)
const generic = parents.parse(hexStr)
// The filter object(s) return from ldap-filter.parse lack the toBer/parse
// decoration that native ldapjs filter possess. cloneFilter adds that back.
return cloneFilter(generic)
}
/// --- API
module.exports = {
parse: function (ber) {
if (!ber || !(ber instanceof BerReader)) { throw new TypeError('ber (BerReader) required') }
return _parse(ber)
},
parseString: parseString,
isFilter: Filter.isFilter,
AndFilter: AndFilter,
ApproximateFilter: ApproximateFilter,
EqualityFilter: EqualityFilter,
ExtensibleFilter: ExtensibleFilter,
GreaterThanEqualsFilter: GreaterThanEqualsFilter,
LessThanEqualsFilter: LessThanEqualsFilter,
NotFilter: NotFilter,
OrFilter: OrFilter,
PresenceFilter: PresenceFilter,
SubstringFilter: SubstringFilter
}

35
lib/filters/le_filter.js Normal file
View File

@ -0,0 +1,35 @@
// Copyright 2011 Mark Cavage, Inc. All rights reserved.
const assert = require('assert')
const util = require('util')
const parents = require('ldap-filter')
const Filter = require('./filter')
/// --- API
function LessThanEqualsFilter (options) {
parents.LessThanEqualsFilter.call(this, options)
}
util.inherits(LessThanEqualsFilter, parents.LessThanEqualsFilter)
Filter.mixin(LessThanEqualsFilter)
module.exports = LessThanEqualsFilter
LessThanEqualsFilter.prototype.parse = function (ber) {
assert.ok(ber)
this.attribute = ber.readString().toLowerCase()
this.value = ber.readString()
return true
}
LessThanEqualsFilter.prototype._toBer = function (ber) {
assert.ok(ber)
ber.writeString(this.attribute)
ber.writeString(this.value)
return ber
}

23
lib/filters/not_filter.js Normal file
View File

@ -0,0 +1,23 @@
// Copyright 2011 Mark Cavage, Inc. All rights reserved.
const assert = require('assert')
const util = require('util')
const parents = require('ldap-filter')
const Filter = require('./filter')
/// --- API
function NotFilter (options) {
parents.NotFilter.call(this, options)
}
util.inherits(NotFilter, parents.NotFilter)
Filter.mixin(NotFilter)
module.exports = NotFilter
NotFilter.prototype._toBer = function (ber) {
assert.ok(ber)
return this.filter.toBer(ber)
}

27
lib/filters/or_filter.js Normal file
View File

@ -0,0 +1,27 @@
// Copyright 2011 Mark Cavage, Inc. All rights reserved.
const assert = require('assert')
const util = require('util')
const parents = require('ldap-filter')
const Filter = require('./filter')
/// --- API
function OrFilter (options) {
parents.OrFilter.call(this, options)
}
util.inherits(OrFilter, parents.OrFilter)
Filter.mixin(OrFilter)
module.exports = OrFilter
OrFilter.prototype._toBer = function (ber) {
assert.ok(ber)
this.filters.forEach(function (f) {
ber = f.toBer(ber)
})
return ber
}

View File

@ -0,0 +1,36 @@
// Copyright 2011 Mark Cavage, Inc. All rights reserved.
const assert = require('assert')
const util = require('util')
const parents = require('ldap-filter')
const Filter = require('./filter')
/// --- API
function PresenceFilter (options) {
parents.PresenceFilter.call(this, options)
}
util.inherits(PresenceFilter, parents.PresenceFilter)
Filter.mixin(PresenceFilter)
module.exports = PresenceFilter
PresenceFilter.prototype.parse = function (ber) {
assert.ok(ber)
this.attribute =
ber.buffer.slice(0, ber.length).toString('utf8').toLowerCase()
ber._offset += ber.length
return true
}
PresenceFilter.prototype._toBer = function (ber) {
assert.ok(ber)
for (let i = 0; i < this.attribute.length; i++) { ber.writeByte(this.attribute.charCodeAt(i)) }
return ber
}

View File

@ -0,0 +1,70 @@
// Copyright 2011 Mark Cavage, Inc. All rights reserved.
const assert = require('assert')
const util = require('util')
const parents = require('ldap-filter')
const Filter = require('./filter')
/// --- API
function SubstringFilter (options) {
parents.SubstringFilter.call(this, options)
}
util.inherits(SubstringFilter, parents.SubstringFilter)
Filter.mixin(SubstringFilter)
module.exports = SubstringFilter
SubstringFilter.prototype.parse = function (ber) {
assert.ok(ber)
this.attribute = ber.readString().toLowerCase()
ber.readSequence()
const end = ber.offset + ber.length
while (ber.offset < end) {
const tag = ber.peek()
switch (tag) {
case 0x80: // Initial
this.initial = ber.readString(tag)
if (this.attribute === 'objectclass') { this.initial = this.initial.toLowerCase() }
break
case 0x81: { // Any
let anyVal = ber.readString(tag)
if (this.attribute === 'objectclass') { anyVal = anyVal.toLowerCase() }
this.any.push(anyVal)
break
}
case 0x82: // Final
this.final = ber.readString(tag)
if (this.attribute === 'objectclass') { this.final = this.final.toLowerCase() }
break
default:
throw new Error('Invalid substrings filter type: 0x' + tag.toString(16))
}
}
return true
}
SubstringFilter.prototype._toBer = function (ber) {
assert.ok(ber)
ber.writeString(this.attribute)
ber.startSequence()
if (this.initial) { ber.writeString(this.initial, 0x80) }
if (this.any && this.any.length) {
this.any.forEach(function (s) {
ber.writeString(s, 0x81)
})
}
if (this.final) { ber.writeString(this.final, 0x82) }
ber.endSequence()
return ber
}

View File

@ -3,16 +3,16 @@
const logger = require('./logger') const logger = require('./logger')
const client = require('./client') const client = require('./client')
const Attribute = require('@ldapjs/attribute') const Attribute = require('./attribute')
const Change = require('@ldapjs/change') const Change = require('./change')
const Protocol = require('@ldapjs/protocol') const Protocol = require('./protocol')
const Server = require('./server') const Server = require('./server')
const controls = require('./controls') const controls = require('./controls')
const persistentSearch = require('./persistent_search') const persistentSearch = require('./persistent_search')
const dn = require('@ldapjs/dn') const dn = require('./dn')
const errors = require('./errors') const errors = require('./errors')
const filters = require('@ldapjs/filter') const filters = require('./filters')
const messages = require('./messages') const messages = require('./messages')
const url = require('./url') const url = require('./url')
@ -24,7 +24,7 @@ module.exports = {
Client: client.Client, Client: client.Client,
createClient: client.createClient, createClient: client.createClient,
Server, Server: Server,
createServer: function (options) { createServer: function (options) {
if (options === undefined) { options = {} } if (options === undefined) { options = {} }
@ -37,21 +37,21 @@ module.exports = {
return new Server(options) return new Server(options)
}, },
Attribute, Attribute: Attribute,
Change, Change: Change,
dn, dn: dn,
DN: dn.DN, DN: dn.DN,
RDN: dn.RDN, RDN: dn.RDN,
parseDN: dn.DN.fromString, parseDN: dn.parse,
persistentSearch, persistentSearch: persistentSearch,
PersistentSearchCache: persistentSearch.PersistentSearchCache, PersistentSearchCache: persistentSearch.PersistentSearchCache,
filters, filters: filters,
parseFilter: filters.parseString, parseFilter: filters.parseString,
url, url: url,
parseURL: url.parse parseURL: url.parse
} }

View File

@ -0,0 +1,87 @@
// Copyright 2011 Mark Cavage, Inc. All rights reserved.
const assert = require('assert-plus')
const util = require('util')
const LDAPMessage = require('./message')
const Protocol = require('../protocol')
/// --- API
function AbandonRequest (options) {
options = options || {}
assert.object(options)
assert.optionalNumber(options.abandonID)
options.protocolOp = Protocol.LDAP_REQ_ABANDON
LDAPMessage.call(this, options)
this.abandonID = options.abandonID || 0
}
util.inherits(AbandonRequest, LDAPMessage)
Object.defineProperties(AbandonRequest.prototype, {
type: {
get: function getType () { return 'AbandonRequest' },
configurable: false
}
})
AbandonRequest.prototype._parse = function (ber, length) {
assert.ok(ber)
assert.ok(length)
// What a PITA - have to replicate ASN.1 integer logic to work around the
// way abandon is encoded and the way ldapjs framework handles "normal"
// messages
const buf = ber.buffer
let offset = 0
let value = 0
const fb = buf[offset++]
value = fb & 0x7F
for (let i = 1; i < length; i++) {
value <<= 8
value |= (buf[offset++] & 0xff)
}
if ((fb & 0x80) === 0x80) { value = -value }
ber._offset += length
this.abandonID = value
return true
}
AbandonRequest.prototype._toBer = function (ber) {
assert.ok(ber)
let i = this.abandonID
let sz = 4
while ((((i & 0xff800000) === 0) || ((i & 0xff800000) === 0xff800000)) &&
(sz > 1)) {
sz--
i <<= 8
}
assert.ok(sz <= 4)
while (sz-- > 0) {
ber.writeByte((i & 0xff000000) >> 24)
i <<= 8
}
return ber
}
AbandonRequest.prototype._json = function (j) {
assert.ok(j)
j.abandonID = this.abandonID
return j
}
/// --- Exports
module.exports = AbandonRequest

View File

@ -0,0 +1,34 @@
// Copyright 2011 Mark Cavage, Inc. All rights reserved.
const assert = require('assert-plus')
const util = require('util')
const LDAPMessage = require('./result')
// var Protocol = require('../protocol')
/// --- API
function AbandonResponse (options) {
options = options || {}
assert.object(options)
options.protocolOp = 0
LDAPMessage.call(this, options)
}
util.inherits(AbandonResponse, LDAPMessage)
Object.defineProperties(AbandonResponse.prototype, {
type: {
get: function getType () { return 'AbandonResponse' },
configurable: false
}
})
AbandonResponse.prototype.end = function (_status) {}
AbandonResponse.prototype._json = function (j) {
return j
}
/// --- Exports
module.exports = AbandonResponse

159
lib/messages/add_request.js Normal file
View File

@ -0,0 +1,159 @@
// Copyright 2011 Mark Cavage, Inc. All rights reserved.
const assert = require('assert-plus')
const util = require('util')
const LDAPMessage = require('./message')
const Attribute = require('../attribute')
const Protocol = require('../protocol')
const lassert = require('../assert')
/// --- API
function AddRequest (options) {
options = options || {}
assert.object(options)
lassert.optionalStringDN(options.entry)
lassert.optionalArrayOfAttribute(options.attributes)
options.protocolOp = Protocol.LDAP_REQ_ADD
LDAPMessage.call(this, options)
this.entry = options.entry || null
this.attributes = options.attributes ? options.attributes.slice(0) : []
}
util.inherits(AddRequest, LDAPMessage)
Object.defineProperties(AddRequest.prototype, {
type: {
get: function getType () { return 'AddRequest' },
configurable: false
},
_dn: {
get: function getDN () { return this.entry },
configurable: false
}
})
AddRequest.prototype._parse = function (ber) {
assert.ok(ber)
this.entry = ber.readString()
ber.readSequence()
const end = ber.offset + ber.length
while (ber.offset < end) {
const a = new Attribute()
a.parse(ber)
a.type = a.type.toLowerCase()
if (a.type === 'objectclass') {
for (let i = 0; i < a.vals.length; i++) { a.vals[i] = a.vals[i].toLowerCase() }
}
this.attributes.push(a)
}
this.attributes.sort(Attribute.compare)
return true
}
AddRequest.prototype._toBer = function (ber) {
assert.ok(ber)
ber.writeString(this.entry.toString())
ber.startSequence()
this.attributes.forEach(function (a) {
a.toBer(ber)
})
ber.endSequence()
return ber
}
AddRequest.prototype._json = function (j) {
assert.ok(j)
j.entry = this.entry.toString()
j.attributes = []
this.attributes.forEach(function (a) {
j.attributes.push(a.json)
})
return j
}
AddRequest.prototype.indexOf = function (attr) {
if (!attr || typeof (attr) !== 'string') { throw new TypeError('attr (string) required') }
for (let i = 0; i < this.attributes.length; i++) {
if (this.attributes[i].type === attr) { return i }
}
return -1
}
AddRequest.prototype.attributeNames = function () {
const attrs = []
for (let i = 0; i < this.attributes.length; i++) { attrs.push(this.attributes[i].type.toLowerCase()) }
return attrs
}
AddRequest.prototype.getAttribute = function (name) {
if (!name || typeof (name) !== 'string') { throw new TypeError('attribute name (string) required') }
name = name.toLowerCase()
for (let i = 0; i < this.attributes.length; i++) {
if (this.attributes[i].type === name) { return this.attributes[i] }
}
return null
}
AddRequest.prototype.addAttribute = function (attr) {
if (!(attr instanceof Attribute)) { throw new TypeError('attribute (Attribute) required') }
return this.attributes.push(attr)
}
/**
* Returns a "pure" JS representation of this object.
*
* An example object would look like:
*
* {
* "dn": "cn=unit, dc=test",
* "attributes": {
* "cn": ["unit", "foo"],
* "objectclass": ["top", "person"]
* }
* }
*
* @return {Object} that looks like the above.
*/
AddRequest.prototype.toObject = function () {
const self = this
const obj = {
dn: self.entry ? self.entry.toString() : '',
attributes: {}
}
if (!this.attributes || !this.attributes.length) { return obj }
this.attributes.forEach(function (a) {
if (!obj.attributes[a.type]) { obj.attributes[a.type] = [] }
a.vals.forEach(function (v) {
if (obj.attributes[a.type].indexOf(v) === -1) { obj.attributes[a.type].push(v) }
})
})
return obj
}
/// --- Exports
module.exports = AddRequest

View File

@ -0,0 +1,22 @@
// Copyright 2011 Mark Cavage, Inc. All rights reserved.
const assert = require('assert-plus')
const util = require('util')
const LDAPResult = require('./result')
const Protocol = require('../protocol')
/// --- API
function AddResponse (options) {
options = options || {}
assert.object(options)
options.protocolOp = Protocol.LDAP_REP_ADD
LDAPResult.call(this, options)
}
util.inherits(AddResponse, LDAPResult)
/// --- Exports
module.exports = AddResponse

View File

@ -0,0 +1,84 @@
// Copyright 2011 Mark Cavage, Inc. All rights reserved.
const assert = require('assert-plus')
const util = require('util')
const asn1 = require('asn1')
const LDAPMessage = require('./message')
const Protocol = require('../protocol')
/// --- Globals
const Ber = asn1.Ber
const LDAP_BIND_SIMPLE = 'simple'
// var LDAP_BIND_SASL = 'sasl'
/// --- API
function BindRequest (options) {
options = options || {}
assert.object(options)
options.protocolOp = Protocol.LDAP_REQ_BIND
LDAPMessage.call(this, options)
this.version = options.version || 0x03
this.name = options.name || null
this.authentication = options.authentication || LDAP_BIND_SIMPLE
this.credentials = options.credentials || ''
}
util.inherits(BindRequest, LDAPMessage)
Object.defineProperties(BindRequest.prototype, {
type: {
get: function getType () { return 'BindRequest' },
configurable: false
},
_dn: {
get: function getDN () { return this.name },
configurable: false
}
})
BindRequest.prototype._parse = function (ber) {
assert.ok(ber)
this.version = ber.readInt()
this.name = ber.readString()
const t = ber.peek()
// TODO add support for SASL et al
if (t !== Ber.Context) { throw new Error('authentication 0x' + t.toString(16) + ' not supported') }
this.authentication = LDAP_BIND_SIMPLE
this.credentials = ber.readString(Ber.Context)
return true
}
BindRequest.prototype._toBer = function (ber) {
assert.ok(ber)
ber.writeInt(this.version)
ber.writeString((this.name || '').toString())
// TODO add support for SASL et al
ber.writeString((this.credentials || ''), Ber.Context)
return ber
}
BindRequest.prototype._json = function (j) {
assert.ok(j)
j.version = this.version
j.name = this.name
j.authenticationType = this.authentication
j.credentials = this.credentials
return j
}
/// --- Exports
module.exports = BindRequest

View File

@ -0,0 +1,22 @@
// Copyright 2011 Mark Cavage, Inc. All rights reserved.
const assert = require('assert-plus')
const util = require('util')
const LDAPResult = require('./result')
const Protocol = require('../protocol')
/// --- API
function BindResponse (options) {
options = options || {}
assert.object(options)
options.protocolOp = Protocol.LDAP_REP_BIND
LDAPResult.call(this, options)
}
util.inherits(BindResponse, LDAPResult)
/// --- Exports
module.exports = BindResponse

View File

@ -0,0 +1,74 @@
// Copyright 2011 Mark Cavage, Inc. All rights reserved.
const assert = require('assert-plus')
const util = require('util')
const LDAPMessage = require('./message')
const Protocol = require('../protocol')
const lassert = require('../assert')
/// --- API
function CompareRequest (options) {
options = options || {}
assert.object(options)
assert.optionalString(options.attribute)
assert.optionalString(options.value)
lassert.optionalStringDN(options.entry)
options.protocolOp = Protocol.LDAP_REQ_COMPARE
LDAPMessage.call(this, options)
this.entry = options.entry || null
this.attribute = options.attribute || ''
this.value = options.value || ''
}
util.inherits(CompareRequest, LDAPMessage)
Object.defineProperties(CompareRequest.prototype, {
type: {
get: function getType () { return 'CompareRequest' },
configurable: false
},
_dn: {
get: function getDN () { return this.entry },
configurable: false
}
})
CompareRequest.prototype._parse = function (ber) {
assert.ok(ber)
this.entry = ber.readString()
ber.readSequence()
this.attribute = ber.readString().toLowerCase()
this.value = ber.readString()
return true
}
CompareRequest.prototype._toBer = function (ber) {
assert.ok(ber)
ber.writeString(this.entry.toString())
ber.startSequence()
ber.writeString(this.attribute)
ber.writeString(this.value)
ber.endSequence()
return ber
}
CompareRequest.prototype._json = function (j) {
assert.ok(j)
j.entry = this.entry.toString()
j.attribute = this.attribute
j.value = this.value
return j
}
/// --- Exports
module.exports = CompareRequest

View File

@ -0,0 +1,33 @@
// Copyright 2011 Mark Cavage, Inc. All rights reserved.
const assert = require('assert-plus')
const util = require('util')
const LDAPResult = require('./result')
const Protocol = require('../protocol')
/// --- API
function CompareResponse (options) {
options = options || {}
assert.object(options)
options.protocolOp = Protocol.LDAP_REP_COMPARE
LDAPResult.call(this, options)
}
util.inherits(CompareResponse, LDAPResult)
CompareResponse.prototype.end = function (matches) {
let status = 0x06
if (typeof (matches) === 'boolean') {
if (!matches) { status = 0x05 } // Compare false
} else {
status = matches
}
return LDAPResult.prototype.end.call(this, status)
}
/// --- Exports
module.exports = CompareResponse

View File

@ -0,0 +1,62 @@
// Copyright 2011 Mark Cavage, Inc. All rights reserved.
const assert = require('assert-plus')
const util = require('util')
const LDAPMessage = require('./message')
const Protocol = require('../protocol')
const lassert = require('../assert')
/// --- API
function DeleteRequest (options) {
options = options || {}
assert.object(options)
lassert.optionalStringDN(options.entry)
options.protocolOp = Protocol.LDAP_REQ_DELETE
LDAPMessage.call(this, options)
this.entry = options.entry || null
}
util.inherits(DeleteRequest, LDAPMessage)
Object.defineProperties(DeleteRequest.prototype, {
type: {
get: function getType () { return 'DeleteRequest' },
configurable: false
},
_dn: {
get: function getDN () { return this.entry },
configurable: false
}
})
DeleteRequest.prototype._parse = function (ber, length) {
assert.ok(ber)
this.entry = ber.buffer.slice(0, length).toString('utf8')
ber._offset += ber.length
return true
}
DeleteRequest.prototype._toBer = function (ber) {
assert.ok(ber)
const buf = Buffer.from(this.entry.toString())
for (let i = 0; i < buf.length; i++) { ber.writeByte(buf[i]) }
return ber
}
DeleteRequest.prototype._json = function (j) {
assert.ok(j)
j.entry = this.entry
return j
}
/// --- Exports
module.exports = DeleteRequest

View File

@ -0,0 +1,22 @@
// Copyright 2011 Mark Cavage, Inc. All rights reserved.
const assert = require('assert-plus')
const util = require('util')
const LDAPResult = require('./result')
const Protocol = require('../protocol')
/// --- API
function DeleteResponse (options) {
options = options || {}
assert.object(options)
options.protocolOp = Protocol.LDAP_REP_DELETE
LDAPResult.call(this, options)
}
util.inherits(DeleteResponse, LDAPResult)
/// --- Exports
module.exports = DeleteResponse

117
lib/messages/ext_request.js Normal file
View File

@ -0,0 +1,117 @@
// Copyright 2011 Mark Cavage, Inc. All rights reserved.
const assert = require('assert-plus')
const util = require('util')
const LDAPMessage = require('./message')
const Protocol = require('../protocol')
/// --- API
function ExtendedRequest (options) {
options = options || {}
assert.object(options)
assert.optionalString(options.requestName)
if (options.requestValue &&
!(Buffer.isBuffer(options.requestValue) ||
typeof (options.requestValue) === 'string')) {
throw new TypeError('options.requestValue must be a buffer or a string')
}
options.protocolOp = Protocol.LDAP_REQ_EXTENSION
LDAPMessage.call(this, options)
this.requestName = options.requestName || ''
this.requestValue = options.requestValue
if (Buffer.isBuffer(this.requestValue)) {
this.requestValueBuffer = this.requestValue
} else {
this.requestValueBuffer = Buffer.from(this.requestValue || '', 'utf8')
}
}
util.inherits(ExtendedRequest, LDAPMessage)
Object.defineProperties(ExtendedRequest.prototype, {
type: {
get: function getType () { return 'ExtendedRequest' },
configurable: false
},
_dn: {
get: function getDN () { return this.requestName },
configurable: false
},
name: {
get: function getName () { return this.requestName },
set: function setName (val) {
assert.string(val)
this.requestName = val
},
configurable: false
},
value: {
get: function getValue () { return this.requestValue },
set: function setValue (val) {
if (!(Buffer.isBuffer(val) || typeof (val) === 'string')) { throw new TypeError('value must be a buffer or a string') }
if (Buffer.isBuffer(val)) {
this.requestValueBuffer = val
} else {
this.requestValueBuffer = Buffer.from(val, 'utf8')
}
this.requestValue = val
},
configurable: false
},
valueBuffer: {
get: function getValueBuffer () {
return this.requestValueBuffer
},
set: function setValueBuffer (val) {
if (!Buffer.isBuffer(val)) { throw new TypeError('valueBuffer must be a buffer') }
this.value = val
},
configurable: false
}
})
ExtendedRequest.prototype._parse = function (ber) {
assert.ok(ber)
this.requestName = ber.readString(0x80)
if (ber.peek() === 0x81) {
this.requestValueBuffer = ber.readString(0x81, true)
this.requestValue = this.requestValueBuffer.toString('utf8')
}
return true
}
ExtendedRequest.prototype._toBer = function (ber) {
assert.ok(ber)
ber.writeString(this.requestName, 0x80)
if (Buffer.isBuffer(this.requestValue)) {
ber.writeBuffer(this.requestValue, 0x81)
} else if (typeof (this.requestValue) === 'string') {
ber.writeString(this.requestValue, 0x81)
}
return ber
}
ExtendedRequest.prototype._json = function (j) {
assert.ok(j)
j.requestName = this.requestName
j.requestValue = (Buffer.isBuffer(this.requestValue))
? this.requestValue.toString('hex')
: this.requestValue
return j
}
/// --- Exports
module.exports = ExtendedRequest

View File

@ -0,0 +1,86 @@
// Copyright 2011 Mark Cavage, Inc. All rights reserved.
const assert = require('assert-plus')
const util = require('util')
const LDAPResult = require('./result')
const Protocol = require('../protocol')
/// --- API
function ExtendedResponse (options) {
options = options || {}
assert.object(options)
assert.optionalString(options.responseName)
assert.optionalString(options.responsevalue)
this.responseName = options.responseName || undefined
this.responseValue = options.responseValue || undefined
options.protocolOp = Protocol.LDAP_REP_EXTENSION
LDAPResult.call(this, options)
}
util.inherits(ExtendedResponse, LDAPResult)
Object.defineProperties(ExtendedResponse.prototype, {
type: {
get: function getType () { return 'ExtendedResponse' },
configurable: false
},
_dn: {
get: function getDN () { return this.responseName },
configurable: false
},
name: {
get: function getName () { return this.responseName },
set: function setName (val) {
assert.string(val)
this.responseName = val
},
configurable: false
},
value: {
get: function getValue () { return this.responseValue },
set: function (val) {
assert.string(val)
this.responseValue = val
},
configurable: false
}
})
ExtendedResponse.prototype._parse = function (ber) {
assert.ok(ber)
if (!LDAPResult.prototype._parse.call(this, ber)) { return false }
if (ber.peek() === 0x8a) { this.responseName = ber.readString(0x8a) }
if (ber.peek() === 0x8b) { this.responseValue = ber.readString(0x8b) }
return true
}
ExtendedResponse.prototype._toBer = function (ber) {
assert.ok(ber)
if (!LDAPResult.prototype._toBer.call(this, ber)) { return false }
if (this.responseName) { ber.writeString(this.responseName, 0x8a) }
if (this.responseValue) { ber.writeString(this.responseValue, 0x8b) }
return ber
}
ExtendedResponse.prototype._json = function (j) {
assert.ok(j)
j = LDAPResult.prototype._json.call(this, j)
j.responseName = this.responseName
j.responseValue = this.responseValue
return j
}
/// --- Exports
module.exports = ExtendedResponse

View File

@ -1,39 +1,61 @@
// Copyright 2011 Mark Cavage, Inc. All rights reserved. // Copyright 2011 Mark Cavage, Inc. All rights reserved.
const messages = require('@ldapjs/messages') const LDAPMessage = require('./message')
const LDAPResult = require('./result')
const Parser = require('./parser') const Parser = require('./parser')
const AbandonRequest = require('./abandon_request')
const AbandonResponse = require('./abandon_response')
const AddRequest = require('./add_request')
const AddResponse = require('./add_response')
const BindRequest = require('./bind_request')
const BindResponse = require('./bind_response')
const CompareRequest = require('./compare_request')
const CompareResponse = require('./compare_response')
const DeleteRequest = require('./del_request')
const DeleteResponse = require('./del_response')
const ExtendedRequest = require('./ext_request')
const ExtendedResponse = require('./ext_response')
const ModifyRequest = require('./modify_request')
const ModifyResponse = require('./modify_response')
const ModifyDNRequest = require('./moddn_request')
const ModifyDNResponse = require('./moddn_response')
const SearchRequest = require('./search_request')
const SearchEntry = require('./search_entry')
const SearchReference = require('./search_reference')
const SearchResponse = require('./search_response') const SearchResponse = require('./search_response')
const UnbindRequest = require('./unbind_request')
const UnbindResponse = require('./unbind_response')
/// --- API /// --- API
module.exports = { module.exports = {
LDAPMessage: messages.LdapMessage, LDAPMessage: LDAPMessage,
LDAPResult: messages.LdapResult, LDAPResult: LDAPResult,
Parser, Parser: Parser,
AbandonRequest: messages.AbandonRequest, AbandonRequest: AbandonRequest,
AbandonResponse: messages.AbandonResponse, AbandonResponse: AbandonResponse,
AddRequest: messages.AddRequest, AddRequest: AddRequest,
AddResponse: messages.AddResponse, AddResponse: AddResponse,
BindRequest: messages.BindRequest, BindRequest: BindRequest,
BindResponse: messages.BindResponse, BindResponse: BindResponse,
CompareRequest: messages.CompareRequest, CompareRequest: CompareRequest,
CompareResponse: messages.CompareResponse, CompareResponse: CompareResponse,
DeleteRequest: messages.DeleteRequest, DeleteRequest: DeleteRequest,
DeleteResponse: messages.DeleteResponse, DeleteResponse: DeleteResponse,
ExtendedRequest: messages.ExtensionRequest, ExtendedRequest: ExtendedRequest,
ExtendedResponse: messages.ExtensionResponse, ExtendedResponse: ExtendedResponse,
ModifyRequest: messages.ModifyRequest, ModifyRequest: ModifyRequest,
ModifyResponse: messages.ModifyResponse, ModifyResponse: ModifyResponse,
ModifyDNRequest: messages.ModifyDnRequest, ModifyDNRequest: ModifyDNRequest,
ModifyDNResponse: messages.ModifyDnResponse, ModifyDNResponse: ModifyDNResponse,
SearchRequest: messages.SearchRequest, SearchRequest: SearchRequest,
SearchEntry: messages.SearchResultEntry, SearchEntry: SearchEntry,
SearchReference: messages.SearchResultReference, SearchReference: SearchReference,
SearchResponse, SearchResponse: SearchResponse,
UnbindRequest: messages.UnbindRequest UnbindRequest: UnbindRequest,
UnbindResponse: UnbindResponse
} }

110
lib/messages/message.js Normal file
View File

@ -0,0 +1,110 @@
// Copyright 2011 Mark Cavage, Inc. All rights reserved.
const assert = require('assert-plus')
const util = require('util')
const asn1 = require('asn1')
const logger = require('../logger')
// var Control = require('../controls').Control
// var Protocol = require('../protocol')
/// --- Globals
// var Ber = asn1.Ber
// var BerReader = asn1.BerReader
const BerWriter = asn1.BerWriter
const getControl = require('../controls').getControl
/// --- API
/**
* LDAPMessage structure.
*
* @param {Object} options stuff.
*/
function LDAPMessage (options) {
assert.object(options)
this.messageID = options.messageID || 0
this.protocolOp = options.protocolOp || undefined
this.controls = options.controls ? options.controls.slice(0) : []
this.log = options.log || logger
}
Object.defineProperties(LDAPMessage.prototype, {
id: {
get: function getId () { return this.messageID },
configurable: false
},
dn: {
get: function getDN () { return this._dn || '' },
configurable: false
},
type: {
get: function getType () { return 'LDAPMessage' },
configurable: false
},
json: {
get: function () {
const out = this._json({
messageID: this.messageID,
protocolOp: this.type
})
out.controls = this.controls
return out
},
configurable: false
}
})
LDAPMessage.prototype.toString = function () {
return JSON.stringify(this.json)
}
LDAPMessage.prototype.parse = function (ber) {
assert.ok(ber)
this.log.trace('parse: data=%s', util.inspect(ber.buffer))
// Delegate off to the specific type to parse
this._parse(ber, ber.length)
// Look for controls
if (ber.peek() === 0xa0) {
ber.readSequence()
const end = ber.offset + ber.length
while (ber.offset < end) {
const c = getControl(ber)
if (c) { this.controls.push(c) }
}
}
this.log.trace('Parsing done: %j', this.json)
return true
}
LDAPMessage.prototype.toBer = function () {
let writer = new BerWriter()
writer.startSequence()
writer.writeInt(this.messageID)
writer.startSequence(this.protocolOp)
if (this._toBer) { writer = this._toBer(writer) }
writer.endSequence()
if (this.controls && this.controls.length) {
writer.startSequence(0xa0)
this.controls.forEach(function (c) {
c.toBer(writer)
})
writer.endSequence()
}
writer.endSequence()
return writer.buffer
}
/// --- Exports
module.exports = LDAPMessage

View File

@ -0,0 +1,85 @@
// Copyright 2011 Mark Cavage, Inc. All rights reserved.
const assert = require('assert-plus')
const util = require('util')
const LDAPMessage = require('./message')
const Protocol = require('../protocol')
const dn = require('../dn')
const lassert = require('../assert')
/// --- API
function ModifyDNRequest (options) {
options = options || {}
assert.object(options)
assert.optionalBool(options.deleteOldRdn)
lassert.optionalStringDN(options.entry)
lassert.optionalDN(options.newRdn)
lassert.optionalDN(options.newSuperior)
options.protocolOp = Protocol.LDAP_REQ_MODRDN
LDAPMessage.call(this, options)
this.entry = options.entry || null
this.newRdn = options.newRdn || null
this.deleteOldRdn = options.deleteOldRdn || true
this.newSuperior = options.newSuperior || null
}
util.inherits(ModifyDNRequest, LDAPMessage)
Object.defineProperties(ModifyDNRequest.prototype, {
type: {
get: function getType () { return 'ModifyDNRequest' },
configurable: false
},
_dn: {
get: function getDN () { return this.entry },
configurable: false
}
})
ModifyDNRequest.prototype._parse = function (ber) {
assert.ok(ber)
this.entry = ber.readString()
this.newRdn = dn.parse(ber.readString())
this.deleteOldRdn = ber.readBoolean()
if (ber.peek() === 0x80) { this.newSuperior = dn.parse(ber.readString(0x80)) }
return true
}
ModifyDNRequest.prototype._toBer = function (ber) {
// assert.ok(ber);
ber.writeString(this.entry.toString())
ber.writeString(this.newRdn.toString())
ber.writeBoolean(this.deleteOldRdn)
if (this.newSuperior) {
const s = this.newSuperior.toString()
const len = Buffer.byteLength(s)
ber.writeByte(0x80) // MODIFY_DN_REQUEST_NEW_SUPERIOR_TAG
ber.writeLength(len)
ber._ensure(len)
ber._buf.write(s, ber._offset)
ber._offset += len
}
return ber
}
ModifyDNRequest.prototype._json = function (j) {
assert.ok(j)
j.entry = this.entry.toString()
j.newRdn = this.newRdn.toString()
j.deleteOldRdn = this.deleteOldRdn
j.newSuperior = this.newSuperior ? this.newSuperior.toString() : ''
return j
}
/// --- Exports
module.exports = ModifyDNRequest

View File

@ -0,0 +1,22 @@
// Copyright 2011 Mark Cavage, Inc. All rights reserved.
const assert = require('assert-plus')
const util = require('util')
const LDAPResult = require('./result')
const Protocol = require('../protocol')
/// --- API
function ModifyDNResponse (options) {
options = options || {}
assert.object(options)
options.protocolOp = Protocol.LDAP_REP_MODRDN
LDAPResult.call(this, options)
}
util.inherits(ModifyDNResponse, LDAPResult)
/// --- Exports
module.exports = ModifyDNResponse

View File

@ -0,0 +1,83 @@
// Copyright 2011 Mark Cavage, Inc. All rights reserved.
const assert = require('assert-plus')
const util = require('util')
const LDAPMessage = require('./message')
const Change = require('../change')
const Protocol = require('../protocol')
const lassert = require('../assert')
/// --- API
function ModifyRequest (options) {
options = options || {}
assert.object(options)
lassert.optionalStringDN(options.object)
lassert.optionalArrayOfAttribute(options.attributes)
options.protocolOp = Protocol.LDAP_REQ_MODIFY
LDAPMessage.call(this, options)
this.object = options.object || null
this.changes = options.changes ? options.changes.slice(0) : []
}
util.inherits(ModifyRequest, LDAPMessage)
Object.defineProperties(ModifyRequest.prototype, {
type: {
get: function getType () { return 'ModifyRequest' },
configurable: false
},
_dn: {
get: function getDN () { return this.object },
configurable: false
}
})
ModifyRequest.prototype._parse = function (ber) {
assert.ok(ber)
this.object = ber.readString()
ber.readSequence()
const end = ber.offset + ber.length
while (ber.offset < end) {
const c = new Change()
c.parse(ber)
c.modification.type = c.modification.type.toLowerCase()
this.changes.push(c)
}
this.changes.sort(Change.compare)
return true
}
ModifyRequest.prototype._toBer = function (ber) {
assert.ok(ber)
ber.writeString(this.object.toString())
ber.startSequence()
this.changes.forEach(function (c) {
c.toBer(ber)
})
ber.endSequence()
return ber
}
ModifyRequest.prototype._json = function (j) {
assert.ok(j)
j.object = this.object
j.changes = []
this.changes.forEach(function (c) {
j.changes.push(c.json)
})
return j
}
/// --- Exports
module.exports = ModifyRequest

View File

@ -0,0 +1,22 @@
// Copyright 2011 Mark Cavage, Inc. All rights reserved.
const assert = require('assert-plus')
const util = require('util')
const LDAPResult = require('./result')
const Protocol = require('../protocol')
/// --- API
function ModifyResponse (options) {
options = options || {}
assert.object(options)
options.protocolOp = Protocol.LDAP_REP_MODIFY
LDAPResult.call(this, options)
}
util.inherits(ModifyResponse, LDAPResult)
/// --- Exports
module.exports = ModifyResponse

View File

@ -4,36 +4,40 @@ const EventEmitter = require('events').EventEmitter
const util = require('util') const util = require('util')
const assert = require('assert-plus') const assert = require('assert-plus')
const asn1 = require('@ldapjs/asn1') const asn1 = require('asn1')
// var VError = require('verror').VError
const logger = require('../logger') const logger = require('../logger')
const messages = require('@ldapjs/messages') const AbandonRequest = require('./abandon_request')
const AbandonRequest = messages.AbandonRequest const AddRequest = require('./add_request')
const AddRequest = messages.AddRequest const AddResponse = require('./add_response')
const AddResponse = messages.AddResponse const BindRequest = require('./bind_request')
const BindRequest = messages.BindRequest const BindResponse = require('./bind_response')
const BindResponse = messages.BindResponse const CompareRequest = require('./compare_request')
const CompareRequest = messages.CompareRequest const CompareResponse = require('./compare_response')
const CompareResponse = messages.CompareResponse const DeleteRequest = require('./del_request')
const DeleteRequest = messages.DeleteRequest const DeleteResponse = require('./del_response')
const DeleteResponse = messages.DeleteResponse const ExtendedRequest = require('./ext_request')
const ExtendedRequest = messages.ExtensionRequest const ExtendedResponse = require('./ext_response')
const ExtendedResponse = messages.ExtensionResponse const ModifyRequest = require('./modify_request')
const ModifyRequest = messages.ModifyRequest const ModifyResponse = require('./modify_response')
const ModifyResponse = messages.ModifyResponse const ModifyDNRequest = require('./moddn_request')
const ModifyDNRequest = messages.ModifyDnRequest const ModifyDNResponse = require('./moddn_response')
const ModifyDNResponse = messages.ModifyDnResponse const SearchRequest = require('./search_request')
const SearchRequest = messages.SearchRequest const SearchEntry = require('./search_entry')
const SearchEntry = messages.SearchResultEntry const SearchReference = require('./search_reference')
const SearchReference = messages.SearchResultReference
const SearchResponse = require('./search_response') const SearchResponse = require('./search_response')
const UnbindRequest = messages.UnbindRequest const UnbindRequest = require('./unbind_request')
const LDAPResult = messages.LdapResult // var UnbindResponse = require('./unbind_response')
const Protocol = require('@ldapjs/protocol') const LDAPResult = require('./result')
// var Message = require('./message')
const Protocol = require('../protocol')
/// --- Globals /// --- Globals
// var Ber = asn1.Ber
const BerReader = asn1.BerReader const BerReader = asn1.BerReader
/// --- API /// --- API
@ -48,13 +52,6 @@ function Parser (options = {}) {
} }
util.inherits(Parser, EventEmitter) util.inherits(Parser, EventEmitter)
/**
* The LDAP server/client implementations will receive data from a stream and feed
* it into this method. This method will collect that data into an internal
* growing buffer. As that buffer fills with enough data to constitute a valid
* LDAP message, the data will be parsed, emitted as a message object, and
* reset the buffer to account for any next message in the stream.
*/
Parser.prototype.write = function (data) { Parser.prototype.write = function (data) {
if (!data || !Buffer.isBuffer(data)) { throw new TypeError('data (buffer) required') } if (!data || !Buffer.isBuffer(data)) { throw new TypeError('data (buffer) required') }
@ -67,9 +64,9 @@ Parser.prototype.write = function (data) {
return true return true
} }
self.buffer = self.buffer ? Buffer.concat([self.buffer, data]) : data self.buffer = (self.buffer ? Buffer.concat([self.buffer, data]) : data)
let ber = new BerReader(self.buffer) const ber = new BerReader(self.buffer)
let foundSeq = false let foundSeq = false
try { try {
@ -83,22 +80,9 @@ Parser.prototype.write = function (data) {
return false return false
} else if (ber.remain > ber.length) { } else if (ber.remain > ber.length) {
// ETOOMUCH // ETOOMUCH
// This is sort of ugly, but allows us to make miminal copies
// This is an odd branch. Basically, it is setting `nextMessage` to
// a buffer that represents data part of a message subsequent to the one
// being processed. It then re-creates `ber` as a representation of
// the message being processed and advances its offset to the value
// position of the TLV.
// Set `nextMessage` to the bytes subsequent to the current message's
// value bytes. That is, slice from the byte immediately following the
// current message's value bytes until the end of the buffer.
nextMessage = self.buffer.slice(ber.offset + ber.length) nextMessage = self.buffer.slice(ber.offset + ber.length)
ber._size = ber.offset + ber.length
const currOffset = ber.offset
ber = new BerReader(ber.buffer.subarray(0, currOffset + ber.length))
ber.readSequence()
assert.equal(ber.remain, ber.length) assert.equal(ber.remain, ber.length)
} }
@ -108,25 +92,13 @@ Parser.prototype.write = function (data) {
let message let message
try { try {
if (Object.prototype.toString.call(ber) === '[object BerReader]') {
// Parse the BER into a JavaScript object representation. The message
// objects require the full sequence in order to construct the object.
// At this point, we have already read the sequence tag and length, so
// we need to rewind the buffer a bit. The `.sequenceToReader` method
// does this for us.
message = messages.LdapMessage.parse(ber.sequenceToReader())
} else {
// Bail here if peer isn't speaking protocol at all // Bail here if peer isn't speaking protocol at all
message = this.getMessage(ber) message = this.getMessage(ber)
}
if (!message) { if (!message) {
return end() return end()
} }
message.parse(ber)
// TODO: find a better way to handle logging now that messages and the
// server are decoupled. ~ jsumners 2023-02-17
message.log = this.log
} catch (e) { } catch (e) {
this.emit('error', e, message) this.emit('error', e, message)
return false return false
@ -141,88 +113,88 @@ Parser.prototype.getMessage = function (ber) {
const self = this const self = this
const messageId = ber.readInt() const messageID = ber.readInt()
const type = ber.readSequence() const type = ber.readSequence()
let Message let Message
switch (type) { switch (type) {
case Protocol.operations.LDAP_REQ_ABANDON: case Protocol.LDAP_REQ_ABANDON:
Message = AbandonRequest Message = AbandonRequest
break break
case Protocol.operations.LDAP_REQ_ADD: case Protocol.LDAP_REQ_ADD:
Message = AddRequest Message = AddRequest
break break
case Protocol.operations.LDAP_RES_ADD: case Protocol.LDAP_REP_ADD:
Message = AddResponse Message = AddResponse
break break
case Protocol.operations.LDAP_REQ_BIND: case Protocol.LDAP_REQ_BIND:
Message = BindRequest Message = BindRequest
break break
case Protocol.operations.LDAP_RES_BIND: case Protocol.LDAP_REP_BIND:
Message = BindResponse Message = BindResponse
break break
case Protocol.operations.LDAP_REQ_COMPARE: case Protocol.LDAP_REQ_COMPARE:
Message = CompareRequest Message = CompareRequest
break break
case Protocol.operations.LDAP_RES_COMPARE: case Protocol.LDAP_REP_COMPARE:
Message = CompareResponse Message = CompareResponse
break break
case Protocol.operations.LDAP_REQ_DELETE: case Protocol.LDAP_REQ_DELETE:
Message = DeleteRequest Message = DeleteRequest
break break
case Protocol.operations.LDAP_RES_DELETE: case Protocol.LDAP_REP_DELETE:
Message = DeleteResponse Message = DeleteResponse
break break
case Protocol.operations.LDAP_REQ_EXTENSION: case Protocol.LDAP_REQ_EXTENSION:
Message = ExtendedRequest Message = ExtendedRequest
break break
case Protocol.operations.LDAP_RES_EXTENSION: case Protocol.LDAP_REP_EXTENSION:
Message = ExtendedResponse Message = ExtendedResponse
break break
case Protocol.operations.LDAP_REQ_MODIFY: case Protocol.LDAP_REQ_MODIFY:
Message = ModifyRequest Message = ModifyRequest
break break
case Protocol.operations.LDAP_RES_MODIFY: case Protocol.LDAP_REP_MODIFY:
Message = ModifyResponse Message = ModifyResponse
break break
case Protocol.operations.LDAP_REQ_MODRDN: case Protocol.LDAP_REQ_MODRDN:
Message = ModifyDNRequest Message = ModifyDNRequest
break break
case Protocol.operations.LDAP_RES_MODRDN: case Protocol.LDAP_REP_MODRDN:
Message = ModifyDNResponse Message = ModifyDNResponse
break break
case Protocol.operations.LDAP_REQ_SEARCH: case Protocol.LDAP_REQ_SEARCH:
Message = SearchRequest Message = SearchRequest
break break
case Protocol.operations.LDAP_RES_SEARCH_ENTRY: case Protocol.LDAP_REP_SEARCH_ENTRY:
Message = SearchEntry Message = SearchEntry
break break
case Protocol.operations.LDAP_RES_SEARCH_REF: case Protocol.LDAP_REP_SEARCH_REF:
Message = SearchReference Message = SearchReference
break break
case Protocol.operations.LDAP_RES_SEARCH: case Protocol.LDAP_REP_SEARCH:
Message = SearchResponse Message = SearchResponse
break break
case Protocol.operations.LDAP_REQ_UNBIND: case Protocol.LDAP_REQ_UNBIND:
Message = UnbindRequest Message = UnbindRequest
break break
@ -231,15 +203,15 @@ Parser.prototype.getMessage = function (ber) {
new Error('Op 0x' + (type ? type.toString(16) : '??') + new Error('Op 0x' + (type ? type.toString(16) : '??') +
' not supported'), ' not supported'),
new LDAPResult({ new LDAPResult({
messageId, messageID: messageID,
protocolOp: type || Protocol.operations.LDAP_RES_EXTENSION protocolOp: type || Protocol.LDAP_REP_EXTENSION
})) }))
return false return false
} }
return new Message({ return new Message({
messageId, messageID: messageID,
log: self.log log: self.log
}) })
} }

121
lib/messages/result.js Normal file
View File

@ -0,0 +1,121 @@
// Copyright 2011 Mark Cavage, Inc. All rights reserved.
const assert = require('assert-plus')
const util = require('util')
// var asn1 = require('asn1')
const dtrace = require('../dtrace')
const LDAPMessage = require('./message')
const Protocol = require('../protocol')
/// --- Globals
// var Ber = asn1.Ber
// var BerWriter = asn1.BerWriter
/// --- API
function LDAPResult (options) {
options = options || {}
assert.object(options)
assert.optionalNumber(options.status)
assert.optionalString(options.matchedDN)
assert.optionalString(options.errorMessage)
assert.optionalArrayOfString(options.referrals)
LDAPMessage.call(this, options)
this.status = options.status || 0 // LDAP SUCCESS
this.matchedDN = options.matchedDN || ''
this.errorMessage = options.errorMessage || ''
this.referrals = options.referrals || []
this.connection = options.connection || null
}
util.inherits(LDAPResult, LDAPMessage)
Object.defineProperties(LDAPResult.prototype, {
type: {
get: function getType () { return 'LDAPResult' },
configurable: false
}
})
LDAPResult.prototype.end = function (status) {
assert.ok(this.connection)
if (typeof (status) === 'number') { this.status = status }
const ber = this.toBer()
this.log.debug('%s: sending: %j', this.connection.ldap.id, this.json)
try {
const self = this
this.connection.write(ber)
if (self._dtraceOp && self._dtraceId) {
dtrace.fire('server-' + self._dtraceOp + '-done', function () {
const c = self.connection || { ldap: {} }
return [
self._dtraceId || 0,
(c.remoteAddress || ''),
c.ldap.bindDN ? c.ldap.bindDN.toString() : '',
(self.requestDN ? self.requestDN.toString() : ''),
status || self.status,
self.errorMessage
]
})
}
} catch (e) {
this.log.warn(e, '%s failure to write message %j',
this.connection.ldap.id, this.json)
}
}
LDAPResult.prototype._parse = function (ber) {
assert.ok(ber)
this.status = ber.readEnumeration()
this.matchedDN = ber.readString()
this.errorMessage = ber.readString()
const t = ber.peek()
if (t === Protocol.LDAP_REP_REFERRAL) {
const end = ber.offset + ber.length
while (ber.offset < end) { this.referrals.push(ber.readString()) }
}
return true
}
LDAPResult.prototype._toBer = function (ber) {
assert.ok(ber)
ber.writeEnumeration(this.status)
ber.writeString(this.matchedDN || '')
ber.writeString(this.errorMessage || '')
if (this.referrals.length) {
ber.startSequence(Protocol.LDAP_REP_REFERRAL)
ber.writeStringArray(this.referrals)
ber.endSequence()
}
return ber
}
LDAPResult.prototype._json = function (j) {
assert.ok(j)
j.status = this.status
j.matchedDN = this.matchedDN
j.errorMessage = this.errorMessage
j.referrals = this.referrals
return j
}
/// --- Exports
module.exports = LDAPResult

View File

@ -0,0 +1,188 @@
// Copyright 2011 Mark Cavage, Inc. All rights reserved.
const assert = require('assert-plus')
const util = require('util')
// var asn1 = require('asn1')
const LDAPMessage = require('./message')
const Attribute = require('../attribute')
const Protocol = require('../protocol')
const lassert = require('../assert')
/// --- Globals
// var BerWriter = asn1.BerWriter
/// --- API
function SearchEntry (options) {
options = options || {}
assert.object(options)
lassert.optionalStringDN(options.objectName)
options.protocolOp = Protocol.LDAP_REP_SEARCH_ENTRY
LDAPMessage.call(this, options)
this.objectName = options.objectName || null
this.setAttributes(options.attributes || [])
}
util.inherits(SearchEntry, LDAPMessage)
Object.defineProperties(SearchEntry.prototype, {
type: {
get: function getType () { return 'SearchEntry' },
configurable: false
},
_dn: {
get: function getDN () { return this.objectName },
configurable: false
},
object: {
get: function getObject () {
const obj = {
dn: this.dn.toString(),
controls: []
}
this.attributes.forEach(function (a) {
if (a.vals && a.vals.length) {
if (a.vals.length > 1) {
obj[a.type] = a.vals.slice()
} else {
obj[a.type] = a.vals[0]
}
} else {
obj[a.type] = []
}
})
this.controls.forEach(function (element) {
obj.controls.push(element.json)
})
return obj
},
configurable: false
},
raw: {
get: function getRaw () {
const obj = {
dn: this.dn.toString(),
controls: []
}
this.attributes.forEach(function (a) {
if (a.buffers && a.buffers.length) {
if (a.buffers.length > 1) {
obj[a.type] = a.buffers.slice()
} else {
obj[a.type] = a.buffers[0]
}
} else {
obj[a.type] = []
}
})
this.controls.forEach(function (element) {
obj.controls.push(element.json)
})
return obj
},
configurable: false
}
})
SearchEntry.prototype.addAttribute = function (attr) {
if (!attr || typeof (attr) !== 'object') { throw new TypeError('attr (attribute) required') }
this.attributes.push(attr)
}
SearchEntry.prototype.toObject = function () {
return this.object
}
SearchEntry.prototype.fromObject = function (obj) {
if (typeof (obj) !== 'object') { throw new TypeError('object required') }
const self = this
if (obj.controls) { this.controls = obj.controls }
if (obj.attributes) { obj = obj.attributes }
this.attributes = []
Object.keys(obj).forEach(function (k) {
self.attributes.push(new Attribute({ type: k, vals: obj[k] }))
})
return true
}
SearchEntry.prototype.setAttributes = function (obj) {
if (typeof (obj) !== 'object') { throw new TypeError('object required') }
if (Array.isArray(obj)) {
obj.forEach(function (a) {
if (!Attribute.isAttribute(a)) { throw new TypeError('entry must be an Array of Attributes') }
})
this.attributes = obj
} else {
const self = this
self.attributes = []
Object.keys(obj).forEach(function (k) {
const attr = new Attribute({ type: k })
if (Array.isArray(obj[k])) {
obj[k].forEach(function (v) {
attr.addValue(v.toString())
})
} else {
attr.addValue(obj[k].toString())
}
self.attributes.push(attr)
})
}
}
SearchEntry.prototype._json = function (j) {
assert.ok(j)
j.objectName = this.objectName.toString()
j.attributes = []
this.attributes.forEach(function (a) {
j.attributes.push(a.json || a)
})
return j
}
SearchEntry.prototype._parse = function (ber) {
assert.ok(ber)
this.objectName = ber.readString()
assert.ok(ber.readSequence())
const end = ber.offset + ber.length
while (ber.offset < end) {
const a = new Attribute()
a.parse(ber)
this.attributes.push(a)
}
return true
}
SearchEntry.prototype._toBer = function (ber) {
assert.ok(ber)
const formattedObjectName = this.objectName.format({ skipSpace: true })
ber.writeString(formattedObjectName)
ber.startSequence()
this.attributes.forEach(function (a) {
// This may or may not be an attribute
ber = Attribute.toBer(a, ber)
})
ber.endSequence()
return ber
}
/// --- Exports
module.exports = SearchEntry

View File

@ -0,0 +1,101 @@
// Copyright 2011 Mark Cavage, Inc. All rights reserved.
const assert = require('assert-plus')
const util = require('util')
// var asn1 = require('asn1')
const LDAPMessage = require('./message')
const Protocol = require('../protocol')
const dn = require('../dn')
const url = require('../url')
/// --- Globals
// var BerWriter = asn1.BerWriter
const parseURL = url.parse
/// --- API
function SearchReference (options) {
options = options || {}
assert.object(options)
options.protocolOp = Protocol.LDAP_REP_SEARCH_REF
LDAPMessage.call(this, options)
this.uris = options.uris || []
}
util.inherits(SearchReference, LDAPMessage)
Object.defineProperties(SearchReference.prototype, {
type: {
get: function getType () { return 'SearchReference' },
configurable: false
},
_dn: {
get: function getDN () { return new dn.DN('') },
configurable: false
},
object: {
get: function getObject () {
return {
dn: this.dn.toString(),
uris: this.uris.slice()
}
},
configurable: false
},
urls: {
get: function getUrls () { return this.uris },
set: function setUrls (val) {
assert.ok(val)
assert.ok(Array.isArray(val))
this.uris = val.slice()
},
configurable: false
}
})
SearchReference.prototype.toObject = function () {
return this.object
}
SearchReference.prototype.fromObject = function (obj) {
if (typeof (obj) !== 'object') { throw new TypeError('object required') }
this.uris = obj.uris ? obj.uris.slice() : []
return true
}
SearchReference.prototype._json = function (j) {
assert.ok(j)
j.uris = this.uris.slice()
return j
}
SearchReference.prototype._parse = function (ber, length) {
assert.ok(ber)
while (ber.offset < length) {
const _url = ber.readString()
parseURL(_url)
this.uris.push(_url)
}
return true
}
SearchReference.prototype._toBer = function (ber) {
assert.ok(ber)
this.uris.forEach(function (u) {
ber.writeString(u.href || u)
})
return ber
}
/// --- Exports
module.exports = SearchReference

View File

@ -0,0 +1,152 @@
// Copyright 2011 Mark Cavage, Inc. All rights reserved.
const assert = require('assert-plus')
const util = require('util')
const asn1 = require('asn1')
const LDAPMessage = require('./message')
// var LDAPResult = require('./result')
const dn = require('../dn')
const filters = require('../filters')
const Protocol = require('../protocol')
/// --- Globals
const Ber = asn1.Ber
/// --- API
function SearchRequest (options) {
options = options || {}
assert.object(options)
options.protocolOp = Protocol.LDAP_REQ_SEARCH
LDAPMessage.call(this, options)
if (options.baseObject !== undefined) {
this.baseObject = options.baseObject
} else {
this.baseObject = dn.parse('')
}
this.scope = options.scope || 'base'
this.derefAliases = options.derefAliases || Protocol.NEVER_DEREF_ALIASES
this.sizeLimit = options.sizeLimit || 0
this.timeLimit = options.timeLimit || 0
this.typesOnly = options.typesOnly || false
this.filter = options.filter || null
this.attributes = options.attributes ? options.attributes.slice(0) : []
}
util.inherits(SearchRequest, LDAPMessage)
Object.defineProperties(SearchRequest.prototype, {
type: {
get: function getType () { return 'SearchRequest' },
configurable: false
},
_dn: {
get: function getDN () { return this.baseObject },
configurable: false
},
scope: {
get: function getScope () {
switch (this._scope) {
case Protocol.SCOPE_BASE_OBJECT: return 'base'
case Protocol.SCOPE_ONE_LEVEL: return 'one'
case Protocol.SCOPE_SUBTREE: return 'sub'
default:
throw new Error(this._scope + ' is an invalid search scope')
}
},
set: function setScope (val) {
if (typeof (val) === 'string') {
switch (val) {
case 'base':
this._scope = Protocol.SCOPE_BASE_OBJECT
break
case 'one':
this._scope = Protocol.SCOPE_ONE_LEVEL
break
case 'sub':
this._scope = Protocol.SCOPE_SUBTREE
break
default:
throw new Error(val + ' is an invalid search scope')
}
} else {
this._scope = val
}
},
configurable: false
}
})
SearchRequest.prototype._parse = function (ber) {
assert.ok(ber)
this.baseObject = ber.readString()
this.scope = ber.readEnumeration()
this.derefAliases = ber.readEnumeration()
this.sizeLimit = ber.readInt()
this.timeLimit = ber.readInt()
this.typesOnly = ber.readBoolean()
this.filter = filters.parse(ber)
// look for attributes
if (ber.peek() === 0x30) {
ber.readSequence()
const end = ber.offset + ber.length
while (ber.offset < end) { this.attributes.push(ber.readString().toLowerCase()) }
}
return true
}
SearchRequest.prototype._toBer = function (ber) {
assert.ok(ber)
// Format only with commas, since that is what RFC 4514 mandates.
// There's a gotcha here: even though it's called baseObject,
// it can be a string or a DN object.
const formattedDN = dn.DN.isDN(this.baseObject)
? this.baseObject.format({ skipSpace: true })
: this.baseObject.toString()
ber.writeString(formattedDN)
ber.writeEnumeration(this._scope)
ber.writeEnumeration(this.derefAliases)
ber.writeInt(this.sizeLimit)
ber.writeInt(this.timeLimit)
ber.writeBoolean(this.typesOnly)
const f = this.filter || new filters.PresenceFilter({ attribute: 'objectclass' })
ber = f.toBer(ber)
ber.startSequence(Ber.Sequence | Ber.Constructor)
if (this.attributes && this.attributes.length) {
this.attributes.forEach(function (a) {
ber.writeString(a)
})
}
ber.endSequence()
return ber
}
SearchRequest.prototype._json = function (j) {
assert.ok(j)
j.baseObject = this.baseObject
j.scope = this.scope
j.derefAliases = this.derefAliases
j.sizeLimit = this.sizeLimit
j.timeLimit = this.timeLimit
j.typesOnly = this.typesOnly
j.filter = this.filter.toString()
j.attributes = this.attributes
return j
}
/// --- Exports
module.exports = SearchRequest

View File

@ -1,31 +1,31 @@
// Copyright 2011 Mark Cavage, Inc. All rights reserved. // Copyright 2011 Mark Cavage, Inc. All rights reserved.
const assert = require('assert-plus') const assert = require('assert-plus')
const util = require('util')
const Attribute = require('@ldapjs/attribute') const LDAPResult = require('./result')
const { const SearchEntry = require('./search_entry')
SearchResultEntry: SearchEntry, const SearchReference = require('./search_reference')
SearchResultReference: SearchReference,
SearchResultDone
} = require('@ldapjs/messages')
const parseDN = require('@ldapjs/dn').DN.fromString const dtrace = require('../dtrace')
const parseDN = require('../dn').parse
const parseURL = require('../url').parse
const Protocol = require('../protocol')
/// --- API /// --- API
class SearchResponse extends SearchResultDone { function SearchResponse (options) {
attributes options = options || {}
notAttributes assert.object(options)
sentEntries
constructor (options = {}) { options.protocolOp = Protocol.LDAP_REP_SEARCH
super(options) LDAPResult.call(this, options)
this.attributes = options.attributes ? options.attributes.slice() : [] this.attributes = options.attributes ? options.attributes.slice() : []
this.notAttributes = [] this.notAttributes = []
this.sentEntries = 0 this.sentEntries = 0
}
} }
util.inherits(SearchResponse, LDAPResult)
/** /**
* Allows you to send a SearchEntry back to the client. * Allows you to send a SearchEntry back to the client.
@ -44,16 +44,12 @@ SearchResponse.prototype.send = function (entry, nofiltering) {
const savedAttrs = {} const savedAttrs = {}
let save = null let save = null
if (entry instanceof SearchEntry || entry instanceof SearchReference) { if (entry instanceof SearchEntry || entry instanceof SearchReference) {
if (!entry.messageId) { entry.messageId = this.messageId } if (!entry.messageID) { entry.messageID = this.messageID }
if (entry.messageId !== this.messageId) { if (entry.messageID !== this.messageID) { throw new Error('SearchEntry messageID mismatch') }
throw new Error('SearchEntry messageId mismatch')
}
} else { } else {
if (!entry.attributes) { throw new Error('entry.attributes required') } if (!entry.attributes) { throw new Error('entry.attributes required') }
const all = (self.attributes.indexOf('*') !== -1) const all = (self.attributes.indexOf('*') !== -1)
// Filter attributes in a plain object according to the magic `_` prefix
// and presence in `notAttributes`.
Object.keys(entry.attributes).forEach(function (a) { Object.keys(entry.attributes).forEach(function (a) {
const _a = a.toLowerCase() const _a = a.toLowerCase()
if (!nofiltering && _a.length && _a[0] === '_') { if (!nofiltering && _a.length && _a[0] === '_') {
@ -73,24 +69,39 @@ SearchResponse.prototype.send = function (entry, nofiltering) {
save = entry save = entry
entry = new SearchEntry({ entry = new SearchEntry({
objectName: typeof (save.dn) === 'string' ? parseDN(save.dn) : save.dn, objectName: typeof (save.dn) === 'string' ? parseDN(save.dn) : save.dn,
messageId: self.messageId, messageID: self.messageID,
attributes: Attribute.fromObject(entry.attributes) log: self.log
}) })
entry.fromObject(save)
} }
try { try {
this.log.debug('%s: sending: %j', this.connection.ldap.id, entry.pojo) this.log.debug('%s: sending: %j', this.connection.ldap.id, entry.json)
this.connection.write(entry.toBer().buffer) this.connection.write(entry.toBer())
this.sentEntries++ this.sentEntries++
if (self._dtraceOp && self._dtraceId) {
dtrace.fire('server-search-entry', function () {
const c = self.connection || { ldap: {} }
return [
self._dtraceId || 0,
(c.remoteAddress || ''),
c.ldap.bindDN ? c.ldap.bindDN.toString() : '',
(self.requestDN ? self.requestDN.toString() : ''),
entry.objectName.toString(),
entry.attributes.length
]
})
}
// Restore attributes // Restore attributes
Object.keys(savedAttrs).forEach(function (k) { Object.keys(savedAttrs).forEach(function (k) {
save.attributes[k] = savedAttrs[k] save.attributes[k] = savedAttrs[k]
}) })
} catch (e) { } catch (e) {
this.log.warn(e, '%s failure to write message %j', this.log.warn(e, '%s failure to write message %j',
this.connection.ldap.id, this.pojo) this.connection.ldap.id, this.json)
} }
} }
@ -98,10 +109,11 @@ SearchResponse.prototype.createSearchEntry = function (object) {
assert.object(object) assert.object(object)
const entry = new SearchEntry({ const entry = new SearchEntry({
messageId: this.messageId, messageID: this.messageID,
objectName: object.objectName || object.dn, log: this.log,
attributes: object.attributes ?? [] objectName: object.objectName || object.dn
}) })
entry.fromObject((object.attributes || object))
return entry return entry
} }
@ -110,10 +122,15 @@ SearchResponse.prototype.createSearchReference = function (uris) {
if (!Array.isArray(uris)) { uris = [uris] } if (!Array.isArray(uris)) { uris = [uris] }
for (let i = 0; i < uris.length; i++) {
if (typeof (uris[i]) === 'string') { uris[i] = parseURL(uris[i]) }
}
const self = this const self = this
return new SearchReference({ return new SearchReference({
messageId: self.messageId, messageID: self.messageID,
uri: uris log: self.log,
uris: uris
}) })
} }

View File

@ -0,0 +1,62 @@
// Copyright 2011 Mark Cavage, Inc. All rights reserved.
const assert = require('assert-plus')
const util = require('util')
const LDAPMessage = require('./message')
const dn = require('../dn')
const Protocol = require('../protocol')
/// --- Globals
const DN = dn.DN
const RDN = dn.RDN
/// --- API
function UnbindRequest (options) {
options = options || {}
assert.object(options)
options.protocolOp = Protocol.LDAP_REQ_UNBIND
LDAPMessage.call(this, options)
}
util.inherits(UnbindRequest, LDAPMessage)
Object.defineProperties(UnbindRequest.prototype, {
type: {
get: function getType () { return 'UnbindRequest' },
configurable: false
},
_dn: {
get: function getDN () {
if (this.connection) {
return this.connection.ldap.bindDN
} else {
return new DN([new RDN({ cn: 'anonymous' })])
}
},
configurable: false
}
})
UnbindRequest.prototype._parse = function (ber) {
assert.ok(ber)
return true
}
UnbindRequest.prototype._toBer = function (ber) {
assert.ok(ber)
return ber
}
UnbindRequest.prototype._json = function (j) {
assert.ok(j)
return j
}
/// --- Exports
module.exports = UnbindRequest

View File

@ -0,0 +1,65 @@
// Copyright 2011 Mark Cavage, Inc. All rights reserved.
const assert = require('assert-plus')
const util = require('util')
const dtrace = require('../dtrace')
const LDAPMessage = require('./result')
// var Protocol = require('../protocol')
/// --- API
// Ok, so there's really no such thing as an unbind 'response', but to make
// the framework not suck, I just made this up, and have it stubbed so it's
// not such a one-off.
function UnbindResponse (options) {
options = options || {}
assert.object(options)
options.protocolOp = 0
LDAPMessage.call(this, options)
}
util.inherits(UnbindResponse, LDAPMessage)
Object.defineProperties(UnbindResponse.prototype, {
type: {
get: function getType () { return 'UnbindResponse' },
configurable: false
}
})
/**
* Special override that just ends the connection, if present.
*
* @param {Number} _status completely ignored.
*/
UnbindResponse.prototype.end = function (_status) {
assert.ok(this.connection)
this.log.trace('%s: unbinding!', this.connection.ldap.id)
this.connection.end()
const self = this
if (self._dtraceOp && self._dtraceId) {
dtrace.fire('server-' + self._dtraceOp + '-done', function () {
const c = self.connection || { ldap: {} }
return [
self._dtraceId || 0,
(c.remoteAddress || ''),
c.ldap.bindDN ? c.ldap.bindDN.toString() : '',
(self.requestDN ? self.requestDN.toString() : ''),
0,
''
]
})
}
}
UnbindResponse.prototype._json = function (j) {
return j
}
/// --- Exports
module.exports = UnbindResponse

View File

@ -89,7 +89,7 @@ function getEntryChangeNotificationControl (req, obj) {
} }
value.changeNumber = attrs.changenumber value.changeNumber = attrs.changenumber
return new EntryChangeNotificationControl({ value }) return new EntryChangeNotificationControl({ value: value })
} else { } else {
return false return false
} }
@ -104,6 +104,6 @@ function checkChangeType (req, requestType) {
module.exports = { module.exports = {
PersistentSearchCache: PersistentSearch, PersistentSearchCache: PersistentSearch,
checkChangeType, checkChangeType: checkChangeType,
getEntryChangeNotificationControl getEntryChangeNotificationControl: getEntryChangeNotificationControl
} }

53
lib/protocol.js Normal file
View File

@ -0,0 +1,53 @@
// Copyright 2011 Mark Cavage, Inc. All rights reserved.
module.exports = {
// Misc
LDAP_VERSION_3: 0x03,
LBER_SET: 0x31,
LDAP_CONTROLS: 0xa0,
// Search
SCOPE_BASE_OBJECT: 0,
SCOPE_ONE_LEVEL: 1,
SCOPE_SUBTREE: 2,
NEVER_DEREF_ALIASES: 0,
DEREF_IN_SEARCHING: 1,
DEREF_BASE_OBJECT: 2,
DEREF_ALWAYS: 3,
FILTER_AND: 0xa0,
FILTER_OR: 0xa1,
FILTER_NOT: 0xa2,
FILTER_EQUALITY: 0xa3,
FILTER_SUBSTRINGS: 0xa4,
FILTER_GE: 0xa5,
FILTER_LE: 0xa6,
FILTER_PRESENT: 0x87,
FILTER_APPROX: 0xa8,
FILTER_EXT: 0xa9,
// Protocol Operations
LDAP_REQ_BIND: 0x60,
LDAP_REQ_UNBIND: 0x42,
LDAP_REQ_SEARCH: 0x63,
LDAP_REQ_MODIFY: 0x66,
LDAP_REQ_ADD: 0x68,
LDAP_REQ_DELETE: 0x4a,
LDAP_REQ_MODRDN: 0x6c,
LDAP_REQ_COMPARE: 0x6e,
LDAP_REQ_ABANDON: 0x50,
LDAP_REQ_EXTENSION: 0x77,
LDAP_REP_BIND: 0x61,
LDAP_REP_SEARCH_ENTRY: 0x64,
LDAP_REP_SEARCH_REF: 0x73,
LDAP_REP_SEARCH: 0x65,
LDAP_REP_MODIFY: 0x67,
LDAP_REP_ADD: 0x69,
LDAP_REP_DELETE: 0x6b,
LDAP_REP_MODRDN: 0x6d,
LDAP_REP_COMPARE: 0x6f,
LDAP_REP_EXTENSION: 0x78
}

View File

@ -6,33 +6,33 @@ const net = require('net')
const tls = require('tls') const tls = require('tls')
const util = require('util') const util = require('util')
// var asn1 = require('@ldapjs/asn1') // var asn1 = require('asn1')
const VError = require('verror').VError const VError = require('verror').VError
const { DN, RDN } = require('@ldapjs/dn') const dn = require('./dn')
const dtrace = require('./dtrace')
const errors = require('./errors') const errors = require('./errors')
const Protocol = require('@ldapjs/protocol') const Protocol = require('./protocol')
const messages = require('@ldapjs/messages')
const Parser = require('./messages').Parser const Parser = require('./messages').Parser
const LdapResult = messages.LdapResult const AbandonResponse = require('./messages/abandon_response')
const AbandonResponse = messages.AbandonResponse const AddResponse = require('./messages/add_response')
const AddResponse = messages.AddResponse const BindResponse = require('./messages/bind_response')
const BindResponse = messages.BindResponse const CompareResponse = require('./messages/compare_response')
const CompareResponse = messages.CompareResponse const DeleteResponse = require('./messages/del_response')
const DeleteResponse = messages.DeleteResponse const ExtendedResponse = require('./messages/ext_response')
const ExtendedResponse = messages.ExtensionResponse // var LDAPResult = require('./messages/result')
const ModifyResponse = messages.ModifyResponse const ModifyResponse = require('./messages/modify_response')
const ModifyDnResponse = messages.ModifyDnResponse const ModifyDNResponse = require('./messages/moddn_response')
const SearchRequest = messages.SearchRequest const SearchRequest = require('./messages/search_request')
const SearchResponse = require('./messages/search_response') const SearchResponse = require('./messages/search_response')
const UnbindResponse = require('./messages/unbind_response')
/// --- Globals /// --- Globals
// var Ber = asn1.Ber // var Ber = asn1.Ber
// var BerReader = asn1.BerReader // var BerReader = asn1.BerReader
// const DN = dn.DN const DN = dn.DN
// var sprintf = util.format // var sprintf = util.format
@ -47,15 +47,15 @@ function mergeFunctionArgs (argv, start, end) {
const handlers = [] const handlers = []
for (let i = start; i < end; i++) { for (let i = start; i < end; i++) {
if (Array.isArray(argv[i])) { if (argv[i] instanceof Array) {
const arr = argv[i] const arr = argv[i]
for (let j = 0; j < arr.length; j++) { for (let j = 0; j < arr.length; j++) {
if (typeof arr[j] !== 'function') { if (!(arr[j] instanceof Function)) {
throw new TypeError('Invalid argument type: ' + typeof (arr[j])) throw new TypeError('Invalid argument type: ' + typeof (arr[j]))
} }
handlers.push(arr[j]) handlers.push(arr[j])
} }
} else if (typeof argv[i] === 'function') { } else if (argv[i] instanceof Function) {
handlers.push(argv[i]) handlers.push(argv[i])
} else { } else {
throw new TypeError('Invalid argument type: ' + typeof (argv[i])) throw new TypeError('Invalid argument type: ' + typeof (argv[i]))
@ -71,42 +71,35 @@ function getResponse (req) {
let Response let Response
switch (req.protocolOp) { switch (req.protocolOp) {
case Protocol.operations.LDAP_REQ_BIND: case Protocol.LDAP_REQ_BIND:
Response = BindResponse Response = BindResponse
break break
case Protocol.operations.LDAP_REQ_ABANDON: case Protocol.LDAP_REQ_ABANDON:
Response = AbandonResponse Response = AbandonResponse
break break
case Protocol.operations.LDAP_REQ_ADD: case Protocol.LDAP_REQ_ADD:
Response = AddResponse Response = AddResponse
break break
case Protocol.operations.LDAP_REQ_COMPARE: case Protocol.LDAP_REQ_COMPARE:
Response = CompareResponse Response = CompareResponse
break break
case Protocol.operations.LDAP_REQ_DELETE: case Protocol.LDAP_REQ_DELETE:
Response = DeleteResponse Response = DeleteResponse
break break
case Protocol.operations.LDAP_REQ_EXTENSION: case Protocol.LDAP_REQ_EXTENSION:
Response = ExtendedResponse Response = ExtendedResponse
break break
case Protocol.operations.LDAP_REQ_MODIFY: case Protocol.LDAP_REQ_MODIFY:
Response = ModifyResponse Response = ModifyResponse
break break
case Protocol.operations.LDAP_REQ_MODRDN: case Protocol.LDAP_REQ_MODRDN:
Response = ModifyDnResponse Response = ModifyDNResponse
break break
case Protocol.operations.LDAP_REQ_SEARCH: case Protocol.LDAP_REQ_SEARCH:
Response = SearchResponse Response = SearchResponse
break break
case Protocol.operations.LDAP_REQ_UNBIND: case Protocol.LDAP_REQ_UNBIND:
// TODO: when the server receives an unbind request this made up response object was returned. Response = UnbindResponse
// Instead, we need to just terminate the connection. ~ jsumners
Response = class extends LdapResult {
status = 0
end () {
req.connection.end()
}
}
break break
default: default:
return null return null
@ -114,83 +107,16 @@ function getResponse (req) {
assert.ok(Response) assert.ok(Response)
const res = new Response({ const res = new Response({
messageId: req.messageId, messageID: req.messageID,
log: req.log,
attributes: ((req instanceof SearchRequest) ? req.attributes : undefined) attributes: ((req instanceof SearchRequest) ? req.attributes : undefined)
}) })
res.log = req.log
res.connection = req.connection res.connection = req.connection
res.logId = req.logId res.logId = req.logId
if (typeof res.end !== 'function') {
// This is a hack to re-add the original tight coupling of the message
// objects and the server connection.
// TODO: remove this during server refactoring ~ jsumners 2023-02-16
switch (res.protocolOp) {
case 0: {
res.end = abandonResponseEnd
break
}
case Protocol.operations.LDAP_RES_COMPARE: {
res.end = compareResponseEnd
break
}
default: {
res.end = defaultResponseEnd
break
}
}
}
return res return res
} }
/**
* Response connection end handler for most responses.
*
* @param {number} status
*/
function defaultResponseEnd (status) {
if (typeof status === 'number') { this.status = status }
const ber = this.toBer()
this.log.debug('%s: sending: %j', this.connection.ldap.id, this.pojo)
try {
this.connection.write(ber.buffer)
} catch (error) {
this.log.warn(
error,
'%s failure to write message %j',
this.connection.ldap.id,
this.pojo
)
}
}
/**
* Response connection end handler for ABANDON responses.
*/
function abandonResponseEnd () {}
/**
* Response connection end handler for COMPARE responses.
*
* @param {number | boolean} status
*/
function compareResponseEnd (status) {
let result = 0x06
if (typeof status === 'boolean') {
if (status === false) {
result = 0x05
}
} else {
result = status
}
return defaultResponseEnd.call(this, result)
}
function defaultHandler (req, res, next) { function defaultHandler (req, res, next) {
assert.ok(req) assert.ok(req)
assert.ok(res) assert.ok(res)
@ -231,6 +157,69 @@ function noExOpHandler (req, res, next) {
return next() return next()
} }
function fireDTraceProbe (req, res) {
assert.ok(req)
req._dtraceId = res._dtraceId = dtrace._nextId()
const probeArgs = [
req._dtraceId,
req.connection.remoteAddress || 'localhost',
req.connection.ldap.bindDN.toString(),
req.dn.toString()
]
let op
switch (req.protocolOp) {
case Protocol.LDAP_REQ_ABANDON:
op = 'abandon'
break
case Protocol.LDAP_REQ_ADD:
op = 'add'
probeArgs.push(req.attributes.length)
break
case Protocol.LDAP_REQ_BIND:
op = 'bind'
break
case Protocol.LDAP_REQ_COMPARE:
op = 'compare'
probeArgs.push(req.attribute)
probeArgs.push(req.value)
break
case Protocol.LDAP_REQ_DELETE:
op = 'delete'
break
case Protocol.LDAP_REQ_EXTENSION:
op = 'exop'
probeArgs.push(req.name)
probeArgs.push(req.value)
break
case Protocol.LDAP_REQ_MODIFY:
op = 'modify'
probeArgs.push(req.changes.length)
break
case Protocol.LDAP_REQ_MODRDN:
op = 'modifydn'
probeArgs.push(req.newRdn.toString())
probeArgs.push((req.newSuperior ? req.newSuperior.toString() : ''))
break
case Protocol.LDAP_REQ_SEARCH:
op = 'search'
probeArgs.push(req.scope)
probeArgs.push(req.filter.toString())
break
case Protocol.LDAP_REQ_UNBIND:
op = 'unbind'
break
default:
break
}
res._dtraceOp = op
dtrace.fire('server-' + op + '-start', function () {
return probeArgs
})
}
/// --- API /// --- API
/** /**
@ -272,6 +261,8 @@ function Server (options) {
this._chain = [] this._chain = []
this.log = options.log this.log = options.log
this.strictDN = (options.strictDN !== undefined) ? options.strictDN : true
const log = this.log const log = this.log
function setupConnection (c) { function setupConnection (c) {
@ -286,12 +277,12 @@ function Server (options) {
c.remotePort = c.socket.remotePort c.remotePort = c.socket.remotePort
} }
const rdn = new RDN({ cn: 'anonymous' }) const rdn = new dn.RDN({ cn: 'anonymous' })
c.ldap = { c.ldap = {
id: c.remoteAddress + ':' + c.remotePort, id: c.remoteAddress + ':' + c.remotePort,
config: options, config: options,
_bindDN: new DN({ rdns: [rdn] }) _bindDN: new DN([rdn])
} }
c.addListener('timeout', function () { c.addListener('timeout', function () {
log.trace('%s timed out', c.ldap.id) log.trace('%s timed out', c.ldap.id)
@ -314,9 +305,7 @@ function Server (options) {
return c.ldap._bindDN return c.ldap._bindDN
}) })
c.ldap.__defineSetter__('bindDN', function (val) { c.ldap.__defineSetter__('bindDN', function (val) {
if (Object.prototype.toString.call(val) !== '[object LdapDn]') { if (!(val instanceof DN)) { throw new TypeError('DN required') }
throw new TypeError('DN required')
}
c.ldap._bindDN = val c.ldap._bindDN = val
return val return val
@ -330,17 +319,19 @@ function Server (options) {
setupConnection(conn) setupConnection(conn)
log.trace('new connection from %s', conn.ldap.id) log.trace('new connection from %s', conn.ldap.id)
dtrace.fire('server-connection', function () {
return [conn.remoteAddress]
})
conn.parser = new Parser({ conn.parser = new Parser({
log: options.log log: options.log
}) })
conn.parser.on('message', function (req) { conn.parser.on('message', function (req) {
// TODO: this is mutating the `@ldapjs/message` objects.
// We should avoid doing that. ~ jsumners 2023-02-16
req.connection = conn req.connection = conn
req.logId = conn.ldap.id + '::' + req.messageId req.logId = conn.ldap.id + '::' + req.messageID
req.startTime = new Date().getTime() req.startTime = new Date().getTime()
log.debug('%s: message received: req=%j', conn.ldap.id, req.pojo) log.debug('%s: message received: req=%j', conn.ldap.id, req.json)
const res = getResponse(req) const res = getResponse(req)
if (!res) { if (!res) {
@ -352,48 +343,32 @@ function Server (options) {
// parse string DNs for routing/etc // parse string DNs for routing/etc
try { try {
switch (req.protocolOp) { switch (req.protocolOp) {
case Protocol.operations.LDAP_REQ_BIND: { case Protocol.LDAP_REQ_BIND:
req.name = DN.fromString(req.name) req.name = dn.parse(req.name)
break break
} case Protocol.LDAP_REQ_ADD:
case Protocol.LDAP_REQ_COMPARE:
case Protocol.operations.LDAP_REQ_ADD: case Protocol.LDAP_REQ_DELETE:
case Protocol.operations.LDAP_REQ_COMPARE: req.entry = dn.parse(req.entry)
case Protocol.operations.LDAP_REQ_DELETE: {
if (typeof req.entry === 'string') {
req.entry = DN.fromString(req.entry)
} else if (Object.prototype.toString.call(req.entry) !== '[object LdapDn]') {
throw Error('invalid entry object for operation')
}
break break
} case Protocol.LDAP_REQ_MODIFY:
req.object = dn.parse(req.object)
case Protocol.operations.LDAP_REQ_MODIFY: {
req.object = DN.fromString(req.object)
break break
} case Protocol.LDAP_REQ_MODRDN:
req.entry = dn.parse(req.entry)
case Protocol.operations.LDAP_REQ_MODRDN: {
if (typeof req.entry === 'string') {
req.entry = DN.fromString(req.entry)
} else if (Object.prototype.toString.call(req.entry) !== '[object LdapDn]') {
throw Error('invalid entry object for operation')
}
// TODO: handle newRdn/Superior // TODO: handle newRdn/Superior
break break
} case Protocol.LDAP_REQ_SEARCH:
req.baseObject = dn.parse(req.baseObject)
case Protocol.operations.LDAP_REQ_SEARCH: {
break break
} default:
default: {
break break
} }
}
} catch (e) { } catch (e) {
if (self.strictDN) {
return res.end(errors.LDAP_INVALID_DN_SYNTAX) return res.end(errors.LDAP_INVALID_DN_SYNTAX)
} }
}
res.connection = conn res.connection = conn
res.logId = req.logId res.logId = req.logId
@ -431,19 +406,19 @@ function Server (options) {
const next = messageIIFE const next = messageIIFE
if (chain.handlers[i]) { return chain.handlers[i++].call(chain.backend, req, res, next) } if (chain.handlers[i]) { return chain.handlers[i++].call(chain.backend, req, res, next) }
if (req.protocolOp === Protocol.operations.LDAP_REQ_BIND && res.status === 0) { if (req.protocolOp === Protocol.LDAP_REQ_BIND && res.status === 0) {
// 0 length == anonymous bind // 0 length == anonymous bind
if (req.dn.length === 0 && req.credentials === '') { if (req.dn.length === 0 && req.credentials === '') {
conn.ldap.bindDN = new DN({ rdns: [new RDN({ cn: 'anonymous' })] }) conn.ldap.bindDN = new DN([new dn.RDN({ cn: 'anonymous' })])
} else { } else {
conn.ldap.bindDN = DN.fromString(req.dn) conn.ldap.bindDN = req.dn
} }
} }
// unbind clear bindDN for safety // unbind clear bindDN for safety
// conn should terminate on unbind (RFC4511 4.3) // conn should terminate on unbind (RFC4511 4.3)
if (req.protocolOp === Protocol.operations.LDAP_REQ_UNBIND && res.status === 0) { if (req.protocolOp === Protocol.LDAP_REQ_UNBIND && res.status === 0) {
conn.ldap.bindDN = new DN({ rdns: [new RDN({ cn: 'anonymous' })] }) conn.ldap.bindDN = new DN([new dn.RDN({ cn: 'anonymous' })])
} }
return after() return after()
@ -533,15 +508,7 @@ Object.defineProperties(Server.prototype, {
} else { } else {
str = 'ldap://' str = 'ldap://'
} }
str += this.host + ':' + this.port
let host = this.host
// Node 18 switched family from returning a string to returning a number
// https://nodejs.org/api/net.html#serveraddress
if (addr.family === 'IPv6' || addr.family === 6) {
host = '[' + this.host + ']'
}
str += host + ':' + this.port
return str return str
}, },
configurable: false configurable: false
@ -561,7 +528,7 @@ module.exports = Server
*/ */
Server.prototype.add = function (name) { Server.prototype.add = function (name) {
const args = Array.prototype.slice.call(arguments, 1) const args = Array.prototype.slice.call(arguments, 1)
return this._mount(Protocol.operations.LDAP_REQ_ADD, name, args) return this._mount(Protocol.LDAP_REQ_ADD, name, args)
} }
/** /**
@ -576,7 +543,7 @@ Server.prototype.add = function (name) {
*/ */
Server.prototype.bind = function (name) { Server.prototype.bind = function (name) {
const args = Array.prototype.slice.call(arguments, 1) const args = Array.prototype.slice.call(arguments, 1)
return this._mount(Protocol.operations.LDAP_REQ_BIND, name, args) return this._mount(Protocol.LDAP_REQ_BIND, name, args)
} }
/** /**
@ -591,7 +558,7 @@ Server.prototype.bind = function (name) {
*/ */
Server.prototype.compare = function (name) { Server.prototype.compare = function (name) {
const args = Array.prototype.slice.call(arguments, 1) const args = Array.prototype.slice.call(arguments, 1)
return this._mount(Protocol.operations.LDAP_REQ_COMPARE, name, args) return this._mount(Protocol.LDAP_REQ_COMPARE, name, args)
} }
/** /**
@ -606,7 +573,7 @@ Server.prototype.compare = function (name) {
*/ */
Server.prototype.del = function (name) { Server.prototype.del = function (name) {
const args = Array.prototype.slice.call(arguments, 1) const args = Array.prototype.slice.call(arguments, 1)
return this._mount(Protocol.operations.LDAP_REQ_DELETE, name, args) return this._mount(Protocol.LDAP_REQ_DELETE, name, args)
} }
/** /**
@ -621,7 +588,7 @@ Server.prototype.del = function (name) {
*/ */
Server.prototype.exop = function (name) { Server.prototype.exop = function (name) {
const args = Array.prototype.slice.call(arguments, 1) const args = Array.prototype.slice.call(arguments, 1)
return this._mount(Protocol.operations.LDAP_REQ_EXTENSION, name, args, true) return this._mount(Protocol.LDAP_REQ_EXTENSION, name, args, true)
} }
/** /**
@ -636,7 +603,7 @@ Server.prototype.exop = function (name) {
*/ */
Server.prototype.modify = function (name) { Server.prototype.modify = function (name) {
const args = Array.prototype.slice.call(arguments, 1) const args = Array.prototype.slice.call(arguments, 1)
return this._mount(Protocol.operations.LDAP_REQ_MODIFY, name, args) return this._mount(Protocol.LDAP_REQ_MODIFY, name, args)
} }
/** /**
@ -651,7 +618,7 @@ Server.prototype.modify = function (name) {
*/ */
Server.prototype.modifyDN = function (name) { Server.prototype.modifyDN = function (name) {
const args = Array.prototype.slice.call(arguments, 1) const args = Array.prototype.slice.call(arguments, 1)
return this._mount(Protocol.operations.LDAP_REQ_MODRDN, name, args) return this._mount(Protocol.LDAP_REQ_MODRDN, name, args)
} }
/** /**
@ -666,7 +633,7 @@ Server.prototype.modifyDN = function (name) {
*/ */
Server.prototype.search = function (name) { Server.prototype.search = function (name) {
const args = Array.prototype.slice.call(arguments, 1) const args = Array.prototype.slice.call(arguments, 1)
return this._mount(Protocol.operations.LDAP_REQ_SEARCH, name, args) return this._mount(Protocol.LDAP_REQ_SEARCH, name, args)
} }
/** /**
@ -680,7 +647,7 @@ Server.prototype.search = function (name) {
*/ */
Server.prototype.unbind = function () { Server.prototype.unbind = function () {
const args = Array.prototype.slice.call(arguments, 0) const args = Array.prototype.slice.call(arguments, 0)
return this._mount(Protocol.operations.LDAP_REQ_UNBIND, 'unbind', args, true) return this._mount(Protocol.LDAP_REQ_UNBIND, 'unbind', args, true)
} }
Server.prototype.use = function use () { Server.prototype.use = function use () {
@ -701,13 +668,13 @@ Server.prototype.after = function () {
}) })
} }
// All these just re-expose the requisite net.Server APIs // All these just reexpose the requisite net.Server APIs
Server.prototype.listen = function (port, host, callback) { Server.prototype.listen = function (port, host, callback) {
if (typeof (port) !== 'number' && typeof (port) !== 'string') { throw new TypeError('port (number or path) required') } if (typeof (port) !== 'number' && typeof (port) !== 'string') { throw new TypeError('port (number or path) required') }
if (typeof (host) === 'function') { if (typeof (host) === 'function') {
callback = host callback = host
host = '127.0.0.1' host = '0.0.0.0'
} }
if (typeof (port) === 'string' && /^[0-9]+$/.test(port)) { if (typeof (port) === 'string' && /^[0-9]+$/.test(port)) {
// Disambiguate between string ports and file paths // Disambiguate between string ports and file paths
@ -750,10 +717,12 @@ Server.prototype.getConnections = function (callback) {
} }
Server.prototype._getRoute = function (_dn, backend) { Server.prototype._getRoute = function (_dn, backend) {
assert.ok(dn)
if (!backend) { backend = this } if (!backend) { backend = this }
let name let name
if (Object.prototype.toString.call(_dn) === '[object LdapDn]') { if (_dn instanceof dn.DN) {
name = _dn.toString() name = _dn.toString()
} else { } else {
name = _dn name = _dn
@ -780,10 +749,10 @@ Server.prototype._sortedRouteKeys = function _sortedRouteKeys () {
Object.keys(this.routes).forEach(function (key) { Object.keys(this.routes).forEach(function (key) {
const _dn = self.routes[key].dn const _dn = self.routes[key].dn
// Ignore non-DN routes such as exop or unbind // Ignore non-DN routes such as exop or unbind
if (Object.prototype.toString.call(_dn) === '[object LdapDn]') { if (_dn instanceof dn.DN) {
const reversed = _dn.clone() const reversed = _dn.clone()
reversed.reverse() reversed.rdns.reverse()
reversedRDNsToKeys[reversed.toString()] = key reversedRDNsToKeys[reversed.format()] = key
} }
}) })
const output = [] const output = []
@ -800,15 +769,17 @@ Server.prototype._sortedRouteKeys = function _sortedRouteKeys () {
return this._routeKeyCache return this._routeKeyCache
} }
Server.prototype._getHandlerChain = function _getHandlerChain (req) { Server.prototype._getHandlerChain = function _getHandlerChain (req, res) {
assert.ok(req) assert.ok(req)
fireDTraceProbe(req, res)
const self = this const self = this
const routes = this.routes const routes = this.routes
let route let route
// check anonymous bind // check anonymous bind
if (req.protocolOp === Protocol.operations.LDAP_REQ_BIND && if (req.protocolOp === Protocol.LDAP_REQ_BIND &&
req.dn.toString() === '' && req.dn.toString() === '' &&
req.credentials === '') { req.credentials === '') {
return { return {
@ -820,7 +791,7 @@ Server.prototype._getHandlerChain = function _getHandlerChain (req) {
const op = '0x' + req.protocolOp.toString(16) const op = '0x' + req.protocolOp.toString(16)
// Special cases are exops, unbinds and abandons. Handle those first. // Special cases are exops, unbinds and abandons. Handle those first.
if (req.protocolOp === Protocol.operations.LDAP_REQ_EXTENSION) { if (req.protocolOp === Protocol.LDAP_REQ_EXTENSION) {
route = routes[req.requestName] route = routes[req.requestName]
if (route) { if (route) {
return { return {
@ -833,7 +804,7 @@ Server.prototype._getHandlerChain = function _getHandlerChain (req) {
handlers: [noExOpHandler] handlers: [noExOpHandler]
} }
} }
} else if (req.protocolOp === Protocol.operations.LDAP_REQ_UNBIND) { } else if (req.protocolOp === Protocol.LDAP_REQ_UNBIND) {
route = routes.unbind route = routes.unbind
if (route) { if (route) {
return { return {
@ -846,7 +817,7 @@ Server.prototype._getHandlerChain = function _getHandlerChain (req) {
handlers: [defaultNoOpHandler] handlers: [defaultNoOpHandler]
} }
} }
} else if (req.protocolOp === Protocol.operations.LDAP_REQ_ABANDON) { } else if (req.protocolOp === Protocol.LDAP_REQ_ABANDON) {
return { return {
backend: self, backend: self,
handlers: [defaultNoOpHandler] handlers: [defaultNoOpHandler]
@ -854,11 +825,11 @@ Server.prototype._getHandlerChain = function _getHandlerChain (req) {
} }
// Otherwise, match via DN rules // Otherwise, match via DN rules
assert.ok(req.dn)
const keys = this._sortedRouteKeys() const keys = this._sortedRouteKeys()
let fallbackHandler = [noSuffixHandler] let fallbackHandler = [noSuffixHandler]
// invalid DNs in non-strict mode are routed to the default handler // invalid DNs in non-strict mode are routed to the default handler
const testDN = (typeof (req.dn) === 'string') ? DN.fromString(req.dn) : req.dn const testDN = (typeof (req.dn) === 'string') ? '' : req.dn
assert.ok(testDN)
for (let i = 0; i < keys.length; i++) { for (let i = 0; i < keys.length; i++) {
const suffix = keys[i] const suffix = keys[i]
@ -905,7 +876,7 @@ Server.prototype._mount = function (op, name, argv, notDN) {
backend = argv[0] backend = argv[0]
index = 1 index = 1
} }
const route = this._getRoute(notDN ? name : DN.fromString(name), backend) const route = this._getRoute(notDN ? name : dn.parse(name), backend)
const chain = this._chain.slice() const chain = this._chain.slice()
argv.slice(index).forEach(function (a) { argv.slice(index).forEach(function (a) {

View File

@ -2,8 +2,8 @@
const querystring = require('querystring') const querystring = require('querystring')
const url = require('url') const url = require('url')
const { DN } = require('@ldapjs/dn') const dn = require('./dn')
const filter = require('@ldapjs/filter') const filter = require('./filters/')
module.exports = { module.exports = {
@ -38,7 +38,7 @@ module.exports = {
if (u.pathname) { if (u.pathname) {
u.pathname = querystring.unescape(u.pathname.substr(1)) u.pathname = querystring.unescape(u.pathname.substr(1))
u.DN = parseDN ? DN.fromString(u.pathname) : u.pathname u.DN = parseDN ? dn.parse(u.pathname) : u.pathname
} }
if (u.search) { if (u.search) {

View File

@ -3,57 +3,57 @@
"name": "ldapjs", "name": "ldapjs",
"homepage": "http://ldapjs.org", "homepage": "http://ldapjs.org",
"description": "LDAP client and server APIs", "description": "LDAP client and server APIs",
"version": "3.0.7", "version": "2.3.2",
"license": "MIT", "license": "MIT",
"repository": { "repository": {
"type": "git", "type": "git",
"url": "git://github.com/ldapjs/node-ldapjs.git" "url": "git://github.com/ldapjs/node-ldapjs.git"
}, },
"main": "lib/index.js", "main": "lib/index.js",
"directories": {
"lib": "./lib"
},
"engines": {
"node": ">=10.13.0"
},
"dependencies": { "dependencies": {
"@ldapjs/asn1": "^2.0.0", "abstract-logging": "^2.0.0",
"@ldapjs/attribute": "^1.0.0", "asn1": "^0.2.4",
"@ldapjs/change": "^1.0.0",
"@ldapjs/controls": "^2.1.0",
"@ldapjs/dn": "^1.1.0",
"@ldapjs/filter": "^2.1.1",
"@ldapjs/messages": "^1.3.0",
"@ldapjs/protocol": "^1.2.1",
"abstract-logging": "^2.0.1",
"assert-plus": "^1.0.0", "assert-plus": "^1.0.0",
"backoff": "^2.5.0", "backoff": "^2.5.0",
"ldap-filter": "^0.3.3",
"once": "^1.4.0", "once": "^1.4.0",
"vasync": "^2.2.1", "vasync": "^2.2.0",
"verror": "^1.10.1" "verror": "^1.8.1"
}, },
"devDependencies": { "devDependencies": {
"@fastify/pre-commit": "^2.0.2", "eslint": "^7.20.0",
"eslint": "^8.44.0", "eslint-config-standard": "^16.0.2",
"eslint-config-standard": "^17.0.0", "eslint-plugin-import": "^2.22.1",
"eslint-plugin-import": "^2.27.5",
"eslint-plugin-n": "^16.0.0",
"eslint-plugin-node": "^11.1.0", "eslint-plugin-node": "^11.1.0",
"eslint-plugin-promise": "6.1.1", "eslint-plugin-promise": "^5.1.0",
"front-matter": "^4.0.2", "front-matter": "^4.0.2",
"get-port": "^5.1.1", "get-port": "^5.1.1",
"highlight.js": "^11.7.0", "highlight.js": "^11.0.1",
"marked": "^4.2.12", "husky": "^4.2.5",
"tap": "^16.3.7" "marked": "^4.0.0",
"tap": "15.1.6"
}, },
"scripts": { "scripts": {
"test": "tap --no-cov -R terse", "test": "tap --no-cov",
"test:ci": "tap --coverage-report=lcovonly -R terse", "test:ci": "tap --coverage-report=lcovonly",
"test:cov": "tap -R terse", "test:cov": "tap",
"test:cov:html": "tap --coverage-report=html -R terse", "test:cov:html": "tap --coverage-report=html",
"test:watch": "tap -n -w --no-coverage-report -R terse", "test:watch": "tap -n -w --no-coverage-report",
"test:integration": "tap --no-cov -R terse 'test-integration/**/*.test.js'", "test:integration": "tap --no-cov 'test-integration/**/*.test.js'",
"test:integration:local": "docker-compose up -d --wait && npm run test:integration ; docker-compose down", "test:integration:local": "docker-compose up -d && npm run test:integration && docker-compose down",
"lint": "eslint . --fix", "lint": "eslint . --fix",
"lint:ci": "eslint .", "lint:ci": "eslint .",
"docs": "node scripts/build-docs.js" "docs": "node scripts/build-docs.js"
}, },
"pre-commit": [ "husky": {
"lint:ci", "hooks": {
"test" "pre-commit": "npm run lint:ci && npm run test"
] }
}
} }

View File

@ -1,95 +0,0 @@
'use strict'
const tap = require('tap')
const ldapjs = require('../../lib')
const parseDN = ldapjs.parseDN
const SCHEME = process.env.SCHEME || 'ldap'
const HOST = process.env.HOST || '127.0.0.1'
const PORT = process.env.PORT || 389
const baseURL = `${SCHEME}://${HOST}:${PORT}`
const client = ldapjs.createClient({ url: baseURL })
tap.before(() => {
return new Promise((resolve, reject) => {
client.bind('cn=admin,dc=planetexpress,dc=com', 'GoodNewsEveryone', (err) => {
if (err) {
return reject(err)
}
resolve()
})
})
})
tap.teardown(() => {
client.unbind()
})
tap.test('can search OUs with Japanese characters', t => {
t.plan(2)
const opts = {
filter: '(&(objectClass=person))',
scope: 'sub',
paged: true,
sizeLimit: 100,
attributes: ['cn', 'employeeID']
}
const baseDN = parseDN('ou=テスト,dc=planetexpress,dc=com')
client.search(baseDN.toString(), opts, (err, res) => {
t.error(err, 'search error')
res.on('searchEntry', (entry) => {
t.match(entry.pojo, {
type: 'SearchResultEntry',
objectName: 'cn=jdoe,ou=\\e3\\83\\86\\e3\\82\\b9\\e3\\83\\88,dc=planetexpress,dc=com',
attributes: [{
type: 'cn',
values: ['John', 'jdoe']
}]
})
})
res.on('error', (err) => {
t.error(err, 'search entry error')
})
res.on('end', () => {
t.end()
})
})
})
tap.test('can search with non-ascii chars in filter', t => {
t.plan(3)
const opts = {
filter: '(&(sn=Rodríguez))',
scope: 'sub',
attributes: ['dn', 'sn', 'cn'],
type: 'user'
}
let searchEntryCount = 0
client.search('dc=planetexpress,dc=com', opts, (err, res) => {
t.error(err, 'search error')
res.on('searchEntry', (entry) => {
searchEntryCount += 1
t.match(entry.pojo, {
type: 'SearchResultEntry',
objectName: 'cn=Bender Bending Rodr\\c3\\adguez,ou=people,dc=planetexpress,dc=com',
attributes: [{
type: 'cn',
values: ['Bender Bending Rodríguez']
}]
})
})
res.on('error', (err) => {
t.error(err, 'search entry error')
})
res.on('end', () => {
t.equal(searchEntryCount, 1, 'should have found 1 entry')
t.end()
})
})
})

View File

@ -1,56 +0,0 @@
'use strict'
const tap = require('tap')
const ldapjs = require('../../lib')
const SCHEME = process.env.SCHEME || 'ldap'
const HOST = process.env.HOST || '127.0.0.1'
const PORT = process.env.PORT || 389
const baseURL = `${SCHEME}://${HOST}:${PORT}`
const client = ldapjs.createClient({ url: baseURL })
tap.test('adds entries with Korean characters', t => {
t.plan(4)
client.bind('cn=admin,dc=planetexpress,dc=com', 'GoodNewsEveryone', (err) => {
t.error(err, 'bind error')
})
const nm = '홍길동'
const dn = `cn=${nm},ou=people,dc=planetexpress,dc=com`
const entry = {
objectclass: 'person',
sn: 'korean test'
}
client.add(dn, entry, err => {
t.error(err, 'add entry error')
const searchOpts = {
filter: '(sn=korean test)',
scope: 'subtree',
attributes: ['cn', 'sn'],
sizeLimit: 10,
timeLimit: 0
}
client.search('ou=people,dc=planetexpress,dc=com', searchOpts, (err, res) => {
t.error(err, 'search error')
res.on('searchEntry', (entry) => {
t.equal(
entry.attributes.filter(a => a.type === 'cn').pop().values.pop(),
nm
)
})
res.on('error', (err) => {
t.error(err, 'search entry error')
})
res.on('end', () => {
client.unbind(t.end)
})
})
})
})

View File

@ -1,55 +0,0 @@
'use strict'
const tap = require('tap')
const ldapjs = require('../../lib')
const parseDN = ldapjs.parseDN
const SCHEME = process.env.SCHEME || 'ldap'
const HOST = process.env.HOST || '127.0.0.1'
const PORT = process.env.PORT || 389
const baseURL = `${SCHEME}://${HOST}:${PORT}`
const client = ldapjs.createClient({ url: baseURL })
const searchOpts = {
filter: '(&(objectClass=person))',
scope: 'sub',
paged: true,
sizeLimit: 0,
attributes: ['cn', 'employeeID']
}
const baseDN = parseDN('ou=large_ou,dc=planetexpress,dc=com')
tap.test('paged search option returns pages', t => {
t.plan(4)
client.bind('cn=admin,dc=planetexpress,dc=com', 'GoodNewsEveryone', (err) => {
t.error(err, 'bind error')
})
client.search(baseDN.toString(), searchOpts, (err, res) => {
t.error(err, 'search error')
let pages = 0
const results = []
res.on('searchEntry', (entry) => {
results.push(entry)
})
res.on('page', () => {
pages += 1
})
res.on('error', (err) => {
t.error(err, 'search entry error')
})
res.on('end', () => {
t.equal(results.length, 2000)
t.equal(pages, 20)
client.unbind(t.end)
})
})
})

View File

@ -1,91 +0,0 @@
'use strict'
const tap = require('tap')
const ldapjs = require('../../lib')
const { DN } = require('@ldapjs/dn')
const Change = require('@ldapjs/change')
const SCHEME = process.env.SCHEME || 'ldap'
const HOST = process.env.HOST || '127.0.0.1'
const PORT = process.env.PORT || 389
const baseURL = `${SCHEME}://${HOST}:${PORT}`
const client = ldapjs.createClient({ url: baseURL })
tap.teardown(() => {
client.unbind()
})
tap.test('modifies entry specified by dn string', t => {
t.plan(4)
client.bind('cn=admin,dc=planetexpress,dc=com', 'GoodNewsEveryone', (err) => {
t.error(err, 'bind error')
})
const dn = 'cn=large10,ou=large_ou,dc=planetexpress,dc=com'
const change = new Change({
operation: 'replace',
modification: {
type: 'givenName',
values: ['test']
}
})
client.modify(dn, change, (err) => {
t.error(err, 'modify error')
validateChange({ t, expected: 'test', client })
})
})
tap.test('modifies entry specified by dn object', t => {
t.plan(4)
client.bind('cn=admin,dc=planetexpress,dc=com', 'GoodNewsEveryone', (err) => {
t.error(err, 'bind error')
})
const dn = DN.fromString('cn=large10,ou=large_ou,dc=planetexpress,dc=com')
const change = new Change({
operation: 'replace',
modification: {
type: 'givenName',
values: ['test2']
}
})
client.modify(dn, change, (err) => {
t.error(err, 'modify error')
validateChange({ t, expected: 'test2', client })
})
})
function validateChange ({ t, expected, client }) {
const searchBase = 'ou=large_ou,dc=planetexpress,dc=com'
const searchOpts = {
filter: '(cn=large10)',
scope: 'subtree',
attributes: ['givenName'],
sizeLimit: 10,
timeLimit: 0
}
client.search(searchBase, searchOpts, (err, res) => {
t.error(err, 'search error')
res.on('searchEntry', entry => {
t.equal(
entry.attributes.filter(a => a.type === 'givenName').pop().values.pop(),
expected
)
})
res.on('error', err => {
t.error(err, 'search entry error')
})
res.on('end', () => {
t.end()
})
})
}

View File

@ -1,81 +0,0 @@
'use strict'
const tap = require('tap')
const ldapjs = require('../../lib')
const Change = require('@ldapjs/change')
const SCHEME = process.env.SCHEME || 'ldap'
const HOST = process.env.HOST || '127.0.0.1'
const PORT = process.env.PORT || 389
const baseURL = `${SCHEME}://${HOST}:${PORT}`
const client = ldapjs.createClient({ url: baseURL })
tap.before(() => {
return new Promise((resolve, reject) => {
client.bind('cn=admin,dc=planetexpress,dc=com', 'GoodNewsEveryone', (err) => {
if (err) {
return reject(err)
}
resolve()
})
})
})
tap.teardown(() => {
client.unbind()
})
tap.test('can modify entries with non-ascii chars in RDN', t => {
t.plan(6)
const dn = 'cn=Mendonça,ou=people,dc=planetexpress,dc=com'
const entry = {
objectclass: 'person',
sn: 'change me'
}
client.add(dn, entry, error => {
t.error(error, 'add should not error')
doSearch('change me', doModify)
})
function doModify () {
const change = new Change({
operation: 'replace',
modification: {
type: 'sn',
values: ['changed']
}
})
client.modify(dn, change, (error) => {
t.error(error, 'modify should not error')
doSearch('changed', t.end.bind(t))
})
}
function doSearch (expected, callback) {
const searchOpts = {
filter: '(&(objectclass=person)(cn=Mendonça))',
scope: 'subtree',
attributes: ['sn']
}
client.search('ou=people,dc=planetexpress,dc=com', searchOpts, (error, res) => {
t.error(error, 'search should not error')
res.on('searchEntry', entry => {
const found = entry.attributes.filter(a => a.type === 'sn').pop().values.pop()
t.equal(found, expected, `expected '${expected}' and got '${found}'`)
})
res.on('error', error => {
t.error(error, 'search result processing should not error')
})
res.on('end', () => {
callback()
})
})
}
})

View File

@ -1,87 +0,0 @@
'use strict'
const tap = require('tap')
const ldapjs = require('../../lib')
const SCHEME = process.env.SCHEME || 'ldap'
const HOST = process.env.HOST || '127.0.0.1'
const PORT = process.env.PORT || 389
const baseURL = `${SCHEME}://${HOST}:${PORT}`
tap.test('can use password policy response', t => {
const client = ldapjs.createClient({ url: baseURL })
const targetDN = 'cn=Bender Bending Rodríguez,ou=people,dc=planetexpress,dc=com'
client.bind('cn=admin,dc=planetexpress,dc=com', 'GoodNewsEveryone', (err, res) => {
t.error(err)
t.ok(res)
t.equal(res.status, 0)
const newPassword = 'bender2'
changePassword(client, newPassword, () => {
client.unbind()
bindNewClient(newPassword, { error: 2 }, (client) => {
const newPassword = 'bender'
changePassword(client, newPassword, () => {
client.unbind()
bindNewClient(newPassword, { timeBeforeExpiration: 1000 }, (client) => {
client.unbind(t.end)
})
})
})
})
})
function bindNewClient (pwd, expected, callback) {
const client = ldapjs.createClient({ url: baseURL })
const control = new ldapjs.PasswordPolicyControl()
client.bind(targetDN, pwd, control, (err, res) => {
t.error(err)
t.ok(res)
t.equal(res.status, 0)
let error = null
let timeBeforeExpiration = null
let graceAuthNsRemaining = null
res.controls.forEach(control => {
if (control.type === ldapjs.PasswordPolicyControl.OID) {
error = control.value.error ?? error
timeBeforeExpiration = control.value.timeBeforeExpiration ?? timeBeforeExpiration
graceAuthNsRemaining = control.value.graceAuthNsRemaining ?? graceAuthNsRemaining
}
})
if (expected.error !== undefined) {
t.equal(error, expected.error)
}
if (expected.timeBeforeExpiration !== undefined) {
t.equal(timeBeforeExpiration, expected.timeBeforeExpiration)
}
if (expected.graceAuthNsRemaining !== undefined) {
t.equal(graceAuthNsRemaining, expected.graceAuthNsRemaining)
}
callback(client)
})
}
function changePassword (client, newPwd, callback) {
const change = new ldapjs.Change({
operation: 'replace',
modification: new ldapjs.Attribute({
type: 'userPassword',
values: newPwd
})
})
client.modify(targetDN, change, (err, res) => {
t.error(err)
t.ok(res)
t.equal(res.status, 0)
callback()
})
}
})

View File

@ -10,14 +10,7 @@ const PORT = process.env.PORT || 389
const baseURL = `${SCHEME}://${HOST}:${PORT}` const baseURL = `${SCHEME}://${HOST}:${PORT}`
tap.test('modifyDN with long name (issue #480)', t => { tap.test('modifyDN with long name (issue #480)', t => {
// 2023-08-15: disabling this 265 character string until a bug can be const longStr = 'a292979f2c86d513d48bbb9786b564b3c5228146e5ba46f404724e322544a7304a2b1049168803a5485e2d57a544c6a0d860af91330acb77e5907a9e601ad1227e80e0dc50abe963b47a004f2c90f570450d0e920d15436fdc771e3bdac0487a9735473ed3a79361d1778d7e53a7fb0e5f01f97a75ef05837d1d5496fc86968ff47fcb64'
// fixed in OpenLDAP. See https://github.com/ldapjs/docker-test-openldap/blob/d48bc2fb001b4ed9a152715ced4a2cb120439ec4/bootstrap/slapd-init.sh#L19-L31.
// const longStr = 'a292979f2c86d513d48bbb9786b564b3c5228146e5ba46f404724e322544a7304a2b1049168803a5485e2d57a544c6a0d860af91330acb77e5907a9e601ad1227e80e0dc50abe963b47a004f2c90f570450d0e920d15436fdc771e3bdac0487a9735473ed3a79361d1778d7e53a7fb0e5f01f97a75ef05837d1d5496fc86968ff47fcb64'
// 2023-08-15: this 140 character string satisfies the original issue
// (https://github.com/ldapjs/node-ldapjs/issues/480) and avoids a bug
// in OpenLDAP 2.5.
const longStr = '292979f2c86d513d48bbb9786b564b3c5228146e5ba46f404724e322544a7304a2b1049168803a5485e2d57a544c6a0d860af91330acb77e5907a9e601ad1227e80e0dc50ab'
const targetDN = 'cn=Turanga Leela,ou=people,dc=planetexpress,dc=com' const targetDN = 'cn=Turanga Leela,ou=people,dc=planetexpress,dc=com'
const client = ldapjs.createClient({ url: baseURL }) const client = ldapjs.createClient({ url: baseURL })
client.bind('cn=admin,dc=planetexpress,dc=com', 'GoodNewsEveryone', bindHandler) client.bind('cn=admin,dc=planetexpress,dc=com', 'GoodNewsEveryone', bindHandler)
@ -55,9 +48,9 @@ tap.test('whois works correctly (issue #370)', t => {
client.exop('1.3.6.1.4.1.4203.1.11.3', (err, value, res) => { client.exop('1.3.6.1.4.1.4203.1.11.3', (err, value, res) => {
t.error(err) t.error(err)
t.ok(value) t.ok(value)
t.equal(value, 'dn:cn=Philip J. Fry,ou=people,dc=planetexpress,dc=com') t.is(value, 'dn:cn=Philip J. Fry,ou=people,dc=planetexpress,dc=com')
t.ok(res) t.ok(res)
t.equal(res.status, 0) t.is(res.status, 0)
client.unbind(t.end) client.unbind(t.end)
}) })
@ -81,15 +74,15 @@ tap.test('can access large groups (issue #582)', t => {
}) })
response.on('error', t.error) response.on('error', t.error)
response.on('end', (result) => { response.on('end', (result) => {
t.equal(result.status, 0) t.is(result.status, 0)
t.equal(results.length === 1, true) t.is(results.length === 1, true)
t.ok(results[0].attributes) t.ok(results[0].attributes)
const memberAttr = results[0].attributes.find(a => a.type === 'member') const memberAttr = results[0].attributes.find(a => a.type === 'member')
t.ok(memberAttr) t.ok(memberAttr)
t.ok(memberAttr.values) t.ok(memberAttr.vals)
t.type(memberAttr.values, Array) t.type(memberAttr.vals, Array)
t.equal(memberAttr.values.length, 2000) t.is(memberAttr.vals.length, 2000)
client.unbind(t.end) client.unbind(t.end)
}) })

View File

@ -1,8 +1,4 @@
module.exports = { module.exports = {
parserOptions: {
ecmaVersion: 'latest'
},
rules: { rules: {
'no-shadow': 'off' 'no-shadow': 'off'
} }

163
test/attribute.test.js Normal file
View File

@ -0,0 +1,163 @@
'use strict'
const { test } = require('tap')
const { BerReader, BerWriter } = require('asn1')
const { Attribute } = require('../lib')
test('new no args', function (t) {
t.ok(new Attribute())
t.end()
})
test('new with args', function (t) {
let attr = new Attribute({
type: 'cn',
vals: ['foo', 'bar']
})
t.ok(attr)
attr.addValue('baz')
t.equal(attr.type, 'cn')
t.equal(attr.vals.length, 3)
t.equal(attr.vals[0], 'foo')
t.equal(attr.vals[1], 'bar')
t.equal(attr.vals[2], 'baz')
t.throws(function () {
attr = new Attribute('not an object')
})
t.throws(function () {
const typeThatIsNotAString = 1
attr = new Attribute({
type: typeThatIsNotAString
})
})
t.end()
})
test('toBer', function (t) {
const attr = new Attribute({
type: 'cn',
vals: ['foo', 'bar']
})
t.ok(attr)
const ber = new BerWriter()
attr.toBer(ber)
const reader = new BerReader(ber.buffer)
t.ok(reader.readSequence())
t.equal(reader.readString(), 'cn')
t.equal(reader.readSequence(), 0x31) // lber set
t.equal(reader.readString(), 'foo')
t.equal(reader.readString(), 'bar')
t.end()
})
test('parse', function (t) {
const ber = new BerWriter()
ber.startSequence()
ber.writeString('cn')
ber.startSequence(0x31)
ber.writeStringArray(['foo', 'bar'])
ber.endSequence()
ber.endSequence()
const attr = new Attribute()
t.ok(attr)
t.ok(attr.parse(new BerReader(ber.buffer)))
t.equal(attr.type, 'cn')
t.equal(attr.vals.length, 2)
t.equal(attr.vals[0], 'foo')
t.equal(attr.vals[1], 'bar')
t.end()
})
test('parse - without 0x31', function (t) {
const ber = new BerWriter()
ber.startSequence()
ber.writeString('sn')
ber.endSequence()
const attr = new Attribute()
t.ok(attr)
t.ok(attr.parse(new BerReader(ber.buffer)))
t.equal(attr.type, 'sn')
t.equal(attr.vals.length, 0)
t.end()
})
test('toString', function (t) {
const attr = new Attribute({
type: 'foobar',
vals: ['asdf']
})
const expected = attr.toString()
const actual = JSON.stringify(attr.json)
t.equal(actual, expected)
t.end()
})
test('isAttribute', function (t) {
const isA = Attribute.isAttribute
t.notOk(isA(null))
t.notOk(isA('asdf'))
t.ok(isA(new Attribute({
type: 'foobar',
vals: ['asdf']
})))
t.ok(isA({
type: 'foo',
vals: ['item', Buffer.alloc(5)],
toBer: function () { /* placeholder */ }
}))
// bad type in vals
t.notOk(isA({
type: 'foo',
vals: ['item', null],
toBer: function () { /* placeholder */ }
}))
t.end()
})
test('compare', function (t) {
const comp = Attribute.compare
const a = new Attribute({
type: 'foo',
vals: ['bar']
})
const b = new Attribute({
type: 'foo',
vals: ['bar']
})
const notAnAttribute = 'this is not an attribute'
t.throws(function () {
comp(a, notAnAttribute)
})
t.throws(function () {
comp(notAnAttribute, b)
})
t.equal(comp(a, b), 0)
// Different types
a.type = 'boo'
t.equal(comp(a, b), -1)
t.equal(comp(b, a), 1)
a.type = 'foo'
// Different value counts
a.vals = ['bar', 'baz']
t.equal(comp(a, b), 1)
t.equal(comp(b, a), -1)
// Different value contents (same count)
a.vals = ['baz']
t.equal(comp(a, b), 1)
t.equal(comp(b, a), -1)
t.end()
})

253
test/change.test.js Normal file
View File

@ -0,0 +1,253 @@
'use strict'
const fs = require('fs')
const path = require('path')
const { test } = require('tap')
const { BerReader, BerWriter } = require('asn1')
const { Attribute, Change } = require('../lib')
test('new no args', function (t) {
t.ok(new Change())
t.end()
})
test('new with args', function (t) {
const change = new Change({
operation: 'add',
modification: new Attribute({
type: 'cn',
vals: ['foo', 'bar']
})
})
t.ok(change)
t.equal(change.operation, 'add')
t.equal(change.modification.type, 'cn')
t.equal(change.modification.vals.length, 2)
t.equal(change.modification.vals[0], 'foo')
t.equal(change.modification.vals[1], 'bar')
t.end()
})
test('new with args and buffer', function (t) {
const img = fs.readFileSync(path.join(__dirname, '/imgs/test.jpg'))
const change = new Change({
operation: 'add',
modification: {
thumbnailPhoto: img
}
})
t.ok(change)
t.equal(change.operation, 'add')
t.equal(change.modification.type, 'thumbnailPhoto')
t.equal(change.modification.vals.length, 1)
t.equal(change.modification.buffers[0].compare(img), 0)
t.end()
})
test('validate fields', function (t) {
const c = new Change()
t.ok(c)
t.throws(function () {
c.operation = 'bogus'
})
t.throws(function () {
c.modification = { too: 'many', fields: 'here' }
})
c.modification = {
foo: ['bar', 'baz']
}
t.ok(c.modification)
t.end()
})
test('GH-31 (multiple attributes per Change)', function (t) {
t.throws(function () {
const c = new Change({
operation: 'replace',
modification: {
cn: 'foo',
sn: 'bar'
}
})
t.notOk(c)
})
t.end()
})
test('toBer', function (t) {
const change = new Change({
operation: 'Add',
modification: new Attribute({
type: 'cn',
vals: ['foo', 'bar']
})
})
t.ok(change)
const ber = new BerWriter()
change.toBer(ber)
const reader = new BerReader(ber.buffer)
t.ok(reader.readSequence())
t.equal(reader.readEnumeration(), 0x00)
t.ok(reader.readSequence())
t.equal(reader.readString(), 'cn')
t.equal(reader.readSequence(), 0x31) // lber set
t.equal(reader.readString(), 'foo')
t.equal(reader.readString(), 'bar')
t.end()
})
test('parse', function (t) {
const ber = new BerWriter()
ber.startSequence()
ber.writeEnumeration(0x00)
ber.startSequence()
ber.writeString('cn')
ber.startSequence(0x31)
ber.writeStringArray(['foo', 'bar'])
ber.endSequence()
ber.endSequence()
ber.endSequence()
const change = new Change()
t.ok(change)
t.ok(change.parse(new BerReader(ber.buffer)))
t.equal(change.operation, 'add')
t.equal(change.modification.type, 'cn')
t.equal(change.modification.vals.length, 2)
t.equal(change.modification.vals[0], 'foo')
t.equal(change.modification.vals[1], 'bar')
t.end()
})
test('apply - replace', function (t) {
let res
const single = new Change({
operation: 'replace',
modification: {
type: 'cn',
vals: ['new']
}
})
const twin = new Change({
operation: 'replace',
modification: {
type: 'cn',
vals: ['new', 'two']
}
})
const empty = new Change({
operation: 'replace',
modification: {
type: 'cn',
vals: []
}
})
// plain
res = Change.apply(single, { cn: ['old'] })
t.same(res.cn, ['new'])
// multiple
res = Change.apply(single, { cn: ['old', 'also'] })
t.same(res.cn, ['new'])
// empty
res = Change.apply(empty, { cn: ['existing'] })
t.equal(res.cn, undefined)
t.ok(Object.keys(res).indexOf('cn') === -1)
// absent
res = Change.apply(single, { dn: ['otherjunk'] })
t.same(res.cn, ['new'])
// scalar formatting "success"
res = Change.apply(single, { cn: 'old' }, true)
t.equal(res.cn, 'new')
// scalar formatting "failure"
res = Change.apply(twin, { cn: 'old' }, true)
t.same(res.cn, ['new', 'two'])
t.end()
})
test('apply - add', function (t) {
let res
const single = new Change({
operation: 'add',
modification: {
type: 'cn',
vals: ['new']
}
})
// plain
res = Change.apply(single, { cn: ['old'] })
t.same(res.cn, ['old', 'new'])
// multiple
res = Change.apply(single, { cn: ['old', 'also'] })
t.same(res.cn, ['old', 'also', 'new'])
// absent
res = Change.apply(single, { dn: ['otherjunk'] })
t.same(res.cn, ['new'])
// scalar formatting "success"
res = Change.apply(single, { }, true)
t.equal(res.cn, 'new')
// scalar formatting "failure"
res = Change.apply(single, { cn: 'old' }, true)
t.same(res.cn, ['old', 'new'])
// duplicate add
res = Change.apply(single, { cn: 'new' })
t.same(res.cn, ['new'])
t.end()
})
test('apply - delete', function (t) {
let res
const single = new Change({
operation: 'delete',
modification: {
type: 'cn',
vals: ['old']
}
})
// plain
res = Change.apply(single, { cn: ['old', 'new'] })
t.same(res.cn, ['new'])
// empty
res = Change.apply(single, { cn: ['old'] })
t.equal(res.cn, undefined)
t.ok(Object.keys(res).indexOf('cn') === -1)
// scalar formatting "success"
res = Change.apply(single, { cn: ['old', 'one'] }, true)
t.equal(res.cn, 'one')
// scalar formatting "failure"
res = Change.apply(single, { cn: ['old', 'several', 'items'] }, true)
t.same(res.cn, ['several', 'items'])
// absent
res = Change.apply(single, { dn: ['otherjunk'] })
t.ok(res)
t.equal(res.cn, undefined)
t.end()
})

View File

@ -6,19 +6,8 @@ const tap = require('tap')
const vasync = require('vasync') const vasync = require('vasync')
const getPort = require('get-port') const getPort = require('get-port')
const { getSock, uuid } = require('./utils') const { getSock, uuid } = require('./utils')
const Attribute = require('@ldapjs/attribute')
const Change = require('@ldapjs/change')
const messages = require('@ldapjs/messages')
const controls = require('@ldapjs/controls')
const dn = require('@ldapjs/dn')
const ldap = require('../lib') const ldap = require('../lib')
const { Attribute, Change } = ldap
const {
SearchRequest,
SearchResultEntry,
SearchResultReference,
SearchResultDone
} = messages
const SUFFIX = 'dc=test' const SUFFIX = 'dc=test'
const LDAP_CONNECT_TIMEOUT = process.env.LDAP_CONNECT_TIMEOUT || 0 const LDAP_CONNECT_TIMEOUT = process.env.LDAP_CONNECT_TIMEOUT || 0
@ -55,7 +44,7 @@ tap.beforeEach((t) => {
// LDAP whoami // LDAP whoami
server.exop('1.3.6.1.4.1.4203.1.11.3', function (req, res, next) { server.exop('1.3.6.1.4.1.4203.1.11.3', function (req, res, next) {
res.responseValue = 'u:xxyyz@EXAMPLE.NET' res.value = 'u:xxyyz@EXAMPLE.NET'
res.end() res.end()
return next() return next()
}) })
@ -99,21 +88,21 @@ tap.beforeEach((t) => {
if (req.dn.equals('cn=ref,' + SUFFIX)) { if (req.dn.equals('cn=ref,' + SUFFIX)) {
res.send(res.createSearchReference('ldap://localhost')) res.send(res.createSearchReference('ldap://localhost'))
} else if (req.dn.equals('cn=bin,' + SUFFIX)) { } else if (req.dn.equals('cn=bin,' + SUFFIX)) {
const attributes = []
attributes.push(new Attribute({ type: 'foo;binary', values: ['wr0gKyDCvCA9IMK+'] }))
attributes.push(new Attribute({ type: 'gb18030', values: [Buffer.from([0xB5, 0xE7, 0xCA, 0xD3, 0xBB, 0xFA])] }))
attributes.push(new Attribute({ type: 'objectclass', values: ['binary'] }))
res.send(res.createSearchEntry({ res.send(res.createSearchEntry({
objectName: req.dn, objectName: req.dn,
attributes attributes: {
'foo;binary': 'wr0gKyDCvCA9IMK+',
gb18030: Buffer.from([0xB5, 0xE7, 0xCA, 0xD3, 0xBB, 0xFA]),
objectclass: 'binary'
}
})) }))
} else { } else {
const attributes = []
attributes.push(new Attribute({ type: 'cn', values: ['unit', 'test'] }))
attributes.push(new Attribute({ type: 'SN', values: ['testy'] }))
const e = res.createSearchEntry({ const e = res.createSearchEntry({
objectName: req.dn, objectName: req.dn,
attributes attributes: {
cn: ['unit', 'test'],
SN: 'testy'
}
}) })
res.send(e) res.send(e)
res.send(e) res.send(e)
@ -153,14 +142,13 @@ tap.beforeEach((t) => {
end = (end > max || end < min) ? max : end end = (end > max || end < min) ? max : end
let i let i
for (i = start; i < end; i++) { for (i = start; i < end; i++) {
res.send(new SearchResultEntry({ res.send({
messageId: res.id, dn: util.format('o=%d, cn=paged', i),
entry: `o=${i},cn=paged`, attributes: {
attributes: Attribute.fromObject({
o: [i], o: [i],
objectclass: ['pagedResult'] objectclass: ['pagedResult']
}
}) })
}))
} }
return i return i
} }
@ -168,17 +156,13 @@ tap.beforeEach((t) => {
let cookie = null let cookie = null
let pageSize = 0 let pageSize = 0
req.controls.forEach(function (control) { req.controls.forEach(function (control) {
if (control.type === controls.PagedResultsControl.OID) { if (control.type === ldap.PagedResultsControl.OID) {
pageSize = control.value.size pageSize = control.value.size
cookie = control.value.cookie cookie = control.value.cookie
} }
}) })
if (!cookie || Buffer.isBuffer(cookie) === false) { if (cookie && Buffer.isBuffer(cookie)) {
// don't allow non-paged searches for this test endpoint
next(Error('unwilling to perform'))
}
// Do simple paging // Do simple paging
let first = min let first = min
if (cookie.length !== 0) { if (cookie.length !== 0) {
@ -192,7 +176,7 @@ tap.beforeEach((t) => {
} else { } else {
resultCookie = Buffer.from('') resultCookie = Buffer.from('')
} }
res.addControl(new controls.PagedResultsControl({ res.controls.push(new ldap.PagedResultsControl({
value: { value: {
size: pageSize, // correctness not required here size: pageSize, // correctness not required here
cookie: resultCookie cookie: resultCookie
@ -200,8 +184,11 @@ tap.beforeEach((t) => {
})) }))
res.end() res.end()
next() next()
} else {
// don't allow non-paged searches for this test endpoint
next(new ldap.UnwillingToPerformError())
}
}) })
server.search('cn=sssvlv', function (req, res, next) { server.search('cn=sssvlv', function (req, res, next) {
const min = 0 const min = 0
const max = 100 const max = 100
@ -497,7 +484,7 @@ tap.test('add success', function (t) {
const attrs = [ const attrs = [
new Attribute({ new Attribute({
type: 'cn', type: 'cn',
values: ['test'] vals: ['test']
}) })
] ]
t.context.client.add('cn=add, ' + SUFFIX, attrs, function (err, res) { t.context.client.add('cn=add, ' + SUFFIX, attrs, function (err, res) {
@ -522,8 +509,8 @@ tap.test('add success with object', function (t) {
}) })
tap.test('add buffer', function (t) { tap.test('add buffer', function (t) {
const { BerReader } = require('@ldapjs/asn1') const { BerReader } = require('asn1')
const dn = `cn=add,${SUFFIX}` const dn = `cn=add, ${SUFFIX}`
const attribute = 'thumbnailPhoto' const attribute = 'thumbnailPhoto'
const binary = 0xa5 const binary = 0xa5
const entry = { const entry = {
@ -532,7 +519,7 @@ tap.test('add buffer', function (t) {
const write = t.context.client._socket.write const write = t.context.client._socket.write
t.context.client._socket.write = (data, encoding, cb) => { t.context.client._socket.write = (data, encoding, cb) => {
const reader = new BerReader(data) const reader = new BerReader(data)
t.equal(data.byteLength, 48) t.equal(data.byteLength, 49)
t.ok(reader.readSequence()) t.ok(reader.readSequence())
t.equal(reader.readInt(), 0x1) t.equal(reader.readInt(), 0x1)
t.equal(reader.readSequence(), 0x68) t.equal(reader.readSequence(), 0x68)
@ -634,7 +621,7 @@ tap.test('modify success', function (t) {
type: 'Replace', type: 'Replace',
modification: new Attribute({ modification: new Attribute({
type: 'cn', type: 'cn',
values: ['test'] vals: ['test']
}) })
}) })
t.context.client.modify('cn=modify, ' + SUFFIX, change, function (err, res) { t.context.client.modify('cn=modify, ' + SUFFIX, change, function (err, res) {
@ -645,11 +632,26 @@ tap.test('modify success', function (t) {
}) })
}) })
tap.test('modify change plain object success', function (t) {
const change = new Change({
type: 'Replace',
modification: {
cn: 'test'
}
})
t.context.client.modify('cn=modify, ' + SUFFIX, change, function (err, res) {
t.error(err)
t.ok(res)
t.equal(res.status, 0)
t.end()
})
})
// https://github.com/ldapjs/node-ldapjs/pull/435 // https://github.com/ldapjs/node-ldapjs/pull/435
tap.test('can delete attributes', function (t) { tap.test('can delete attributes', function (t) {
const change = new Change({ const change = new Change({
type: 'Delete', type: 'Delete',
modification: new Attribute({ type: 'cn', values: [null] }) modification: { cn: null }
}) })
t.context.client.modify('cn=modify,' + SUFFIX, change, function (err, res) { t.context.client.modify('cn=modify,' + SUFFIX, change, function (err, res) {
t.error(err) t.error(err)
@ -665,7 +667,7 @@ tap.test('modify array success', function (t) {
operation: 'Replace', operation: 'Replace',
modification: new Attribute({ modification: new Attribute({
type: 'cn', type: 'cn',
values: ['test'] vals: ['test']
}) })
}), }),
new Change({ new Change({
@ -683,6 +685,22 @@ tap.test('modify array success', function (t) {
}) })
}) })
tap.test('modify change plain object success (GH-31)', function (t) {
const change = {
type: 'replace',
modification: {
cn: 'test',
sn: 'bar'
}
}
t.context.client.modify('cn=modify, ' + SUFFIX, change, function (err, res) {
t.error(err)
t.ok(res)
t.equal(res.status, 0)
t.end()
})
})
tap.test('modify DN new RDN only', function (t) { tap.test('modify DN new RDN only', function (t) {
t.context.client.modifyDN('cn=old, ' + SUFFIX, 'cn=new', function (err, res) { t.context.client.modifyDN('cn=old, ' + SUFFIX, 'cn=new', function (err, res) {
t.error(err) t.error(err)
@ -711,42 +729,47 @@ tap.test('modify DN excessive length (GH-480)', function (t) {
}) })
tap.test('modify DN excessive superior length', function (t) { tap.test('modify DN excessive superior length', function (t) {
const { ModifyDnRequest } = messages const { BerReader, BerWriter } = require('asn1')
const ModifyDNRequest = require('../lib/messages/moddn_request')
const ber = new BerWriter()
const entry = 'cn=Test User,ou=A Long OU ,ou=Another Long OU ,ou=Another Long OU ,dc=acompany,DC=io' const entry = 'cn=Test User,ou=A Long OU ,ou=Another Long OU ,ou=Another Long OU ,dc=acompany,DC=io'
const newSuperior = 'ou=A New Long OU , ou=Another New Long OU , ou=An OU , dc=acompany, dc=io' const newSuperior = 'ou=A New Long OU , ou=Another New Long OU , ou=An OU , dc=acompany, dc=io'
const newRdn = entry.replace(/(.*?),.*/, '$1') const newRdn = entry.replace(/(.*?),.*/, '$1')
const deleteOldRdn = true const deleteOldRdn = true
const req = new ModifyDNRequest({
const req = new ModifyDnRequest({ entry: entry,
entry, deleteOldRdn: deleteOldRdn,
deleteOldRdn, controls: []
newRdn,
newSuperior
}) })
req.newRdn = newRdn
t.equal(req.entry.toString(), 'cn=Test User,ou=A Long OU,ou=Another Long OU,ou=Another Long OU,dc=acompany,DC=io') req.newSuperior = newSuperior
t.equal(req.newRdn.toString(), 'cn=Test User') req._toBer(ber)
t.equal(req.deleteOldRdn, true) const reader = new BerReader(ber.buffer)
t.equal(req.newSuperior.toString(), 'ou=A New Long OU,ou=Another New Long OU,ou=An OU,dc=acompany,dc=io') t.equal(reader.readString(), entry)
t.equal(reader.readString(), newRdn)
t.equal(reader.readBoolean(), deleteOldRdn)
t.equal(reader.readByte(), 0x80)
reader.readLength()
t.equal(reader._len, newSuperior.length)
reader._buf[--reader._offset] = 0x4
t.equal(reader.readString(), newSuperior)
t.end() t.end()
}) })
tap.test('search basic', function (t) { tap.test('search basic', function (t) {
const { SearchResultEntry, SearchResultDone } = messages
t.context.client.search('cn=test, ' + SUFFIX, '(objectclass=*)', function (err, res) { t.context.client.search('cn=test, ' + SUFFIX, '(objectclass=*)', function (err, res) {
t.error(err) t.error(err)
t.ok(res) t.ok(res)
let gotEntry = 0 let gotEntry = 0
res.on('searchEntry', function (entry) { res.on('searchEntry', function (entry) {
t.ok(entry) t.ok(entry)
t.ok(entry instanceof SearchResultEntry) t.ok(entry instanceof ldap.SearchEntry)
t.equal(entry.dn.toString(), 'cn=test,' + SUFFIX) t.equal(entry.dn.toString(), 'cn=test,' + SUFFIX)
t.ok(entry.attributes) t.ok(entry.attributes)
t.ok(entry.attributes.length) t.ok(entry.attributes.length)
t.equal(entry.attributes[0].type, 'cn') t.equal(entry.attributes[0].type, 'cn')
t.equal(entry.attributes[1].type, 'SN') t.equal(entry.attributes[1].type, 'SN')
t.ok(entry.object)
gotEntry++ gotEntry++
}) })
res.on('error', function (err) { res.on('error', function (err) {
@ -754,37 +777,7 @@ tap.test('search basic', function (t) {
}) })
res.on('end', function (res) { res.on('end', function (res) {
t.ok(res) t.ok(res)
t.ok(res instanceof SearchResultDone) t.ok(res instanceof ldap.SearchResponse)
t.equal(res.status, 0)
t.equal(gotEntry, 2)
t.end()
})
})
})
tap.test('search basic with DN', function (t) {
const { SearchResultEntry, SearchResultDone } = messages
t.context.client.search(dn.DN.fromString('cn=test, ' + SUFFIX, '(objectclass=*)'), function (err, res) {
t.error(err)
t.ok(res)
let gotEntry = 0
res.on('searchEntry', function (entry) {
t.ok(entry)
t.ok(entry instanceof SearchResultEntry)
t.equal(entry.dn.toString(), 'cn=test,' + SUFFIX)
t.ok(entry.attributes)
t.ok(entry.attributes.length)
t.equal(entry.attributes[0].type, 'cn')
t.equal(entry.attributes[1].type, 'SN')
gotEntry++
})
res.on('error', function (err) {
t.fail(err)
})
res.on('end', function (res) {
t.ok(res)
t.ok(res instanceof SearchResultDone)
t.equal(res.status, 0) t.equal(res.status, 0)
t.equal(gotEntry, 2) t.equal(gotEntry, 2)
t.end() t.end()
@ -851,7 +844,7 @@ tap.test('search paged', { timeout: 10000 }, function (t) {
t2.error(err) t2.error(err)
res.on('searchEntry', entryListener) res.on('searchEntry', entryListener)
res.on('searchRequest', (searchRequest) => { res.on('searchRequest', (searchRequest) => {
t2.ok(searchRequest instanceof SearchRequest) t2.ok(searchRequest instanceof ldap.SearchRequest)
if (currentSearchRequest === null) { if (currentSearchRequest === null) {
t2.equal(countPages, 0) t2.equal(countPages, 0)
} }
@ -862,7 +855,7 @@ tap.test('search paged', { timeout: 10000 }, function (t) {
res.on('end', function (result) { res.on('end', function (result) {
t2.equal(countEntries, 1000) t2.equal(countEntries, 1000)
t2.equal(countPages, 10) t2.equal(countPages, 10)
t2.equal(result.messageId, currentSearchRequest.messageId) t2.equal(result.messageID, currentSearchRequest.messageID)
t2.end() t2.end()
}) })
@ -878,7 +871,7 @@ tap.test('search paged', { timeout: 10000 }, function (t) {
function pageListener (result) { function pageListener (result) {
countPages += 1 countPages += 1
if (countPages < 10) { if (countPages < 10) {
t2.equal(result.messageId, currentSearchRequest.messageId) t2.equal(result.messageID, currentSearchRequest.messageID)
} }
} }
}) })
@ -1002,12 +995,7 @@ tap.test('search paged', { timeout: 10000 }, function (t) {
t.end() t.end()
}) })
// We are skipping the ServerSideSorting test because we have skipped tap.test('search - sssvlv', { timeout: 10000 }, function (t) {
// properly implementing the controls in order to get v3 shipped. These
// tests should be re-enabled once we have addressed this issue.
// ~ jsumners 2023-02-19
// TODO: re-enable after adding back SSSR support
tap.test('search - sssvlv', { timeout: 10000, skip: true }, function (t) {
t.test('ssv - asc', function (t2) { t.test('ssv - asc', function (t2) {
let preventry = null let preventry = null
const sssrcontrol = new ldap.ServerSideSortingRequestControl( const sssrcontrol = new ldap.ServerSideSortingRequestControl(
@ -1037,7 +1025,6 @@ tap.test('search - sssvlv', { timeout: 10000, skip: true }, function (t) {
}) })
}) })
}) })
t.test('ssv - desc', function (t2) { t.test('ssv - desc', function (t2) {
let preventry = null let preventry = null
const sssrcontrol = new ldap.ServerSideSortingRequestControl( const sssrcontrol = new ldap.ServerSideSortingRequestControl(
@ -1068,9 +1055,7 @@ tap.test('search - sssvlv', { timeout: 10000, skip: true }, function (t) {
}) })
}) })
t.test('vlv - first page', { skip: true }, function (t2) { t.test('vlv - first page', function (t2) {
// This test is disabled.
// See https://github.com/ldapjs/node-ldapjs/pull/797#issuecomment-1094132289
const sssrcontrol = new ldap.ServerSideSortingRequestControl( const sssrcontrol = new ldap.ServerSideSortingRequestControl(
{ {
value: { value: {
@ -1112,10 +1097,7 @@ tap.test('search - sssvlv', { timeout: 10000, skip: true }, function (t) {
}) })
}) })
}) })
t.test('vlv - last page', function (t2) {
t.test('vlv - last page', { skip: true }, function (t2) {
// This test is disabled.
// See https://github.com/ldapjs/node-ldapjs/pull/797#issuecomment-1094132289
const sssrcontrol = new ldap.ServerSideSortingRequestControl( const sssrcontrol = new ldap.ServerSideSortingRequestControl(
{ {
value: { value: {
@ -1157,7 +1139,6 @@ tap.test('search - sssvlv', { timeout: 10000, skip: true }, function (t) {
}) })
}) })
}) })
t.end() t.end()
}) })
@ -1173,7 +1154,7 @@ tap.test('search referral', function (t) {
res.on('searchReference', function (referral) { res.on('searchReference', function (referral) {
gotReferral = true gotReferral = true
t.ok(referral) t.ok(referral)
t.ok(referral instanceof SearchResultReference) t.ok(referral instanceof ldap.SearchReference)
t.ok(referral.uris) t.ok(referral.uris)
t.ok(referral.uris.length) t.ok(referral.uris.length)
}) })
@ -1182,7 +1163,7 @@ tap.test('search referral', function (t) {
}) })
res.on('end', function (res) { res.on('end', function (res) {
t.ok(res) t.ok(res)
t.ok(res instanceof SearchResultDone) t.ok(res instanceof ldap.SearchResponse)
t.equal(res.status, 0) t.equal(res.status, 0)
t.equal(gotEntry, 0) t.equal(gotEntry, 0)
t.ok(gotReferral) t.ok(gotReferral)
@ -1199,13 +1180,14 @@ tap.test('search rootDSE', function (t) {
t.ok(entry) t.ok(entry)
t.equal(entry.dn.toString(), '') t.equal(entry.dn.toString(), '')
t.ok(entry.attributes) t.ok(entry.attributes)
t.ok(entry.object)
}) })
res.on('error', function (err) { res.on('error', function (err) {
t.fail(err) t.fail(err)
}) })
res.on('end', function (res) { res.on('end', function (res) {
t.ok(res) t.ok(res)
t.ok(res instanceof SearchResultDone) t.ok(res instanceof ldap.SearchResponse)
t.equal(res.status, 0) t.equal(res.status, 0)
t.end() t.end()
}) })
@ -1218,16 +1200,12 @@ tap.test('search empty attribute', function (t) {
t.ok(res) t.ok(res)
let gotEntry = 0 let gotEntry = 0
res.on('searchEntry', function (entry) { res.on('searchEntry', function (entry) {
const obj = entry.pojo const obj = entry.toObject()
t.equal('dc=empty', obj.objectName) t.equal('dc=empty', obj.dn)
t.ok(obj.member)
const member = entry.attributes[0] t.equal(obj.member.length, 0)
t.ok(member) t.ok(obj['member;range=0-1'])
t.equal(member.values.length, 0) t.ok(obj['member;range=0-1'].length)
const rangedMember = entry.attributes[1]
t.equal(rangedMember.type, 'member;range=0-1')
t.equal(rangedMember.values.length, 2)
gotEntry++ gotEntry++
}) })
res.on('error', function (err) { res.on('error', function (err) {
@ -1235,7 +1213,7 @@ tap.test('search empty attribute', function (t) {
}) })
res.on('end', function (res) { res.on('end', function (res) {
t.ok(res) t.ok(res)
t.ok(res instanceof SearchResultDone) t.ok(res instanceof ldap.SearchResponse)
t.equal(res.status, 0) t.equal(res.status, 0)
t.equal(gotEntry, 1) t.equal(gotEntry, 1)
t.end() t.end()
@ -1252,12 +1230,12 @@ tap.test('GH-21 binary attributes', function (t) {
const expect2 = Buffer.from([0xB5, 0xE7, 0xCA, 0xD3, 0xBB, 0xFA]) const expect2 = Buffer.from([0xB5, 0xE7, 0xCA, 0xD3, 0xBB, 0xFA])
res.on('searchEntry', function (entry) { res.on('searchEntry', function (entry) {
t.ok(entry) t.ok(entry)
t.ok(entry instanceof SearchResultEntry) t.ok(entry instanceof ldap.SearchEntry)
t.equal(entry.dn.toString(), 'cn=bin,' + SUFFIX) t.equal(entry.dn.toString(), 'cn=bin,' + SUFFIX)
t.ok(entry.attributes) t.ok(entry.attributes)
t.ok(entry.attributes.length) t.ok(entry.attributes.length)
t.equal(entry.attributes[0].type, 'foo;binary') t.equal(entry.attributes[0].type, 'foo;binary')
t.equal(entry.attributes[0].values[0], expect.toString('base64')) t.equal(entry.attributes[0].vals[0], expect.toString('base64'))
t.equal(entry.attributes[0].buffers[0].toString('base64'), t.equal(entry.attributes[0].buffers[0].toString('base64'),
expect.toString('base64')) expect.toString('base64'))
@ -1266,6 +1244,7 @@ tap.test('GH-21 binary attributes', function (t) {
t.equal(expect2.length, entry.attributes[1].buffers[0].length) t.equal(expect2.length, entry.attributes[1].buffers[0].length)
for (let i = 0; i < expect2.length; i++) { t.equal(expect2[i], entry.attributes[1].buffers[0][i]) } for (let i = 0; i < expect2.length; i++) { t.equal(expect2[i], entry.attributes[1].buffers[0][i]) }
t.ok(entry.object)
gotEntry++ gotEntry++
}) })
res.on('error', function (err) { res.on('error', function (err) {
@ -1273,7 +1252,7 @@ tap.test('GH-21 binary attributes', function (t) {
}) })
res.on('end', function (res) { res.on('end', function (res) {
t.ok(res) t.ok(res)
t.ok(res instanceof SearchResultDone) t.ok(res instanceof ldap.SearchResponse)
t.equal(res.status, 0) t.equal(res.status, 0)
t.equal(gotEntry, 1) t.equal(gotEntry, 1)
t.end() t.end()
@ -1292,11 +1271,12 @@ tap.test('GH-23 case insensitive attribute filtering', function (t) {
let gotEntry = 0 let gotEntry = 0
res.on('searchEntry', function (entry) { res.on('searchEntry', function (entry) {
t.ok(entry) t.ok(entry)
t.ok(entry instanceof SearchResultEntry) t.ok(entry instanceof ldap.SearchEntry)
t.equal(entry.dn.toString(), 'cn=test,' + SUFFIX) t.equal(entry.dn.toString(), 'cn=test,' + SUFFIX)
t.ok(entry.attributes) t.ok(entry.attributes)
t.ok(entry.attributes.length) t.ok(entry.attributes.length)
t.equal(entry.attributes[0].type, 'cn') t.equal(entry.attributes[0].type, 'cn')
t.ok(entry.object)
gotEntry++ gotEntry++
}) })
res.on('error', function (err) { res.on('error', function (err) {
@ -1304,7 +1284,7 @@ tap.test('GH-23 case insensitive attribute filtering', function (t) {
}) })
res.on('end', function (res) { res.on('end', function (res) {
t.ok(res) t.ok(res)
t.ok(res instanceof SearchResultDone) t.ok(res instanceof ldap.SearchResponse)
t.equal(res.status, 0) t.equal(res.status, 0)
t.equal(gotEntry, 2) t.equal(gotEntry, 2)
t.end() t.end()
@ -1323,12 +1303,13 @@ tap.test('GH-24 attribute selection of *', function (t) {
let gotEntry = 0 let gotEntry = 0
res.on('searchEntry', function (entry) { res.on('searchEntry', function (entry) {
t.ok(entry) t.ok(entry)
t.ok(entry instanceof SearchResultEntry) t.ok(entry instanceof ldap.SearchEntry)
t.equal(entry.dn.toString(), 'cn=test,' + SUFFIX) t.equal(entry.dn.toString(), 'cn=test,' + SUFFIX)
t.ok(entry.attributes) t.ok(entry.attributes)
t.ok(entry.attributes.length) t.ok(entry.attributes.length)
t.equal(entry.attributes[0].type, 'cn') t.equal(entry.attributes[0].type, 'cn')
t.equal(entry.attributes[1].type, 'SN') t.equal(entry.attributes[1].type, 'SN')
t.ok(entry.object)
gotEntry++ gotEntry++
}) })
res.on('error', function (err) { res.on('error', function (err) {
@ -1336,7 +1317,7 @@ tap.test('GH-24 attribute selection of *', function (t) {
}) })
res.on('end', function (res) { res.on('end', function (res) {
t.ok(res) t.ok(res)
t.ok(res instanceof SearchResultDone) t.ok(res instanceof ldap.SearchResponse)
t.equal(res.status, 0) t.equal(res.status, 0)
t.equal(gotEntry, 2) t.equal(gotEntry, 2)
t.end() t.end()
@ -1673,7 +1654,7 @@ tap.test('connection timeout', function (t) {
}) })
}) })
tap.test('emitError', function (t) { tap.only('emitError', function (t) {
t.test('connectTimeout', function (t) { t.test('connectTimeout', function (t) {
getPort().then(function (unusedPortNumber) { getPort().then(function (unusedPortNumber) {
const client = ldap.createClient({ const client = ldap.createClient({

View File

@ -1,7 +1,7 @@
'use strict' 'use strict'
const { test } = require('tap') const { test } = require('tap')
const { BerReader, BerWriter } = require('@ldapjs/asn1') const { BerReader, BerWriter } = require('asn1')
const { Control, getControl } = require('../../lib') const { Control, getControl } = require('../../lib')
test('new no args', function (t) { test('new no args', function (t) {

View File

@ -0,0 +1,66 @@
'use strict'
const { test } = require('tap')
const { BerReader, BerWriter } = require('asn1')
const { getControl, EntryChangeNotificationControl } = require('../../lib')
test('new no args', function (t) {
t.ok(new EntryChangeNotificationControl())
t.end()
})
test('new with args', function (t) {
const c = new EntryChangeNotificationControl({
type: '2.16.840.1.113730.3.4.7',
criticality: true,
value: {
changeType: 8,
previousDN: 'cn=foobarbazcar',
changeNumber: 123456789
}
})
t.ok(c)
t.equal(c.type, '2.16.840.1.113730.3.4.7')
t.ok(c.criticality)
t.equal(c.value.changeType, 8)
t.equal(c.value.previousDN, 'cn=foobarbazcar')
t.equal(c.value.changeNumber, 123456789)
const writer = new BerWriter()
c.toBer(writer)
const reader = new BerReader(writer.buffer)
const psc = getControl(reader)
t.ok(psc)
t.equal(psc.type, '2.16.840.1.113730.3.4.7')
t.ok(psc.criticality)
t.equal(psc.value.changeType, 8)
t.equal(psc.value.previousDN, 'cn=foobarbazcar')
t.equal(psc.value.changeNumber, 123456789)
t.end()
})
test('tober', function (t) {
const psc = new EntryChangeNotificationControl({
type: '2.16.840.1.113730.3.4.7',
criticality: true,
value: {
changeType: 8,
previousDN: 'cn=foobarbazcar',
changeNumber: 123456789
}
})
const ber = new BerWriter()
psc.toBer(ber)
const c = getControl(new BerReader(ber.buffer))
t.ok(c)
t.equal(c.type, '2.16.840.1.113730.3.4.7')
t.ok(c.criticality)
t.equal(c.value.changeType, 8)
t.equal(c.value.previousDN, 'cn=foobarbazcar')
t.equal(c.value.changeNumber, 123456789)
t.end()
})

View File

@ -0,0 +1,61 @@
'use strict'
const { test } = require('tap')
const { BerReader, BerWriter } = require('asn1')
const { getControl, PagedResultsControl } = require('../../lib')
test('new no args', function (t) {
t.ok(new PagedResultsControl())
t.end()
})
test('new with args', function (t) {
const c = new PagedResultsControl({
type: '1.2.840.113556.1.4.319',
criticality: true,
value: {
size: 1000,
cookie: Buffer.from([1, 2, 3])
}
})
t.ok(c)
t.equal(c.type, '1.2.840.113556.1.4.319')
t.ok(c.criticality)
t.equal(c.value.size, 1000)
t.equal(Buffer.compare(c.value.cookie, Buffer.from([1, 2, 3])), 0)
const writer = new BerWriter()
c.toBer(writer)
const reader = new BerReader(writer.buffer)
const psc = getControl(reader)
t.ok(psc)
t.equal(psc.type, '1.2.840.113556.1.4.319')
t.ok(psc.criticality)
t.equal(psc.value.size, 1000)
t.equal(Buffer.compare(psc.value.cookie, Buffer.from([1, 2, 3])), 0)
t.end()
})
test('tober', function (t) {
const psc = new PagedResultsControl({
type: '1.2.840.113556.1.4.319',
criticality: true,
value: {
size: 20,
cookie: Buffer.alloc(0)
}
})
const ber = new BerWriter()
psc.toBer(ber)
const c = getControl(new BerReader(ber.buffer))
t.ok(c)
t.equal(c.type, '1.2.840.113556.1.4.319')
t.ok(c.criticality)
t.equal(c.value.size, 20)
t.equal(Buffer.compare(c.value.cookie, Buffer.alloc(0)), 0)
t.end()
})

View File

@ -0,0 +1,99 @@
// Copyright 2011 Mark Cavage, Inc. All rights reserved.
const test = require('tap').test
const asn1 = require('asn1')
const BerReader = asn1.BerReader
const BerWriter = asn1.BerWriter
let getControl
let PersistentSearchControl
/// --- Tests
test('load library', function (t) {
PersistentSearchControl = require('../../lib').PersistentSearchControl
t.ok(PersistentSearchControl)
getControl = require('../../lib').getControl
t.ok(getControl)
t.end()
})
test('new no args', function (t) {
t.ok(new PersistentSearchControl())
t.end()
})
test('new with args', function (t) {
const c = new PersistentSearchControl({
type: '2.16.840.1.113730.3.4.3',
criticality: true,
value: {
changeTypes: 15,
changesOnly: false,
returnECs: false
}
})
t.ok(c)
t.equal(c.type, '2.16.840.1.113730.3.4.3')
t.ok(c.criticality)
t.equal(c.value.changeTypes, 15)
t.equal(c.value.changesOnly, false)
t.equal(c.value.returnECs, false)
const writer = new BerWriter()
c.toBer(writer)
const reader = new BerReader(writer.buffer)
const psc = getControl(reader)
t.ok(psc)
t.equal(psc.type, '2.16.840.1.113730.3.4.3')
t.ok(psc.criticality)
t.equal(psc.value.changeTypes, 15)
t.equal(psc.value.changesOnly, false)
t.equal(psc.value.returnECs, false)
t.end()
})
test('getControl with args', function (t) {
const buf = Buffer.from([
0x30, 0x26, 0x04, 0x17, 0x32, 0x2e, 0x31, 0x36, 0x2e, 0x38, 0x34, 0x30,
0x2e, 0x31, 0x2e, 0x31, 0x31, 0x33, 0x37, 0x33, 0x30, 0x2e, 0x33, 0x2e,
0x34, 0x2e, 0x33, 0x04, 0x0b, 0x30, 0x09, 0x02, 0x01, 0x0f, 0x01, 0x01,
0xff, 0x01, 0x01, 0xff])
const ber = new BerReader(buf)
const psc = getControl(ber)
t.ok(psc)
t.equal(psc.type, '2.16.840.1.113730.3.4.3')
t.equal(psc.criticality, false)
t.equal(psc.value.changeTypes, 15)
t.equal(psc.value.changesOnly, true)
t.equal(psc.value.returnECs, true)
t.end()
})
test('tober', function (t) {
const psc = new PersistentSearchControl({
type: '2.16.840.1.113730.3.4.3',
criticality: true,
value: {
changeTypes: 15,
changesOnly: false,
returnECs: false
}
})
const ber = new BerWriter()
psc.toBer(ber)
const c = getControl(new BerReader(ber.buffer))
t.ok(c)
t.equal(c.type, '2.16.840.1.113730.3.4.3')
t.ok(c.criticality)
t.equal(c.value.changeTypes, 15)
t.equal(c.value.changesOnly, false)
t.equal(c.value.returnECs, false)
t.end()
})

View File

@ -0,0 +1,95 @@
'use strict'
const { test } = require('tap')
const { BerReader, BerWriter } = require('asn1')
const { getControl, ServerSideSortingRequestControl: SSSRControl } = require('../../lib')
test('new no args', function (t) {
t.ok(new SSSRControl())
t.end()
})
test('new with args', function (t) {
const c = new SSSRControl({
criticality: true,
value: {
attributeType: 'sn'
}
})
t.ok(c)
t.equal(c.type, '1.2.840.113556.1.4.473')
t.ok(c.criticality)
t.equal(c.value.length, 1)
t.equal(c.value[0].attributeType, 'sn')
t.end()
})
test('toBer - object', function (t) {
const sssc = new SSSRControl({
criticality: true,
value: {
attributeType: 'sn',
orderingRule: 'caseIgnoreOrderingMatch',
reverseOrder: true
}
})
const ber = new BerWriter()
sssc.toBer(ber)
const c = getControl(new BerReader(ber.buffer))
t.ok(c)
t.equal(c.type, '1.2.840.113556.1.4.473')
t.ok(c.criticality)
t.equal(c.value[0].attributeType, 'sn')
t.equal(c.value[0].orderingRule, 'caseIgnoreOrderingMatch')
t.equal(c.value[0].reverseOrder, true)
t.end()
})
test('toBer - array', function (t) {
const sssc = new SSSRControl({
criticality: true,
value: [
{
attributeType: 'sn',
orderingRule: 'caseIgnoreOrderingMatch',
reverseOrder: true
},
{
attributeType: 'givenName',
orderingRule: 'caseIgnoreOrderingMatch'
}
]
})
const ber = new BerWriter()
sssc.toBer(ber)
const c = getControl(new BerReader(ber.buffer))
t.ok(c)
t.equal(c.type, '1.2.840.113556.1.4.473')
t.ok(c.criticality)
t.equal(c.value.length, 2)
t.equal(c.value[0].attributeType, 'sn')
t.equal(c.value[0].orderingRule, 'caseIgnoreOrderingMatch')
t.equal(c.value[0].reverseOrder, true)
t.equal(c.value[1].attributeType, 'givenName')
t.equal(c.value[1].orderingRule, 'caseIgnoreOrderingMatch')
t.end()
})
test('toBer - empty', function (t) {
const sssc = new SSSRControl()
const ber = new BerWriter()
sssc.toBer(ber)
const c = getControl(new BerReader(ber.buffer))
t.ok(c)
t.equal(c.type, '1.2.840.113556.1.4.473')
t.equal(c.value.length, 0)
t.end()
})

View File

@ -0,0 +1,105 @@
'use strict'
const { test } = require('tap')
const { BerReader, BerWriter } = require('asn1')
const ldap = require('../../lib')
const { getControl, ServerSideSortingResponseControl: SSSResponseControl } = ldap
const OID = '1.2.840.113556.1.4.474'
test('new no args', function (t) {
const c = new SSSResponseControl()
t.ok(c)
t.equal(c.type, OID)
t.equal(c.criticality, false)
t.end()
})
test('new with args', function (t) {
const c = new SSSResponseControl({
criticality: true,
value: {
result: ldap.LDAP_SUCCESS,
failedAttribute: 'cn'
}
})
t.ok(c)
t.equal(c.type, OID)
t.equal(c.criticality, false)
t.equal(c.value.result, ldap.LDAP_SUCCESS)
t.equal(c.value.failedAttribute, 'cn')
t.end()
})
test('toBer - success', function (t) {
const sssc = new SSSResponseControl({
value: {
result: ldap.LDAP_SUCCESS,
failedAttribute: 'foobar'
}
})
const ber = new BerWriter()
sssc.toBer(ber)
const c = getControl(new BerReader(ber.buffer))
t.ok(c)
t.equal(c.type, '1.2.840.113556.1.4.474')
t.equal(c.criticality, false)
t.equal(c.value.result, ldap.LDAP_SUCCESS)
t.notOk(c.value.failedAttribute)
t.end()
})
test('toBer - simple failure', function (t) {
const sssc = new SSSResponseControl({
value: {
result: ldap.LDAP_NO_SUCH_ATTRIBUTE
}
})
const ber = new BerWriter()
sssc.toBer(ber)
const c = getControl(new BerReader(ber.buffer))
t.ok(c)
t.equal(c.type, OID)
t.equal(c.criticality, false)
t.equal(c.value.result, ldap.LDAP_NO_SUCH_ATTRIBUTE)
t.notOk(c.value.failedAttribute)
t.end()
})
test('toBer - detailed failure', function (t) {
const sssc = new SSSResponseControl({
value: {
result: ldap.LDAP_NO_SUCH_ATTRIBUTE,
failedAttribute: 'foobar'
}
})
const ber = new BerWriter()
sssc.toBer(ber)
const c = getControl(new BerReader(ber.buffer))
t.ok(c)
t.equal(c.type, OID)
t.equal(c.criticality, false)
t.equal(c.value.result, ldap.LDAP_NO_SUCH_ATTRIBUTE)
t.equal(c.value.failedAttribute, 'foobar')
t.end()
})
test('toBer - empty', function (t) {
const sssc = new SSSResponseControl()
const ber = new BerWriter()
sssc.toBer(ber)
const c = getControl(new BerReader(ber.buffer))
t.ok(c)
t.equal(c.type, OID)
t.equal(c.criticality, false)
t.notOk(c.value.result)
t.notOk(c.value.failedAttribute)
t.end()
})

View File

@ -0,0 +1,94 @@
'use strict'
const { test } = require('tap')
const { BerReader, BerWriter } = require('asn1')
const { getControl, VirtualListViewRequestControl: VLVRControl } = require('../../lib')
test('VLV request - new no args', function (t) {
t.ok(new VLVRControl())
t.end()
})
test('VLV request - new with args', function (t) {
const c = new VLVRControl({
criticality: true,
value: {
beforeCount: 0,
afterCount: 3,
targetOffset: 1,
contentCount: 0
}
})
t.ok(c)
t.equal(c.type, '2.16.840.1.113730.3.4.9')
t.ok(c.criticality)
t.equal(c.value.beforeCount, 0)
t.equal(c.value.afterCount, 3)
t.equal(c.value.targetOffset, 1)
t.equal(c.value.contentCount, 0)
t.end()
})
test('VLV request - toBer - with offset', function (t) {
const vlvc = new VLVRControl({
criticality: true,
value: {
beforeCount: 0,
afterCount: 3,
targetOffset: 1,
contentCount: 0
}
})
const ber = new BerWriter()
vlvc.toBer(ber)
const c = getControl(new BerReader(ber.buffer))
t.ok(c)
t.equal(c.type, '2.16.840.1.113730.3.4.9')
t.ok(c.criticality)
t.equal(c.value.beforeCount, 0)
t.equal(c.value.afterCount, 3)
t.equal(c.value.targetOffset, 1)
t.equal(c.value.contentCount, 0)
t.end()
})
test('VLV request - toBer - with assertion', function (t) {
const vlvc = new VLVRControl({
criticality: true,
value: {
beforeCount: 0,
afterCount: 3,
greaterThanOrEqual: '*foo*'
}
})
const ber = new BerWriter()
vlvc.toBer(ber)
const c = getControl(new BerReader(ber.buffer))
t.ok(c)
t.equal(c.type, '2.16.840.1.113730.3.4.9')
t.ok(c.criticality)
t.equal(c.value.beforeCount, 0)
t.equal(c.value.afterCount, 3)
t.equal(c.value.greaterThanOrEqual, '*foo*')
t.end()
})
test('VLV request - toBer - empty', function (t) {
const vlvc = new VLVRControl()
const ber = new BerWriter()
vlvc.toBer(ber)
const c = getControl(new BerReader(ber.buffer))
t.ok(c)
t.equal(c.type, '2.16.840.1.113730.3.4.9')
t.equal(c.criticality, false)
t.notOk(c.value.result)
t.end()
})

View File

@ -0,0 +1,68 @@
'use strict'
const { test } = require('tap')
const { BerReader, BerWriter } = require('asn1')
const ldap = require('../../lib')
const { getControl, VirtualListViewResponseControl: VLVResponseControl } = require('../../lib')
const OID = '2.16.840.1.113730.3.4.10'
test('VLV response - new no args', function (t) {
const c = new VLVResponseControl()
t.ok(c)
t.equal(c.type, OID)
t.equal(c.criticality, false)
t.end()
})
test('VLV response - new with args', function (t) {
const c = new VLVResponseControl({
criticality: true,
value: {
result: ldap.LDAP_SUCCESS,
targetPosition: 0,
contentCount: 10
}
})
t.ok(c)
t.equal(c.type, OID)
t.equal(c.criticality, false)
t.equal(c.value.result, ldap.LDAP_SUCCESS)
t.equal(c.value.targetPosition, 0)
t.equal(c.value.contentCount, 10)
t.end()
})
test('VLV response - toBer', function (t) {
const vlpc = new VLVResponseControl({
value: {
targetPosition: 0,
contentCount: 10,
result: ldap.LDAP_SUCCESS
}
})
const ber = new BerWriter()
vlpc.toBer(ber)
const c = getControl(new BerReader(ber.buffer))
t.ok(c)
t.equal(c.type, OID)
t.equal(c.criticality, false)
t.equal(c.value.result, ldap.LDAP_SUCCESS)
t.equal(c.value.targetPosition, 0)
t.equal(c.value.contentCount, 10)
t.end()
})
test('VLV response - toBer - empty', function (t) {
const vlpc = new VLVResponseControl()
const ber = new BerWriter()
vlpc.toBer(ber)
const c = getControl(new BerReader(ber.buffer))
t.ok(c)
t.equal(c.type, OID)
t.equal(c.criticality, false)
t.notOk(c.value.result)
t.end()
})

234
test/dn.test.js Normal file
View File

@ -0,0 +1,234 @@
'use strict'
const { test } = require('tap')
const { dn } = require('../lib')
test('parse basic', function (t) {
const DN_STR = 'cn=mark, ou=people, o=joyent'
const name = dn.parse(DN_STR)
t.ok(name)
t.ok(name.rdns)
t.ok(Array.isArray(name.rdns))
t.equal(3, name.rdns.length)
name.rdns.forEach(function (rdn) {
t.equal('object', typeof (rdn))
})
t.equal(name.toString(), DN_STR)
t.end()
})
test('parse escaped', function (t) {
const DN_STR = 'cn=m\\,ark, ou=people, o=joyent'
const name = dn.parse(DN_STR)
t.ok(name)
t.ok(name.rdns)
t.ok(Array.isArray(name.rdns))
t.equal(3, name.rdns.length)
name.rdns.forEach(function (rdn) {
t.equal('object', typeof (rdn))
})
t.equal(name.toString(), DN_STR)
t.end()
})
test('parse compound', function (t) {
const DN_STR = 'cn=mark+sn=cavage, ou=people, o=joyent'
const name = dn.parse(DN_STR)
t.ok(name)
t.ok(name.rdns)
t.ok(Array.isArray(name.rdns))
t.equal(3, name.rdns.length)
name.rdns.forEach(function (rdn) {
t.equal('object', typeof (rdn))
})
t.equal(name.toString(), DN_STR)
t.end()
})
test('parse quoted', function (t) {
const DN_STR = 'cn="mark+sn=cavage", ou=people, o=joyent'
const ESCAPE_STR = 'cn=mark\\+sn\\=cavage, ou=people, o=joyent'
const name = dn.parse(DN_STR)
t.ok(name)
t.ok(name.rdns)
t.ok(Array.isArray(name.rdns))
t.equal(3, name.rdns.length)
name.rdns.forEach(function (rdn) {
t.equal('object', typeof (rdn))
})
t.equal(name.toString(), ESCAPE_STR)
t.end()
})
test('equals', function (t) {
const dn1 = dn.parse('cn=foo,dc=bar')
t.ok(dn1.equals('cn=foo,dc=bar'))
t.ok(!dn1.equals('cn=foo1,dc=bar'))
t.ok(dn1.equals(dn.parse('cn=foo,dc=bar')))
t.ok(!dn1.equals(dn.parse('cn=foo2,dc=bar')))
t.end()
})
test('child of', function (t) {
const dn1 = dn.parse('cn=foo,dc=bar')
t.ok(dn1.childOf('dc=bar'))
t.ok(!dn1.childOf('dc=moo'))
t.ok(!dn1.childOf('dc=foo'))
t.ok(!dn1.childOf('cn=foo,dc=bar'))
t.ok(dn1.childOf(dn.parse('dc=bar')))
t.end()
})
test('parent of', function (t) {
const dn1 = dn.parse('cn=foo,dc=bar')
t.ok(dn1.parentOf('cn=moo,cn=foo,dc=bar'))
t.ok(!dn1.parentOf('cn=moo,cn=bar,dc=foo'))
t.ok(!dn1.parentOf('cn=foo,dc=bar'))
t.ok(dn1.parentOf(dn.parse('cn=moo,cn=foo,dc=bar')))
t.end()
})
test('DN parent', function (t) {
const _dn = dn.parse('cn=foo,ou=bar')
const parent1 = _dn.parent()
const parent2 = parent1.parent()
t.ok(parent1.equals('ou=bar'))
t.ok(parent2.equals(''))
t.equal(parent2.parent(), null)
t.end()
})
test('empty DNs', function (t) {
const _dn = dn.parse('')
const _dn2 = dn.parse('cn=foo')
t.ok(_dn.isEmpty())
t.notOk(_dn2.isEmpty())
t.notOk(_dn.equals('cn=foo'))
t.notOk(_dn2.equals(''))
t.ok(_dn.parentOf('cn=foo'))
t.notOk(_dn.childOf('cn=foo'))
t.notOk(_dn2.parentOf(''))
t.ok(_dn2.childOf(''))
t.end()
})
test('case insensitive attribute names', function (t) {
const dn1 = dn.parse('CN=foo,dc=bar')
t.ok(dn1.equals('cn=foo,dc=bar'))
t.ok(dn1.equals(dn.parse('cn=foo,DC=bar')))
t.end()
})
test('format', function (t) {
const DN_ORDER = dn.parse('sn=bar+cn=foo,ou=test')
const DN_QUOTE = dn.parse('cn="foo",ou=test')
const DN_QUOTE2 = dn.parse('cn=" foo",ou=test')
const DN_SPACE = dn.parse('cn=foo,ou=test')
const DN_SPACE2 = dn.parse('cn=foo ,ou=test')
const DN_CASE = dn.parse('CN=foo,Ou=test')
t.equal(DN_ORDER.format({ keepOrder: false }), 'cn=foo+sn=bar, ou=test')
t.equal(DN_ORDER.format({ keepOrder: true }), 'sn=bar+cn=foo, ou=test')
t.equal(DN_QUOTE.format({ keepQuote: false }), 'cn=foo, ou=test')
t.equal(DN_QUOTE.format({ keepQuote: true }), 'cn="foo", ou=test')
t.equal(DN_QUOTE2.format({ keepQuote: false }), 'cn=" foo", ou=test')
t.equal(DN_QUOTE2.format({ keepQuote: true }), 'cn=" foo", ou=test')
t.equal(DN_SPACE.format({ keepSpace: false }), 'cn=foo, ou=test')
t.equal(DN_SPACE.format({ keepSpace: true }), 'cn=foo,ou=test')
t.equal(DN_SPACE.format({ skipSpace: true }), 'cn=foo,ou=test')
t.equal(DN_SPACE2.format({ keepSpace: false }), 'cn=foo, ou=test')
t.equal(DN_SPACE2.format({ keepSpace: true }), 'cn=foo ,ou=test')
t.equal(DN_SPACE2.format({ skipSpace: true }), 'cn=foo,ou=test')
t.equal(DN_CASE.format({ keepCase: false }), 'cn=foo, ou=test')
t.equal(DN_CASE.format({ keepCase: true }), 'CN=foo, Ou=test')
t.equal(DN_CASE.format({ upperName: true }), 'CN=foo, OU=test')
t.end()
})
test('set format', function (t) {
const _dn = dn.parse('uid="user", sn=bar+cn=foo, dc=test , DC=com')
t.equal(_dn.toString(), 'uid=user, cn=foo+sn=bar, dc=test, dc=com')
_dn.setFormat({ keepOrder: true })
t.equal(_dn.toString(), 'uid=user, sn=bar+cn=foo, dc=test, dc=com')
_dn.setFormat({ keepQuote: true })
t.equal(_dn.toString(), 'uid="user", cn=foo+sn=bar, dc=test, dc=com')
_dn.setFormat({ keepSpace: true })
t.equal(_dn.toString(), 'uid=user, cn=foo+sn=bar, dc=test , dc=com')
_dn.setFormat({ keepCase: true })
t.equal(_dn.toString(), 'uid=user, cn=foo+sn=bar, dc=test, DC=com')
_dn.setFormat({ upperName: true })
t.equal(_dn.toString(), 'UID=user, CN=foo+SN=bar, DC=test, DC=com')
t.end()
})
test('format persists across clone', function (t) {
const _dn = dn.parse('uid="user", sn=bar+cn=foo, dc=test , DC=com')
const OUT = 'UID="user", CN=foo+SN=bar, DC=test, DC=com'
_dn.setFormat({ keepQuote: true, upperName: true })
const clone = _dn.clone()
t.equal(_dn.toString(), OUT)
t.equal(clone.toString(), OUT)
t.end()
})
test('initialization', function (t) {
const dn1 = new dn.DN()
t.ok(dn1)
t.equal(dn1.toString(), '')
t.ok(dn1.isEmpty(), 'DN with no initializer defaults to null DN')
const data = [
new dn.RDN({ foo: 'bar' }),
new dn.RDN({ o: 'base' })
]
const dn2 = new dn.DN(data)
t.ok(dn2)
t.equal(dn2.toString(), 'foo=bar, o=base')
t.ok(!dn2.isEmpty())
t.end()
})
test('array functions', function (t) {
const dn1 = dn.parse('a=foo, b=bar, c=baz')
t.ok(dn1)
t.equal(dn1.toString(), 'a=foo, b=bar, c=baz')
t.ok(dn1.reverse())
t.equal(dn1.toString(), 'c=baz, b=bar, a=foo')
let rdn = dn1.pop()
t.ok(rdn)
t.equal(dn1.toString(), 'c=baz, b=bar')
t.ok(dn1.push(rdn))
t.equal(dn1.toString(), 'c=baz, b=bar, a=foo')
rdn = dn1.shift()
t.ok(rdn)
t.equal(dn1.toString(), 'b=bar, a=foo')
t.ok(dn1.unshift(rdn))
t.equal(dn1.toString(), 'c=baz, b=bar, a=foo')
t.end()
})
test('isDN duck-testing', function (t) {
const valid = dn.parse('cn=foo')
const isDN = dn.DN.isDN
t.notOk(isDN(null))
t.notOk(isDN('cn=foo'))
t.ok(isDN(valid))
const duck = {
rdns: [{ look: 'ma' }, { a: 'dn' }],
toString: function () { return 'look=ma, a=dn' }
}
t.ok(isDN(duck))
t.end()
})

54
test/filters/and.test.js Normal file
View File

@ -0,0 +1,54 @@
'use strict'
const { test } = require('tap')
const { filters: { EqualityFilter, AndFilter } } = require('../../lib')
test('Construct no args', function (t) {
t.ok(new AndFilter())
t.end()
})
test('Construct args', function (t) {
const f = new AndFilter()
f.addFilter(new EqualityFilter({
attribute: 'foo',
value: 'bar'
}))
f.addFilter(new EqualityFilter({
attribute: 'zig',
value: 'zag'
}))
t.ok(f)
t.equal(f.toString(), '(&(foo=bar)(zig=zag))')
t.end()
})
test('match true', function (t) {
const f = new AndFilter()
f.addFilter(new EqualityFilter({
attribute: 'foo',
value: 'bar'
}))
f.addFilter(new EqualityFilter({
attribute: 'zig',
value: 'zag'
}))
t.ok(f)
t.ok(f.matches({ foo: 'bar', zig: 'zag' }))
t.end()
})
test('match false', function (t) {
const f = new AndFilter()
f.addFilter(new EqualityFilter({
attribute: 'foo',
value: 'bar'
}))
f.addFilter(new EqualityFilter({
attribute: 'zig',
value: 'zag'
}))
t.ok(f)
t.ok(!f.matches({ foo: 'bar', zig: 'zonk' }))
t.end()
})

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