diff options
Diffstat (limited to 'comm/mailnews/addrbook/modules/LDAPClient.jsm')
-rw-r--r-- | comm/mailnews/addrbook/modules/LDAPClient.jsm | 285 |
1 files changed, 285 insertions, 0 deletions
diff --git a/comm/mailnews/addrbook/modules/LDAPClient.jsm b/comm/mailnews/addrbook/modules/LDAPClient.jsm new file mode 100644 index 0000000000..e26b7b5fce --- /dev/null +++ b/comm/mailnews/addrbook/modules/LDAPClient.jsm @@ -0,0 +1,285 @@ +/* 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 = ["LDAPClient"]; + +var { CommonUtils } = ChromeUtils.importESModule( + "resource://services-common/utils.sys.mjs" +); +var { setTimeout } = ChromeUtils.importESModule( + "resource://gre/modules/Timer.sys.mjs" +); +var { + AbandonRequest, + BindRequest, + UnbindRequest, + SearchRequest, + LDAPResponse, +} = ChromeUtils.import("resource:///modules/LDAPMessage.jsm"); + +class LDAPClient { + /** + * @param {string} host - The LDAP server host. + * @param {number} port - The LDAP server port. + * @param {boolean} useSecureTransport - Whether to use TLS connection. + */ + constructor(host, port, useSecureTransport) { + this.onOpen = () => {}; + this.onError = () => {}; + + this._host = host; + this._port = port; + this._useSecureTransport = useSecureTransport; + + this._messageId = 1; + this._callbackMap = new Map(); + + this._logger = console.createInstance({ + prefix: "mailnews.ldap", + maxLogLevel: "Warn", + maxLogLevelPref: "mailnews.ldap.loglevel", + }); + + this._dataEventsQueue = []; + } + + connect() { + let hostname = this._host.toLowerCase(); + this._logger.debug( + `Connecting to ${ + this._useSecureTransport ? "ldaps" : "ldap" + }://${hostname}:${this._port}` + ); + this._socket = new TCPSocket(hostname, this._port, { + binaryType: "arraybuffer", + useSecureTransport: this._useSecureTransport, + }); + this._socket.onopen = this._onOpen; + this._socket.onerror = this._onError; + } + + /** + * Send a simple bind request to the server. + * + * @param {string} dn - The name to bind. + * @param {string} password - The password. + * @param {Function} callback - Callback function when receiving BindResponse. + * @returns {number} The id of the sent request. + */ + bind(dn, password, callback) { + this._logger.debug(`Binding ${dn}`); + let req = new BindRequest(dn || "", password || ""); + return this._send(req, callback); + } + + /** + * Send a SASL bind request to the server. + * + * @param {string} service - The service host name to bind. + * @param {string} mechanism - The SASL mechanism to use, e.g. GSSAPI. + * @param {string} authModuleType - The auth module type, @see nsIMailAuthModule. + * @param {ArrayBuffer} serverCredentials - The challenge token returned from + * the server, which must be used to generate a new request token. Or + * undefined for the first request. + * @param {Function} callback - Callback function when receiving BindResponse. + * @returns {number} The id of the sent request. + */ + saslBind(service, mechanism, authModuleType, serverCredentials, callback) { + this._logger.debug(`Binding ${service} using ${mechanism}`); + if (!this._authModule || this._authModuleType != authModuleType) { + this._authModuleType = authModuleType; + this._authModule = Cc["@mozilla.org/mail/auth-module;1"].createInstance( + Ci.nsIMailAuthModule + ); + this._authModule.init( + authModuleType, + service, + 0, // nsIAuthModule::REQ_DEFAULT + null, // domain + null, // username + null // password + ); + } + // getNextToken expects a base64 string. + let token = this._authModule.getNextToken( + serverCredentials + ? btoa( + CommonUtils.arrayBufferToByteString( + new Uint8Array(serverCredentials) + ) + ) + : "" + ); + // token is a base64 string, convert it to Uint8Array. + let credentials = CommonUtils.byteStringToArrayBuffer(atob(token)); + let req = new BindRequest("", "", { mechanism, credentials }); + return this._send(req, callback); + } + + /** + * Send an unbind request to the server. + */ + unbind() { + return this._send(new UnbindRequest(), () => this._socket.close()); + } + + /** + * Send a search request to the server. + * + * @param {string} dn - The name to search. + * @param {number} scope - The scope to search. + * @param {string} filter - The filter string. + * @param {string} attributes - Attributes to include in the search result. + * @param {number} timeout - The seconds to wait. + * @param {number} limit - Maximum number of entries to return. + * @param {Function} callback - Callback function when receiving search responses. + * @returns {number} The id of the sent request. + */ + search(dn, scope, filter, attributes, timeout, limit, callback) { + this._logger.debug(`Searching dn="${dn}" filter="${filter}"`); + let req = new SearchRequest(dn, scope, filter, attributes, timeout, limit); + return this._send(req, callback); + } + + /** + * Send an abandon request to the server. + * + * @param {number} messageId - The id of the message to abandon. + */ + abandon(messageId) { + this._logger.debug(`Abandoning ${messageId}`); + this._callbackMap.delete(messageId); + let req = new AbandonRequest(messageId); + this._send(req); + } + + /** + * The open event handler. + */ + _onOpen = () => { + this._logger.debug("Connected"); + this._socket.ondata = this._onData; + this._socket.onclose = this._onClose; + this.onOpen(); + }; + + /** + * The data event handler. Server may send multiple data events after a + * search, we want to handle them asynchonosly and in sequence. + * + * @param {TCPSocketEvent} event - The data event. + */ + _onData = async event => { + if (this._processingData) { + this._dataEventsQueue.push(event); + return; + } + this._processingData = true; + let data = event.data; + if (this._buffer) { + // Concatenate left over data from the last event with the new data. + let arr = new Uint8Array(this._buffer.byteLength + data.byteLength); + arr.set(new Uint8Array(this._buffer)); + arr.set(new Uint8Array(data), this._buffer.byteLength); + data = arr.buffer; + this._buffer = null; + } + let i = 0; + // The payload can contain multiple messages, parse it to the end. + while (data.byteLength) { + i++; + let res; + try { + res = LDAPResponse.fromBER(data); + if (typeof res == "number") { + data = data.slice(res); + continue; + } + } catch (e) { + if (e.result == Cr.NS_ERROR_CANNOT_CONVERT_DATA) { + // The remaining data doesn't form a valid LDAP message, save it for + // the next round. + this._buffer = data; + this._handleNextDataEvent(); + return; + } + throw e; + } + this._logger.debug( + `S: [${res.messageId}] ${res.constructor.name}`, + res.result.resultCode >= 0 + ? `resultCode=${res.result.resultCode} message="${res.result.diagnosticMessage}"` + : "" + ); + if (res.constructor.name == "SearchResultReference") { + this._logger.debug("References=", res.result); + } + let callback = this._callbackMap.get(res.messageId); + if (callback) { + callback(res); + if ( + !["SearchResultEntry", "SearchResultReference"].includes( + res.constructor.name + ) + ) { + this._callbackMap.delete(res.messageId); + } + } + data = data.slice(res.byteLength); + if (i % 10 == 0) { + // Prevent blocking the main thread for too long. + await new Promise(resolve => setTimeout(resolve)); + } + } + this._handleNextDataEvent(); + }; + + /** + * Process a queued data event, if there is any. + */ + _handleNextDataEvent() { + this._processingData = false; + let next = this._dataEventsQueue.shift(); + if (next) { + this._onData(next); + } + } + + /** + * The close event handler. + */ + _onClose = () => { + this._logger.debug("Connection closed"); + }; + + /** + * The error event handler. + * + * @param {TCPSocketErrorEvent} event - The error event. + */ + _onError = async event => { + this._logger.error(event); + this._socket.close(); + this.onError( + event.errorCode, + await event.target.transport?.tlsSocketControl?.asyncGetSecurityInfo() + ); + }; + + /** + * Send a message to the server. + * + * @param {LDAPMessage} msg - The message to send. + * @param {Function} callback - Callback function when receiving server responses. + * @returns {number} The id of the sent message. + */ + _send(msg, callback) { + if (callback) { + this._callbackMap.set(this._messageId, callback); + } + this._logger.debug(`C: [${this._messageId}] ${msg.constructor.name}`); + this._socket.send(msg.toBER(this._messageId)); + return this._messageId++; + } +} |