Bug 662192 - Add a fake LDAP server. r=darktrojan

Includes some test contact data and an nsLDAPSyncQuery unit test to provide
a little sanity checking for both nsLDAPSyncQuery and the fake server.
This commit is contained in:
Ben Campbell 2020-12-11 10:05:03 +02:00
Родитель ce684b1359
Коммит 1256a7f293
5 изменённых файлов: 974 добавлений и 0 удалений

Просмотреть файл

@ -0,0 +1,59 @@
/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
/**
* Test suite for nsILDAPSyncQuery.
*/
const { LDAPDaemon, LDAPHandlerFn } = ChromeUtils.import(
"resource://testing-common/mailnews/Ldapd.jsm"
);
const { BinaryServer } = ChromeUtils.import(
"resource://testing-common/mailnews/Binaryd.jsm"
);
const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
const nsILDAPSyncQuery = Ci.nsILDAPSyncQuery;
const LDAPSyncQueryContractID = "@mozilla.org/ldapsyncquery;1";
function getLDAPAttributes(urlSpec) {
let url = Services.io.newURI(urlSpec).QueryInterface(Ci.nsILDAPURL);
let ldapquery = Cc[LDAPSyncQueryContractID].createInstance(nsILDAPSyncQuery);
let payload = ldapquery.getQueryResults(url, Ci.nsILDAPConnection.VERSION3);
// Returns a string with one attr per line.
return payload;
}
add_task(async function test_LDAPSyncQuery() {
// Set up fake LDAP server, loaded with some contacts.
let daemon = new LDAPDaemon();
let raw = await IOUtils.readUTF8(
do_get_file(
"../../../../mailnews/addrbook/test/unit/data/ldap_contacts.json"
).path
);
let testContacts = JSON.parse(raw);
daemon.add(...Object.values(testContacts));
// daemon.setDebug(true);
let server = new BinaryServer(LDAPHandlerFn, daemon);
server.start();
// Fetch only the Holmes family.
let out = getLDAPAttributes(
`ldap://localhost:${server.port}/??sub?(sn=Holmes)`
);
if (daemon.debug) {
dump(`--- getLDAPAttributes() ---\n${out}\n--------------------\n`);
}
// Make sure we got the contacts we expected:
Assert.ok(out.includes("cn=Eurus Holmes"));
Assert.ok(out.includes("cn=Mycroft Holmes"));
Assert.ok(out.includes("cn=Sherlock Holmes"));
// Sanity check: make sure some non-Holmes people were excluded.
Assert.ok(!out.includes("cn=John Watson"));
Assert.ok(!out.includes("cn=Jim Moriarty"));
server.stop();
});

Просмотреть файл

@ -3,3 +3,4 @@ head =
tail =
[test_nsLDAPURL.js]
[test_nsLDAPSyncQuery.js]

Просмотреть файл

