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:
Родитель
ce684b1359
Коммит
1256a7f293
|
@ -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();
|
||||
}
|
Загрузка…
Ссылка в новой задаче