From 6bf0a5cb5034a7e684dcc3500e841785237ce2dd Mon Sep 17 00:00:00 2001 From: Daniel Baumann Date: Sun, 7 Apr 2024 19:32:43 +0200 Subject: Adding upstream version 1:115.7.0. Signed-off-by: Daniel Baumann --- comm/mailnews/test/fakeserver/Ldapd.jsm | 665 ++++++++++++++++++++++++++++++++ 1 file changed, 665 insertions(+) create mode 100644 comm/mailnews/test/fakeserver/Ldapd.jsm (limited to 'comm/mailnews/test/fakeserver/Ldapd.jsm') 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.} - 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.} 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.} - 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.} - 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(); +} -- cgit v1.2.3