@ -56,7 +56,9 @@ JAR_MANIFESTS += ["jar.mn"]
TESTING_JS_MODULES.mailnews += [
"test/fakeserver/Auth.jsm",
"test/fakeserver/Binaryd.jsm",
"test/fakeserver/Imapd.jsm",
"test/fakeserver/Ldapd.jsm",
"test/fakeserver/Maild.jsm",
"test/fakeserver/Nntpd.jsm",
"test/fakeserver/Pop3d.jsm",

Просмотреть файл

@ -0,0 +1,253 @@
/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
/* vim:set ts=2 sw=2 sts=2 et: */
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
const EXPORTED_SYMBOLS = ["BinaryServer"];
const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
const CC = Components.Constructor;
const ServerSocket = CC(
"@mozilla.org/network/server-socket;1",
"nsIServerSocket",
"init"
);
const BinaryInputStream = CC(
"@mozilla.org/binaryinputstream;1",
"nsIBinaryInputStream",
"setInputStream"
);
const BinaryOutputStream = CC(
"@mozilla.org/binaryoutputstream;1",
"nsIBinaryOutputStream",
"setOutputStream"
);
/**
* A binary stream-based server.
* Listens on a socket, and whenever a new connection is made it runs
* a user-supplied handler function.
*
* Example:
* A trivial echo server (with a null daemon, so no state shared between
* connections):
*
* let echoServer = new BinaryServer(function(conn, daemon) {
* while(1) {
* let data = conn.read(1);
* conn.write(data);
* }
* }, null);
*
*/
class BinaryServer {
/**
* The handler function should be of the form:
* async function handlerFn(conn, daemon)
*
* @async
* @callback handlerFn
* @param {Connection} conn
* @param {Object} daemon
*
* The handler function runs as long as it wants - reading and writing bytes
* (via methods on conn) until it is finished with the connection.
* The handler simply returns to indicate the connection is done, or throws
* an exception to indicate that something went wrong.
* The daemon is the object which holds the server data/state, shared with
* all connection handler. The BinaryServer doesn't do anything with daemon
* other than passing it directly on to the handler function.
*/
/**
* Construct a new BinaryServer.
*
* @param {handlerFn} handlerFn - Function to call to handle each new connection.
* @param {Object} daemon - Object to pass on to the handler, to share state
* and functionality between across connections.
*/
constructor(handlerFn, daemon) {
this._port = -1;
this._handlerFn = handlerFn;
this._daemon = daemon;
this._listener = null; // Listening socket to accept new connections.
this._connections = new Set();
}
/**
* Starts the server running.
*
* @param {number} port - The port to run on (or -1 to pick one automatically).
*/
async start(port = -1) {
if (this._listener) {
throw Components.Exception(
"Server already started",
Cr.NS_ERROR_ALREADY_INITIALIZED
);
}
let socket = new ServerSocket(
port,
true, // Loopback only.
-1 // Default max pending connections.
);
let server = this;
socket.asyncListen({
async onSocketAccepted(socket, transport) {
let conn = new Connection(transport);
server._connections.add(conn);
try {
await server._handlerFn(conn, server._daemon);
// If we get here, handler completed, without error.
} catch (e) {
if (conn.isClosed()) {
// if we get here, assume the error occured because we're
// shutting down, and ignore it.
} else {
// if we get here, something went wrong.
dump("ERROR " + e.toString());
}
}
conn.close();
server._connections.delete(conn);
},
onStopListening(socket, status) {
// Server is stopping, time to close any outstanding connections.
server._connections.forEach(conn => conn.close());
server._connections.clear();
},
QueryInterface: ChromeUtils.generateQI(["nsIServerSocketListener"]),
});
// We're running!
this._listener = socket;
}
/**
* Provides port, a read-only attribute to get which port the server
* server is listening upon. Behaviour is undefined if server is not
* running.
*/
get port() {
return this._listener.port;
}
/**
* Stops the server, if it is running.
*/
stop() {
if (!this._listener) {
// Already stopped.
return;
}
this._listener.close();
this._listener = null;
// We could still be accepting new connections at this point,
// so we wait until the onStopListening callback to tear down the
// connections.
}
}
/**
* Connection wraps a nsITransport with read/write functions that are
* javascript async, to simplify writing server handers.
* Handlers should only need to use read() and write() from here, leaving
* all connection management up to the BinaryServer.
*/
class Connection {
constructor(transport) {
this._transport = transport;
this._input = transport.openInputStream(0, 0, 0);
let outStream = transport.openOutputStream(0, 0, 0);
this._output = new BinaryOutputStream(outStream);
}
/**
* @return true if close() has been called.
*/
isClosed() {
return this._transport === null;
}
/**
* Closes the connection. Can be safely called multiple times.
* The BinaryServer will call this - handlers don't need to worry about
* the connection status.
*/
close() {
if (this.isClosed()) {
return;
}
this._input.close();
this._output.close();
this._transport.close(Cr.NS_OK);
this._input = null;
this._output = null;
this._transport = null;
}
/**
* Read exactly nBytes from the connection.
*
* @param {number} nBytes - The number of bytes required.
* @return {Array.<number>} - An array containing the requested bytes.
*/
async read(nBytes) {
let conn = this;
let buf = [];
while (buf.length < nBytes) {
let want = nBytes - buf.length;
// A slightly odd-looking construct to wrap the listener-based
// asyncwait() into a javascript async function.
await new Promise((resolve, reject) => {
try {
conn._input.asyncWait(
{
onInputStreamReady(stream) {
// how many bytes are actually available?
let n;
try {
n = stream.available();
} catch (e) {
// stream was closed.
reject(e);
}
if (n > want) {
n = want;
}
let chunk = new BinaryInputStream(stream).readByteArray(n);
Array.prototype.push.apply(buf, chunk);
resolve();
},
},
0,
want,
Services.tm.mainThread
);
} catch (e) {
// asyncwait() failed
reject(e);
}
});
}
return buf;
}
/**
* Write data to the connection.
*
* @param {Array.<number>} data - The bytes to send.
*/
async write(data) {
// TODO: need to check outputstream for writeability here???
// Might be an issue if we start throwing bigger chunks of data about...
await this._output.writeByteArray(data);
}
}

Просмотреть файл

@ -0,0 +1,659 @@
/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
/* vim:set ts=2 sw=2 sts=2 et: */
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
const EXPORTED_SYMBOLS = ["LDAPDaemon", "LDAPHandlerFn"];
/**
* This file provides fake LDAP server functionality, just enough to run
* our unit tests against.
*
* Currently:
* - it accepts any bind request (no authentication).
* - it supports searches, but only some types of filter.
* - it supports unbind (quit) requests.
* - all other requests are ignored.
*
* It should be extensible enough that extra features can be added as
* required.
*/
/*
* Helpers for application-neutral BER-encoding/decoding.
*
* BER is self-describing enough to allow us to parse it without knowing
* the meaning. So we can break down a binary stream into ints, strings,
* sequences etc... and then leave it up to the separate LDAP code to
* interpret the meaning of it.
*
* Clearest BER reference I've read:
* https://docs.oracle.com/cd/E19476-01/821-0510/def-basic-encoding-rules.html
*/
/**
* Encodes a BER length, returning an array of (wire-format) bytes.
* It's variable length encoding - smaller numbers are encoded with
* fewer bytes.
*
* @param {number} i - The length to encode.
*/
function encodeLength(i) {
if (i < 128) {
return [i];
}
let temp = i;
let bytes = [];
while (temp >= 128) {
bytes.unshift(temp & 255);
temp >>= 8;
}
bytes.unshift(temp);
bytes.unshift(0x80 | bytes.length);
return bytes;
}
/**
* Helper for encoding and decoding BER values.
* Each value is notionally a type-length-data triplet, although we just
* store type and an array for data (with the array knowing it's length).
* BERValue.data is held in raw form (wire format) for non-sequence values.
* For sequences and sets (constructed values), .data is empty, and
* instead .children is used to hold the contained BERValue objects.
*/
class BERValue {
constructor(type) {
this.type = type;
this.children = []; // only for constructed values (sequences)
this.data = []; // the raw data (empty for constructed ones)
}
/**
* Encode the BERValue to an array of bytes, ready to be written to the wire.
*
* @return {Array.<number>} - The encoded bytes.
*/
encode() {
let bytes = [];
if (this.isConstructed()) {
for (let c of this.children) {
bytes = bytes.concat(c.encode());
}
} else {
bytes = this.data;
}
return [this.type].concat(encodeLength(bytes.length), bytes);
}
// Functions to check class (upper two bits of type).
isUniversal() {
return (this.type & 0xc0) == 0x00;
}
isApplication() {
return (this.type & 0xc0) == 0x40;
}
isContextSpecific() {
return (this.type & 0xc0) == 0x80;
}
isPrivate() {
return (this.type & 0xc0) == 0xc0;
}
/*
* @return {boolean} - Is this value a constructed type a sequence or set?
* (As encoded in bit 5 of the type)
*/
isConstructed() {
return !!(this.type & 0x20);
}
/**
* @return {number} - The tag number of the type (the lower 5 bits).
*/
tag() {
return this.type & 0x1f;
}
// Functions to check for some of the core universal types.
isNull() {
return this.type == 0x05;
}
isBoolean() {
return this.type == 0x01;
}
isInteger() {
return this.type == 0x02;
}
isOctetString() {
return this.type == 0x04;
}
isEnumerated() {
return this.type == 0x0a;
}
// Functions to interpret the value in particular ways.
// No type checking is performed, as application/context-specific
// types can also use these.
asBoolean() {
return this.data[0] != 0;
}
asInteger() {
let i = 0;
// TODO: handle negative numbers!
for (let b of this.data) {
i = (i << 8) | b;
}
return i;
}
asEnumerated() {
return this.asInteger();
}
// Helper to interpret an octet string as an ASCII string.
asString() {
// TODO: pass in expected encoding?
if (this.data.length > 0) {
return String.fromCharCode(...this.data);
}
return "";
}
// Static helpers to construct specific types of BERValue.
static newNull() {
let ber = new BERValue(0x05);
ber.data = [];
return ber;
}
static newBoolean(b) {
let ber = new BERValue(0x01);
ber.data = [b ? 0xff : 0x00];
return ber;
}
static newInteger(i) {
let ber = new BERValue(0x02);
// TODO: does this handle negative correctly?
while (i >= 128) {
ber.data.unshift(i & 255);
i >>= 8;
}
ber.data.unshift(i);
return ber;
}
static newEnumerated(i) {
let ber = BERValue.newInteger(i);
ber.type = 0x0a; // sneaky but valid.
return ber;
}
static newOctetString(bytes) {
let ber = new BERValue(0x04);
ber.data = bytes;
return ber;
}
/**
* Create an octet string from an ASCII string.
*/
static newString(str) {
let ber = new BERValue(0x04);
if (str.length > 0) {
ber.data = Array.from(str, c => c.charCodeAt(0));
}
return ber;
}
/**
* Create a new sequence
* @param {number} type - BER type byte
* @param {Array.<BERValue>} children - The contents of the sequence.
*/
static newSequence(type, children) {
let ber = new BERValue(type);
ber.children = children;
return ber;
}
/*
* A helper to dump out the value (and it's children) in a human-readable
* way.
*/
dbug(prefix = "") {
let desc = "";
switch (this.type) {
case 0x01:
desc += `BOOLEAN (${this.asBoolean()})`;
break;
case 0x02:
desc += `INTEGER (${this.asInteger()})`;
break;
case 0x04:
desc += `OCTETSTRING ("${this.asString()}")`;
break;
case 0x05:
desc += `NULL`;
break;
case 0x0a:
desc += `ENUMERATED (${this.asEnumerated()})`;
break;
case 0x30:
desc += `SEQUENCE`;
break;
case 0x31:
desc += `SET`;
break;
default:
desc = `0x${this.type.toString(16)}`;
if (this.isConstructed()) {
desc += " SEQUENCE";
}
break;
}
switch (this.type & 0xc0) {
case 0x00:
break; // universal
case 0x40:
desc += " APPLICATION";
break;
case 0x80:
desc += " CONTEXT-SPECIFIC";
break;
case 0xc0:
desc += " PRIVATE";
break;
}
if (this.isConstructed()) {
desc += ` ${this.children.length} children`;
} else {
desc += ` ${this.data.length} bytes`;
}
// Dump out the beginning of the payload as raw bytes.
let rawdump = this.data.slice(0, 8).join(" ");
if (this.data.length > 8) {
rawdump += "...";
}
dump(`${prefix}${desc} ${rawdump}\n`);
for (let c of this.children) {
c.dbug(prefix + " ");
}
}
}
/**
* Parser to decode BER elements from a Connection.
*/
class BERParser {
constructor(conn) {
this._conn = conn;
}
/**
* Helper to fetch the next byte in the stream.
*
* @return {number} - The byte.
*/
async _nextByte() {
let buf = await this._conn.read(1);
return buf[0];
}
/**
* Helper to read a BER length field from the connection.
*
* @return {Array.<number>} - 2 elements: [length, bytesconsumed].
*/
async _readLength() {
let n = await this._nextByte();
if ((n & 0x80) == 0) {
return [n, 1]; // msb clear => single-byte encoding
}
// lower 7 bits are number of bytes encoding length (big-endian order).
n = n & 0x7f;
let len = 0;
for (let i = 0; i < n; ++i) {
len = (len << 8) + (await this._nextByte());
}
return [len, 1 + n];
}
/**
* Reads a single BERValue from the connection (including any children).
*
* @return {Array.<number>} - 2 elements: [value, bytesconsumed].
*/
async decodeBERValue() {
// BER values always encoded as TLV (type, length, value) triples,
// where type is a single byte, length can be a variable number of bytes
// and value is a byte string, of size length.
let type = await this._nextByte();
let [length, lensize] = await this._readLength();
let ber = new BERValue(type);
if (type & 0x20) {
// it's a sequence
let cnt = 0;
while (cnt < length) {
let [child, consumed] = await this.decodeBERValue();
cnt += consumed;
ber.children.push(child);
}
if (cnt != length) {
// All the bytes in the sequence must be accounted for.
// TODO: should define a specific BER error type so handler can
// detect and respond to BER decoding issues?
throw new Error("Mismatched length in sequence");
}
} else {
ber.data = await this._conn.read(length);
}
return [ber, 1 + lensize + length];
}
}
/*
* LDAP-specific code from here on.
*/
/*
* LDAPDaemon holds our LDAP database and has methods for
* searching and manipulating the data.
* So tests can set up test data here, shared by any number of LDAPHandlerFn
* connections.
*/
class LDAPDaemon {
constructor() {
// An entry is an object of the form:
// {dn:"....", attributes: {attr1: [val1], attr2:[val2,val3], ...}}
// Note that the attribute values are arrays (attributes can have multiple
// values in LDAP).
this.entries = {}; // We map dn to entry, to ensure dn is unique.
this.debug = false;
}
/**
* If set, will dump out assorted debugging info.
*/
setDebug(yesno) {
this.debug = yesno;
}
/**
* Add entries to the LDAP database.
* Overwrites previous entries with same dn.
* since attributes can have multiple values, they should be arrays.
* For example:
* {dn: "...", {cn: ["Bob Smith"], ...}}
* But because that can be a pain, non-arrays values will be promoted.
* So we'll also accept:
* {dn: "...", {cn: "Bob Smith", ...}}
*/
add(...entries) {
for (let e of entries) {
if (e.dn === undefined || e.attributes === undefined) {
throw new Error("bad entry");
}
// Convert attr values to arrays, if required.
for (let [attr, val] of Object.entries(e.attributes)) {
if (!Array.isArray(val)) {
e.attributes[attr] = [val];
}
}
this.entries[e.dn] = e;
}
}
/**
* Find entries in our LDAP db.
*
* @param {BERValue} berFilter - BERValue containing the filter to apply.
* @return {Array} - The matching entries.
*/
search(berFilter) {
let f = this.buildFilter(berFilter);
return Object.values(this.entries).filter(f);
}
/**
* Recursively build a filter function from a BER-encoded filter.
* The resulting function accepts a single entry as parameter, and
* returns a bool to say if it passes the filter or not.
*
* @param {BERValue} ber - The filter.
* @return {function} - A function to test an entry against the filter.
*/
buildFilter(ber) {
if (!ber.isContextSpecific()) {
throw new Error("Bad filter");
}
switch (ber.tag()) {
case 0: {
// and
if (ber.children.length < 1) {
throw new Error("Bad 'and' filter");
}
let subFilters = ber.children.map(this.buildFilter);
return function(e) {
return subFilters.every(filt => filt(e));
};
}
case 1: {
// or
if (ber.children.length < 1) {
throw new Error("Bad 'or' filter");
}
let subFilters = ber.children.map(this.buildFilter);
return function(e) {
return subFilters.some(filt => filt(e));
};
}
case 2: {
// not
if (ber.children.length != 1) {
throw new Error("Bad 'not' filter");
}
let subFilter = this.buildFilter(ber.children[0]); // one child
return function(e) {
return !subFilter(e);
};
}
case 3: {
// equalityMatch
if (ber.children.length != 2) {
throw new Error("Bad 'equality' filter");
}
let attrName = ber.children[0].asString().toLowerCase();
let attrVal = ber.children[1].asString().toLowerCase();
return function(e) {
let attrs = Object.keys(e.attributes).reduce(function(c, key) {
c[key.toLowerCase()] = e.attributes[key];
return c;
}, {});
return (
attrs[attrName] !== undefined &&
attrs[attrName].map(val => val.toLowerCase()).includes(attrVal)
);
};
}
case 7: {
// present
let attrName = ber.asString().toLowerCase();
return function(e) {
let attrs = Object.keys(e.attributes).reduce(function(c, key) {
c[key.toLowerCase()] = e.attributes[key];
return c;
}, {});
return attrs[attrName] !== undefined;
};
}
case 4: // substring (Probably need to implement this!)
case 5: // greaterOrEqual
case 6: // lessOrEqual
case 8: // approxMatch
case 9: // extensibleMatch
// UNSUPPORTED! just match everything.
dump("WARNING: unsupported filter\n");
return e => true;
default:
throw new Error("unknown filter");
}
}
}
/**
* Helper class to help break down LDAP handler into multiple functions.
* Used by LDAPHandlerFn, below.
* Handler state for a single connection (as opposed to any state common
* across all connections, which is handled by LDAPDaemon).
*/
class LDAPHandler {
constructor(conn, daemon) {
this._conn = conn;
this._daemon = daemon;
}
// handler run() should exit when done, or throw exception to crash out.
async run() {
let parser = new BERParser(this._conn);
while (1) {
let [msg] = await parser.decodeBERValue();
if (this._daemon.debug) {
dump("=== received ===\n");
msg.dbug("C: ");
}
if (
msg.type != 0x30 ||
msg.children.length < 2 ||
!msg.children[0].isInteger()
) {
// badly formed message - TODO: bail out gracefully...
throw new Error("Bad message..");
}
let msgID = msg.children[0].asInteger();
let req = msg.children[1];
// Handle a teeny tiny subset of requests.
switch (req.type) {
case 0x60:
this.handleBindRequest(msgID, req);
break;
case 0x63:
this.handleSearchRequest(msgID, req);
break;
case 0x42: // unbindRequest (essentially a "quit").
return;
}
}
}
/**
* Send out an LDAP message.
*
* @param {number} msgID - The ID of the message we're responding to.
* @param {BERValue} payload - The message content.
*/
async sendLDAPMessage(msgID, payload) {
let msg = BERValue.newSequence(0x30, [BERValue.newInteger(msgID), payload]);
if (this._daemon.debug) {
msg.dbug("S: ");
}
await this._conn.write(msg.encode());
}
async handleBindRequest(msgID, req) {
// Ignore the details, just say "OK!"
// TODO: Add some auth support here, would be handy for testing.
let bindResponse = new BERValue(0x61);
bindResponse.children = [
BERValue.newEnumerated(0), // resultCode 0=success
BERValue.newString(""), // matchedDN
BERValue.newString(""), // diagnosticMessage
];
if (this._daemon.debug) {
dump("=== send bindResponse ===\n");
}
await this.sendLDAPMessage(msgID, bindResponse);
}
async handleSearchRequest(msgID, req) {
// Make sure all the parts we expect are present and of correct type.
if (
req.children.length < 8 ||
!req.children[0].isOctetString() ||
!req.children[1].isEnumerated() ||
!req.children[2].isEnumerated() ||
!req.children[3].isInteger() ||
!req.children[4].isInteger() ||
!req.children[5].isBoolean()
) {
throw new Error("Bad search request!");
}
// Perform search
let filt = req.children[6];
let matches = this._daemon.search(filt);
// Send a searchResultEntry for each match
for (let match of matches) {
let dn = BERValue.newString(match.dn);
let attrList = new BERValue(0x30);
for (let [key, values] of Object.entries(match.attributes)) {
let valueSet = new BERValue(0x31);
for (let v of values) {
valueSet.children.push(BERValue.newString(v));
}
attrList.children.push(
BERValue.newSequence(0x30, [BERValue.newString(key), valueSet])
);
}
// 0x64 = searchResultEntry
let searchResultEntry = BERValue.newSequence(0x64, [dn, attrList]);
if (this._daemon.debug) {
dump(`=== send searchResultEntry ===\n`);
}
this.sendLDAPMessage(msgID, searchResultEntry);
}
//SearchResultDone ::= [APPLICATION 5] LDAPResult
let searchResultDone = new BERValue(0x65);
searchResultDone.children = [
BERValue.newEnumerated(0), // resultCode 0=success
BERValue.newString(""), // matchedDN
BERValue.newString(""), // diagnosticMessage
];
if (this._daemon.debug) {
dump(`=== send searchResultDone ===\n`);
}
this.sendLDAPMessage(msgID, searchResultDone);
}
}
/**
* Handler function to deal with a connection to our LDAP server.
*/
async function LDAPHandlerFn(conn, daemon) {
let handler = new LDAPHandler(conn, daemon);
await handler.run();
}