diff options
Diffstat (limited to 'comm/mailnews/addrbook/test/LDAPServer.jsm')
-rw-r--r-- | comm/mailnews/addrbook/test/LDAPServer.jsm | 324 |
1 files changed, 324 insertions, 0 deletions
diff --git a/comm/mailnews/addrbook/test/LDAPServer.jsm b/comm/mailnews/addrbook/test/LDAPServer.jsm new file mode 100644 index 0000000000..c8d8edb82b --- /dev/null +++ b/comm/mailnews/addrbook/test/LDAPServer.jsm @@ -0,0 +1,324 @@ +/* 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 = ["LDAPServer"]; +const PRINT_DEBUG = false; + +const { Assert } = ChromeUtils.importESModule( + "resource://testing-common/Assert.sys.mjs" +); + +/** + * This is a partial implementation of an LDAP server as defined by RFC 4511. + * It's not intended to serve any particular dataset, rather, tests should + * cause the application to make requests and tell the server what to respond. + * + * https://docs.ldap.com/specs/rfc4511.txt + * + * @implements {nsIInputStreamCallback} + * @implements {nsIServerSocketListener} + */ +var LDAPServer = { + BindRequest: 0x60, + UnbindRequest: 0x42, + SearchRequest: 0x63, + AbandonRequest: 0x50, + + serverSocket: null, + + QueryInterface: ChromeUtils.generateQI([ + "nsIInputStreamCallback", + "nsIServerSocketListener", + ]), + + /** + * Start listening on an OS-selected port. The port number can be found at + * LDAPServer.port. + */ + open() { + this.serverSocket = Cc[ + "@mozilla.org/network/server-socket;1" + ].createInstance(Ci.nsIServerSocket); + this.serverSocket.init(-1, true, 1); + console.log(`socket open on port ${this.serverSocket.port}`); + + this.serverSocket.asyncListen(this); + }, + /** + * Stop listening for new connections and close any that are open. + */ + close() { + this.serverSocket.close(); + }, + /** + * The port this server is listening on. + */ + get port() { + return this.serverSocket.port; + }, + + /** + * Retrieves any data sent to the server since connection or the previous + * call to read(). This should be called every time the application is + * expected to send data. + * + * @returns {Promise} Resolves when data is received by the server, with the + * data as a byte array. + */ + async read(expectedOperation) { + let data; + if (this._data) { + data = this._data; + delete this._data; + } else { + data = await new Promise(resolve => { + this._inputStreamReadyResolve = resolve; + }); + } + + // Simplified parsing to get the message ID and operation code. + + let index = 4; + // The value at [1] may be more than one byte. If it is, skip more bytes. + if (data[1] & 0x80) { + index += data[1] & 0x7f; + } + + // Assumes the ID is not greater than 127. + this._lastMessageID = data[index]; + + if (expectedOperation) { + let actualOperation = data[index + 1]; + + // Unbind and abandon requests can happen at any point, when an + // nsLDAPConnection is destroyed. This is unpredictable, and irrelevant + // for testing. Ignore. + if ( + actualOperation == LDAPServer.UnbindRequest || + actualOperation == LDAPServer.AbandonRequest + ) { + if (PRINT_DEBUG) { + console.log("Ignoring unbind or abandon request"); + } + return this.read(expectedOperation); + } + + Assert.equal( + actualOperation.toString(16), + expectedOperation.toString(16), + "LDAP Operation type" + ); + } + + return data; + }, + /** + * Sends raw data to the application. Generally this shouldn't be used + * directly but it may be useful for testing. + * + * @param {byte[]} data - The data to write. + */ + write(data) { + if (PRINT_DEBUG) { + console.log( + ">>> " + data.map(b => b.toString(16).padStart(2, 0)).join(" ") + ); + } + this._outputStream.writeByteArray(data); + }, + /** + * Sends a simple BindResponse to the application. + * See section 4.2.2 of the RFC. + */ + writeBindResponse() { + let message = new Sequence(0x30, new IntegerValue(this._lastMessageID)); + let person = new Sequence( + 0x61, + new EnumeratedValue(0), + new StringValue(""), + new StringValue("") + ); + message.children.push(person); + this.write(message.getBytes()); + }, + /** + * Sends a SearchResultEntry to the application. + * See section 4.5.2 of the RFC. + * + * @param {object} entry + * @param {string} entry.dn - The LDAP DN of the person. + * @param {string} entry.attributes - A key/value or key/array-of-values + * object representing the person. + */ + writeSearchResultEntry({ dn, attributes }) { + let message = new Sequence(0x30, new IntegerValue(this._lastMessageID)); + + let person = new Sequence(0x64, new StringValue(dn)); + message.children.push(person); + + let attributeSequence = new Sequence(0x30); + person.children.push(attributeSequence); + + for (let [key, value] of Object.entries(attributes)) { + let seq = new Sequence(0x30, new StringValue(key), new Sequence(0x31)); + if (typeof value == "string") { + value = [value]; + } + for (let v of value) { + seq.children[1].children.push(new StringValue(v)); + } + attributeSequence.children.push(seq); + } + + this.write(message.getBytes()); + }, + /** + * Sends a SearchResultDone to the application. + * See RFC 4511 section 4.5.2. + */ + writeSearchResultDone() { + let message = new Sequence(0x30, new IntegerValue(this._lastMessageID)); + let person = new Sequence( + 0x65, + new EnumeratedValue(0), + new StringValue(""), + new StringValue("") + ); + message.children.push(person); + this.write(message.getBytes()); + }, + + /** + * nsIServerSocketListener.onSocketAccepted + */ + onSocketAccepted(socket, transport) { + let inputStream = transport + .openInputStream(0, 8192, 1024) + .QueryInterface(Ci.nsIAsyncInputStream); + + let outputStream = transport.openOutputStream(0, 0, 0); + this._outputStream = Cc["@mozilla.org/binaryoutputstream;1"].createInstance( + Ci.nsIBinaryOutputStream + ); + this._outputStream.setOutputStream(outputStream); + + if (this._socketConnectedResolve) { + this._socketConnectedResolve(); + delete this._socketConnectedResolve; + } + inputStream.asyncWait(this, 0, 0, Services.tm.mainThread); + }, + /** + * nsIServerSocketListener.onStopListening + */ + onStopListening(socket, status) { + console.log(`socket closed with status ${status.toString(16)}`); + }, + + /** + * nsIInputStreamCallback.onInputStreamReady + */ + onInputStreamReady(stream) { + let available; + try { + available = stream.available(); + } catch (ex) { + if ( + [Cr.NS_BASE_STREAM_CLOSED, Cr.NS_ERROR_NET_RESET].includes(ex.result) + ) { + return; + } + throw ex; + } + + let binaryInputStream = Cc[ + "@mozilla.org/binaryinputstream;1" + ].createInstance(Ci.nsIBinaryInputStream); + binaryInputStream.setInputStream(stream); + let data = binaryInputStream.readByteArray(available); + if (PRINT_DEBUG) { + console.log( + "<<< " + data.map(b => b.toString(16).padStart(2, 0)).join(" ") + ); + } + + if (this._inputStreamReadyResolve) { + this._inputStreamReadyResolve(data); + delete this._inputStreamReadyResolve; + } else { + this._data = data; + } + + stream.asyncWait(this, 0, 0, Services.tm.mainThread); + }, +}; + +/** + * Helper classes to convert primitives to LDAP byte sequences. + */ + +class Sequence { + constructor(number, ...children) { + this.number = number; + this.children = children; + } + getBytes() { + let bytes = []; + for (let c of this.children) { + bytes = bytes.concat(c.getBytes()); + } + return [this.number].concat(getLengthBytes(bytes.length), bytes); + } +} +class IntegerValue { + constructor(int) { + this.int = int; + this.number = 0x02; + } + getBytes() { + let temp = this.int; + let bytes = []; + + while (temp >= 128) { + bytes.unshift(temp & 255); + temp >>= 8; + } + bytes.unshift(temp); + return [this.number].concat(getLengthBytes(bytes.length), bytes); + } +} +class StringValue { + constructor(str) { + this.str = str; + } + getBytes() { + return [0x04].concat( + getLengthBytes(this.str.length), + Array.from(this.str, c => c.charCodeAt(0)) + ); + } +} +class EnumeratedValue extends IntegerValue { + constructor(int) { + super(int); + this.number = 0x0a; + } +} + +function getLengthBytes(int) { + if (int < 128) { + return [int]; + } + + let temp = int; + let bytes = []; + + while (temp >= 128) { + bytes.unshift(temp & 255); + temp >>= 8; + } + bytes.unshift(temp); + bytes.unshift(0x80 | bytes.length); + return bytes; +} |