Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,7 @@ scope | String | One of base, one, or sub. Defaults to base
filter | String | A string version of an LDAP filter. Defaults to (objectclass=*)
attributes | Array of String | attributes to select and return. Defaults to the empty set, which means all attributes
sizeLimit | Number | the maximum number of entries to return. Defaults to 0 (unlimited)
pageSize | Number | Page size for paged search. If this attribute is set, search results are retrieved in pages. ActiveDirectory has a default limit of 1000 returned entries. If a larger number of results is expected, this attribute should be set
timeLimit | Number | the maximum amount of time the server should take in responding, in seconds. Defaults to 10. Lots of servers will ignore this
typesOnly | Boolean | on whether you want the server to only return the names of the attributes, and not their values. Borderline useless. Defaults to false

Expand Down
16 changes: 16 additions & 0 deletions __test__/client.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,22 @@ describe('Client', () => {
await client.destroy();
});

it('paged search', async () => {
expect.assertions(4);

const client = new Client({ url });

await client.bind(user, password);
const response = await client.search('ou=scientists,dc=example,dc=com', { scope: 'sub', pageSize: 1 });

expect(response.length).toBeGreaterThan(0);
expect(response[0].dn).toBeDefined();
expect(response[0].ou).toBe('scientists');
expect(response[0].objectClass.length).toBeGreaterThan(0);

await client.destroy();
});

