Virtual List View control support

This commit is contained in:
Soisik Froger 2019-09-03 22:25:14 +02:00
parent d6f58e6487
commit 311c94ef15
7 changed files with 587 additions and 1 deletions

View File

@ -12,6 +12,10 @@ var ServerSideSortingRequestControl =
require('./server_side_sorting_request_control.js')
var ServerSideSortingResponseControl =
require('./server_side_sorting_response_control.js')
var VirtualListViewRequestControl =
require('./virtual_list_view_request_control.js')
var VirtualListViewResponseControl =
require('./virtual_list_view_response_control.js')
/// --- API
@ -56,6 +60,12 @@ module.exports = {
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)
@ -70,5 +80,7 @@ module.exports = {
PagedResultsControl: PagedResultsControl,
PersistentSearchControl: PersistentSearchControl,
ServerSideSortingRequestControl: ServerSideSortingRequestControl,
ServerSideSortingResponseControl: ServerSideSortingResponseControl
ServerSideSortingResponseControl: ServerSideSortingResponseControl,
VirtualListViewRequestControl: VirtualListViewRequestControl,
VirtualListViewResponseControl: VirtualListViewResponseControl
}

View File

@ -0,0 +1,94 @@
var assert = require('assert-plus')
var util = require('util')
var asn1 = require('asn1')
var Control = require('./control')
/// --- Globals
var BerReader = asn1.BerReader
var 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)
var 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
}
var 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 @@
var assert = require('assert-plus')
var util = require('util')
var asn1 = require('asn1')
var Control = require('./control')
var CODES = require('../errors/codes')
/// --- Globals
var BerReader = asn1.BerReader
var BerWriter = asn1.BerWriter
var 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)
var 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
}
var 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

View File

