From 311c94ef15d999231d4aa73d806b64735e33a14b Mon Sep 17 00:00:00 2001 From: Soisik Froger Date: Tue, 3 Sep 2019 22:25:14 +0200 Subject: [PATCH] Virtual List View control support --- lib/controls/index.js | 14 +- .../virtual_list_view_request_control.js | 94 ++++++++ .../virtual_list_view_response_control.js | 112 ++++++++++ lib/errors/codes.js | 3 + test/client.test.js | 203 ++++++++++++++++++ .../virtual_list_view_request_control.test.js | 94 ++++++++ ...virtual_list_view_response_control.test.js | 68 ++++++ 7 files changed, 587 insertions(+), 1 deletion(-) create mode 100644 lib/controls/virtual_list_view_request_control.js create mode 100644 lib/controls/virtual_list_view_response_control.js create mode 100644 test/controls/virtual_list_view_request_control.test.js create mode 100644 test/controls/virtual_list_view_response_control.test.js diff --git a/lib/controls/index.js b/lib/controls/index.js index 3ac11b2..dffc288 100644 --- a/lib/controls/index.js +++ b/lib/controls/index.js @@ -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 } diff --git a/lib/controls/virtual_list_view_request_control.js b/lib/controls/virtual_list_view_request_control.js new file mode 100644 index 0000000..b49dc58 --- /dev/null +++ b/lib/controls/virtual_list_view_request_control.js @@ -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 diff --git a/lib/controls/virtual_list_view_response_control.js b/lib/controls/virtual_list_view_response_control.js new file mode 100644 index 0000000..2dd66e1 --- /dev/null +++ b/lib/controls/virtual_list_view_response_control.js @@ -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 diff --git a/lib/errors/codes.js b/lib/errors/codes.js index c64b285..944398a 100644 --- a/lib/errors/codes.js +++ b/lib/errors/codes.js @@ -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 } diff --git a/test/client.test.js b/test/client.test.js index 3b84611..96b2159 100644 --- a/test/client.test.js +++ b/test/client.test.js @@ -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) diff --git a/test/controls/virtual_list_view_request_control.test.js b/test/controls/virtual_list_view_request_control.test.js new file mode 100644 index 0000000..bcb83c6 --- /dev/null +++ b/test/controls/virtual_list_view_request_control.test.js @@ -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() +}) diff --git a/test/controls/virtual_list_view_response_control.test.js b/test/controls/virtual_list_view_response_control.test.js new file mode 100644 index 0000000..1460959 --- /dev/null +++ b/test/controls/virtual_list_view_response_control.test.js @@ -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() +})