summaryrefslogtreecommitdiffstats
path: root/comm/mailnews/test/fakeserver/Ldapd.jsm
diff options
context:
space:
mode:
Diffstat (limited to 'comm/mailnews/test/fakeserver/Ldapd.jsm')
-rw-r--r--comm/mailnews/test/fakeserver/Ldapd.jsm665
1 files changed, 665 insertions, 0 deletions
diff --git a/comm/mailnews/test/fakeserver/Ldapd.jsm b/comm/mailnews/test/fakeserver/Ldapd.jsm
new file mode 100644
index 0000000000..2206ebce81
--- /dev/null
+++ b/comm/mailnews/test/fakeserver/Ldapd.jsm
@@ -0,0 +1,665 @@
+/* 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"];
+
+var { MailStringUtils } = ChromeUtils.import(
+ "resource:///modules/MailStringUtils.jsm"
+);
+
+/**
+ * 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.
+ *
+ * @returns {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);
+ }
+
+ /**
+ * @returns {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 MailStringUtils.uint8ArrayToByteString(new Uint8Array(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.
+ *
+ * @returns {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.
+ *
+ * @returns {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).
+ *
+ * @returns {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) {
+ // Clone the data before munging it.
+ let entriesCopy = JSON.parse(JSON.stringify(entries));
+ for (let e of entriesCopy) {
+ 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.
+ * @returns {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.
+ * @returns {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();
+}