@ -32,6 +32,8 @@ module.exports = {
LDAP_UNAVAILABLE: 52,
LDAP_UNWILLING_TO_PERFORM: 53,
LDAP_LOOP_DETECT: 54,
LDAP_SORT_CONTROL_MISSING: 60,
LDAP_INDEX_RANGE_ERROR: 61,
LDAP_NAMING_VIOLATION: 64,
LDAP_OBJECTCLASS_VIOLATION: 65,
LDAP_NOT_ALLOWED_ON_NON_LEAF: 66,
@ -39,6 +41,7 @@ module.exports = {
LDAP_ENTRY_ALREADY_EXISTS: 68,
LDAP_OBJECTCLASS_MODS_PROHIBITED: 69,
LDAP_AFFECTS_MULTIPLE_DSAS: 71,
LDAP_CONTROL_ERROR: 76,
LDAP_OTHER: 80,
LDAP_PROXIED_AUTHORIZATION_DENIED: 123
}

View File

@ -188,6 +188,62 @@ tap.beforeEach((done, t) => {
next(new ldap.UnwillingToPerformError())
}
})
server.search('cn=sssvlv', function (req, res, next) {
const min = 0
const max = 100
const results = []
let o = 'aa'
for (let i = min; i < max; i++) {
results.push({
dn: util.format('o=%s, cn=sssvlv', o),
attributes: {
o: [o],
objectclass: ['sssvlvResult']
}
})
o = ((parseInt(o, 36) + 1).toString(36)).replace(/0/g, 'a')
}
function sendResults (start, end, sortBy, sortDesc) {
start = (start < min) ? min : start
end = (end > max || end < min) ? max : end
const sorted = results.sort((a, b) => {
if (a.attributes[sortBy][0] < b.attributes[sortBy][0]) {
return sortDesc ? 1 : -1
} else if (a.attributes[sortBy][0] > b.attributes[sortBy][0]) {
return sortDesc ? -1 : 1
}
return 0
})
for (let i = start; i < end; i++) {
res.send(sorted[i])
}
}
let sortBy = null
let sortDesc = null
let afterCount = null
let targetOffset = null
req.controls.forEach(function (control) {
if (control.type === ldap.ServerSideSortingRequestControl.OID) {
sortBy = control.value[0].attributeType
sortDesc = control.value[0].reverseOrder
}
if (control.type === ldap.VirtualListViewRequestControl.OID) {
afterCount = control.value.afterCount
targetOffset = control.value.targetOffset
}
})
if (sortBy) {
if (afterCount && targetOffset) {
sendResults(targetOffset - 1, (targetOffset + afterCount), sortBy, sortDesc)
} else {
sendResults(min, max, sortBy, sortDesc)
}
res.end()
next()
} else {
next(new ldap.UnwillingToPerformError())
}
})
server.search('cn=pagederr', function (req, res, next) {
let cookie = null
@ -738,6 +794,153 @@ tap.test('search paged', { timeout: 10000 }, function (t) {
t.end()
})
tap.test('search - sssvlv', { timeout: 10000 }, function (t) {
t.test('ssv - asc', function (t2) {
let preventry = null
const sssrcontrol = new ldap.ServerSideSortingRequestControl(
{
value: {
attributeType: 'o',
orderingRule: 'caseIgnoreOrderingMatch',
reverseOrder: false
}
}
)
t.context.client.search('cn=sssvlv', {}, sssrcontrol, function (err, res) {
t2.error(err)
res.on('searchEntry', function (entry) {
t2.ok(entry)
t2.ok(entry instanceof ldap.SearchEntry)
t2.ok(entry.attributes)
t2.ok(entry.attributes.length)
if (preventry != null) {
t2.ok(entry.attributes[0]._vals[0] >= preventry.attributes[0]._vals[0])
}
preventry = entry
})
res.on('error', (err) => t2.error(err))
res.on('end', function () {
t2.end()
})
})
})
t.test('ssv - desc', function (t2) {
let preventry = null
const sssrcontrol = new ldap.ServerSideSortingRequestControl(
{
value: {
attributeType: 'o',
orderingRule: 'caseIgnoreOrderingMatch',
reverseOrder: true
}
}
)
t.context.client.search('cn=sssvlv', {}, sssrcontrol, function (err, res) {
t2.error(err)
res.on('searchEntry', function (entry) {
t2.ok(entry)
t2.ok(entry instanceof ldap.SearchEntry)
t2.ok(entry.attributes)
t2.ok(entry.attributes.length)
if (preventry != null) {
t2.ok(entry.attributes[0]._vals[0] <= preventry.attributes[0]._vals[0])
}
preventry = entry
})
res.on('error', (err) => t2.error(err))
res.on('end', function () {
t2.end()
})
})
})
t.test('vlv - first page', function (t2) {
const sssrcontrol = new ldap.ServerSideSortingRequestControl(
{
value: {
attributeType: 'o',
orderingRule: 'caseIgnoreOrderingMatch',
reverseOrder: false
}
}
)
const vlvrcontrol = new ldap.VirtualListViewRequestControl(
{
value: {
beforeCount: 0,
afterCount: 9,
targetOffset: 1,
contentCount: 0
}
}
)
let count = 0
let preventry = null
t.context.client.search('cn=sssvlv', {}, [sssrcontrol, vlvrcontrol], function (err, res) {
t2.error(err)
res.on('searchEntry', function (entry) {
t2.ok(entry)
t2.ok(entry instanceof ldap.SearchEntry)
t2.ok(entry.attributes)
t2.ok(entry.attributes.length)
if (preventry != null) {
t2.ok(entry.attributes[0]._vals[0] >= preventry.attributes[0]._vals[0])
}
preventry = entry
count++
})
res.on('error', (err) => t2.error(err))
res.on('end', function (result) {
t2.equals(count, 10)
t2.end()
})
})
})
t.test('vlv - last page', function (t2) {
const sssrcontrol = new ldap.ServerSideSortingRequestControl(
{
value: {
attributeType: 'o',
orderingRule: 'caseIgnoreOrderingMatch',
reverseOrder: false
}
}
)
const vlvrcontrol = new ldap.VirtualListViewRequestControl(
{
value: {
beforeCount: 0,
afterCount: 9,
targetOffset: 91,
contentCount: 0
}
}
)
let count = 0
let preventry = null
t.context.client.search('cn=sssvlv', {}, [sssrcontrol, vlvrcontrol], function (err, res) {
t2.error(err)
res.on('searchEntry', function (entry) {
t2.ok(entry)
t2.ok(entry instanceof ldap.SearchEntry)
t2.ok(entry.attributes)
t2.ok(entry.attributes.length)
if (preventry != null) {
t2.ok(entry.attributes[0]._vals[0] >= preventry.attributes[0]._vals[0])
}
preventry = entry
count++
})
res.on('error', (err) => t2.error(err))
res.on('end', function (result) {
t2.equals(count, 10)
t2.end()
})
})
})
t.end()
})
tap.test('search referral', function (t) {
t.context.client.search('cn=ref, ' + SUFFIX, '(objectclass=*)', function (err, res) {
t.error(err)

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()
})