xit('unbind', async () => {
expect.assertions(4);

Expand Down
6 changes: 3 additions & 3 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions src/attribute.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ const assert = require('assert-plus');
const asn1 = require('asn1');
const Protocol = require('./utils/protocol');

const _bufferEncoding = type => /;binary$/.test(type) ? 'base64' : 'utf8';
const _bufferEncoding = type => type.endsWith(';binary') ? 'base64' : 'utf8';

class Attribute {
constructor(options) {
Expand Down Expand Up @@ -43,7 +43,7 @@ class Attribute {
if (Buffer.isBuffer(val)) {
this._vals.push(val);
} else {
this._vals.push(new Buffer(String(val), _bufferEncoding(this.type)));
this._vals.push(Buffer.from(String(val), _bufferEncoding(this.type)));
}
}

Expand Down
2 changes: 1 addition & 1 deletion src/filters/presence_filter.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ module.exports = class PresenceFilter extends parents.PresenceFilter {
assert.ok(ber instanceof BerWriter, 'ber (BerWriter) required');

ber.startSequence(FILTER_PRESENT);
new Buffer(this.attribute).forEach(i => ber.writeByte(i));
Buffer.from(this.attribute).forEach(i => ber.writeByte(i));
ber.endSequence();

return ber;
Expand Down
78 changes: 62 additions & 16 deletions src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ const { getError, ConnectionError, TimeoutError, ProtocolError, LDAP_SUCCESS } =
const { Add, Bind, Del, Modify, ModifyDN, Search, Unbind } = require('./requests');
const { Response, SearchEntry, SearchReference, Parser } = require('./responses');
const parseUrl = require('./utils/parse-url');
const OID = require('./utils/OID');

class Client {
constructor(options) {
Expand All @@ -29,13 +30,16 @@ class Client {
} else {
const qItem = this._queue.get(msg.id);
if (qItem) {
const { resolve, reject, result, request } = qItem;
const { resolve, reject, result, request, controls } = qItem;

if (msg instanceof Response) {
if (msg.status !== LDAP_SUCCESS) {
reject(getError(msg));
}

controls.length = 0;
msg.controls.forEach((control) => controls.push(control));

resolve(request instanceof Search ? result : msg.object);
} else if (msg instanceof Error) {
reject(msg);
Expand All @@ -49,37 +53,37 @@ class Client {
});
}

async add(entry, attributes) {
async add(entry, attributes, controls = []) {
assert.string(entry, 'entry');
assert.object(attributes, 'attributes');

return this._send(new Add({ entry, attributes: Attribute.fromObject(attributes) }));
return this._send(new Add({ entry, attributes: Attribute.fromObject(attributes), controls }));
}

async bind(name, credentials) {
async bind(name, credentials, controls = []) {
assert.string(name, 'name');
assert.optionalString(credentials, 'credentials');

return this._send(new Bind({ name, credentials }));
return this._send(new Bind({ name, credentials, controls }));
}

async del(entry) {
async del(entry, controls = []) {
assert.string(entry, 'entry');

return this._send(new Del({ entry }));
return this._send(new Del({ entry, controls }));
}

async modify(entry, change) {
async modify(entry, change, controls = []) {
assert.string(entry, 'entry');
assert.object(change, 'change');

const changes = [];
(Array.isArray(change) ? change : [change]).forEach(c => changes.push(...Change.fromObject(c)));

return this._send(new Modify({ entry, changes }));
return this._send(new Modify({ entry, changes, controls }));
}

async modifyDN(entry, newName) {
async modifyDN(entry, newName, controls = []) {
assert.string(entry, 'entry');
assert.string(newName, 'newName');

Expand All @@ -88,24 +92,66 @@ class Client {
if (newRdn.rdns.length !== 1) {
return this._send(new ModifyDN({ entry, newRdn: parse(newRdn.rdns.shift().toString()), newSuperior: newRdn }));
} else {
return this._send(new ModifyDN({ entry, newRdn }));
return this._send(new ModifyDN({ entry, newRdn, controls }));
}
}

async search(baseObject, options) {
async search(baseObject, options, controls = []) {
assert.string(baseObject, 'baseObject');
assert.object(options, 'options');
assert.optionalString(options.scope, 'options.scope');
assert.optionalString(options.filter, 'options.filter');
assert.optionalNumber(options.sizeLimit, 'options.sizeLimit');
assert.optionalNumber(options.pageSize, 'options.pageSize');
assert.optionalNumber(options.timeLimit, 'options.timeLimit');
assert.optionalArrayOfString(options.attributes, 'options.attributes');

return this._send(new Search(Object.assign({ baseObject }, options)));
if (options.pageSize) {
let pageSize = options.pageSize;
if (pageSize > options.sizeLimit) pageSize = options.sizeLimit;

const pagedResults = {
OID: OID.PagedResults,
criticality: true,
value: {
size: pageSize,
cookie: ''
}
};

controls = controls.filter((control) => {
return control.OID !== OID.PagedResults;
});
controls.push(pagedResults);

let cookie = '';
const results = [];
let hasNext = true;
while (hasNext) {
pagedResults.value.cookie = cookie;

results.push(await this._send(new Search(Object.assign({ baseObject, controls }, options))));

const responsePagedResults = controls.find((control) => {
return control.OID === OID.PagedResults;
});

if (responsePagedResults !== undefined) {
cookie = responsePagedResults.value.cookie;
} else {
hasNext = false;
}
}

return results.flat();

} else {
return this._send(new Search(Object.assign({ baseObject, controls }, options)));
}
}

async unbind() {
return this._send(new Unbind());
async unbind(controls = []) {
return this._send(new Unbind({controls}));
}

async destroy() {
Expand Down Expand Up @@ -169,7 +215,7 @@ class Client {

return new Promise((resolve, reject) => {
try {
this._queue.set(message.id, { resolve, reject, request: message, result: [] });
this._queue.set(message.id, { resolve, reject, request: message, result: [], controls: message.controls });
this._socket.write(message.toBer());

if (message instanceof Unbind) {
Expand Down
2 changes: 1 addition & 1 deletion src/requests/del_request.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ module.exports = class extends Request {
}

_toBer(ber) {
new Buffer(this.entry).forEach(i => ber.writeByte(i));
Buffer.from(this.entry).forEach(i => ber.writeByte(i));

return ber;
}
Expand Down
40 changes: 39 additions & 1 deletion src/requests/request.js
Original file line number Diff line number Diff line change
@@ -1,11 +1,40 @@
const { BerWriter } = require('asn1');
const asn1 = require('asn1');
const BerWriter = asn1.BerWriter;
const { LDAP_CONTROLS } = require('../utils/protocol');
const OID = require('../utils/OID');

let id = 0;
const nextID = () => {
id = Math.max(1, (id + 1) % 2147483647);
return id;
};

const controlToBer = (control, writer) => {
writer.startSequence();
writer.writeString(control.OID);
writer.writeBoolean(control.criticality);

const ber = new BerWriter();
ber.startSequence();
switch (control.OID) {
case OID.PagedResults:
ber.writeInt(control.value.size);
if (control.value.cookie === '') {
ber.writeString('');
} else {
ber.writeBuffer(control.value.cookie, asn1.Ber.OctetString);
}
break;
// Add New OID controls here
default:
}

ber.endSequence();
writer.writeBuffer(ber.buffer, 0x04);

writer.endSequence();
};

module.exports = class {
constructor(options) {
Object.assign(this, options, { id: nextID() });
Expand All @@ -18,6 +47,15 @@ module.exports = class {
writer.startSequence(this.protocolOp);
writer = this._toBer(writer);
writer.endSequence();

if (this.controls.length > 0) {
writer.startSequence(LDAP_CONTROLS);
this.controls.forEach((control) => {
controlToBer(control, writer);
});
writer.endSequence();
}

writer.endSequence();
return writer.buffer;
}
Expand Down
54 changes: 52 additions & 2 deletions src/responses/response.js
Original file line number Diff line number Diff line change
@@ -1,14 +1,55 @@
const assert = require('assert-plus');
const { LDAP_REP_REFERRAL } = require('../utils/protocol');
const asn1 = require('asn1');
const Ber = asn1.Ber;
const BerReader = asn1.BerReader;
const { LDAP_REP_REFERRAL, LDAP_CONTROLS } = require('../utils/protocol');
const OID = require('../utils/OID');

const getControl = (ber) => {
if (ber.readSequence() === null) { return null; }

const control = {
OID: '',
criticality: false,
value: null
};

if (ber.length) {
const end = ber.offset + ber.length;

control.OID = ber.readString();
if (ber.offset < end && ber.peek() === Ber.Boolean) control.criticality = ber.readBoolean();

if (ber.offset < end) control.value = ber.readString(Ber.OctetString, true);

const controlBer = new BerReader(control.value);
switch (control.OID) {
case OID.PagedResults:
controlBer.readSequence();
control.value = {};
control.value.size = controlBer.readInt();
control.value.cookie = controlBer.readString(asn1.Ber.OctetString, true);
if (control.value.cookie.length === 0) {
control.value.cookie = '';
}
break;
// Add New OID controls here
default:
}
}

return control;
};

module.exports = class {
constructor(options) {
assert.optionalNumber(options.status);
assert.optionalString(options.matchedDN);
assert.optionalString(options.errorMessage);
assert.optionalArrayOfString(options.referrals);
assert.optionalArrayOfObject(options.controls);

Object.assign(this, { status: 0, matchedDN: '', errorMessage: '', referrals: [], type: 'Response' }, options);
Object.assign(this, { status: 0, matchedDN: '', errorMessage: '', referrals: [], type: 'Response', controls: [] }, options);
}

get object() {
Expand All @@ -27,6 +68,15 @@ module.exports = class {
}
}

if (ber.peek() === LDAP_CONTROLS) {
ber.readSequence();
const end = ber.offset + ber.length;
while (ber.offset < end) {
const c = getControl(ber);
if (c) { this.controls.push(c); }
}
}

return true;
}
};
8 changes: 8 additions & 0 deletions src/utils/OID.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
/**
* @see {@link https://ldap.com/ldap-oid-reference-guide/}
*/
const OID = {
PagedResults: '1.2.840.113556.1.4.319'
};

module.exports = OID;