diff options
Diffstat (limited to 'comm/mailnews/addrbook/modules/LDAPMessage.jsm')
-rw-r--r-- | comm/mailnews/addrbook/modules/LDAPMessage.jsm | 632 |
1 files changed, 632 insertions, 0 deletions
diff --git a/comm/mailnews/addrbook/modules/LDAPMessage.jsm b/comm/mailnews/addrbook/modules/LDAPMessage.jsm new file mode 100644 index 0000000000..6ee7574605 --- /dev/null +++ b/comm/mailnews/addrbook/modules/LDAPMessage.jsm @@ -0,0 +1,632 @@ +/* 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 = [ + "AbandonRequest", + "BindRequest", + "UnbindRequest", + "SearchRequest", + "LDAPResponse", +]; + +var { asn1js } = ChromeUtils.importESModule("resource:///modules/asn1js.mjs"); + +/** + * A base class for all LDAP request and response messages, see + * rfc4511#section-4.1.1. + * + * @property {number} messageId - The message id. + * @property {LocalBaseBlock} protocolOp - The message content, in a data + * structure provided by asn1js. + */ +class LDAPMessage { + /** + * Encode the current message by Basic Encoding Rules (BER). + * + * @param {number} messageId - The id of the current message. + * @returns {ArrayBuffer} BER encoded message. + */ + toBER(messageId = this.messageId) { + let msg = new asn1js.Sequence({ + value: [new asn1js.Integer({ value: messageId }), this.protocolOp], + }); + return msg.toBER(); + } + + static TAG_CLASS_APPLICATION = 2; + static TAG_CLASS_CONTEXT = 3; + + /** + * Get the idBlock of [APPLICATION n]. + * + * @param {number} tagNumber - The tag number of this block. + */ + _getApplicationId(tagNumber) { + return { + tagClass: LDAPMessage.TAG_CLASS_APPLICATION, + tagNumber, + }; + } + + /** + * Get the idBlock of context-specific [n]. + * + * @param {number} tagNumber - The tag number of this block. + */ + _getContextId(tagNumber) { + return { + tagClass: LDAPMessage.TAG_CLASS_CONTEXT, + tagNumber, + }; + } + + /** + * Create a string block with context-specific [n]. + * + * @param {number} tagNumber - The tag number of this block. + * @param {string} value - The string value of this block. + * @returns {LocalBaseBlock} + */ + _contextStringBlock(tagNumber, value) { + return new asn1js.Primitive({ + idBlock: this._getContextId(tagNumber), + valueHex: new TextEncoder().encode(value), + }); + } +} + +class BindRequest extends LDAPMessage { + static APPLICATION = 0; + + AUTH_SIMPLE = 0; + AUTH_SASL = 3; + + /** + * @param {string} dn - The name to bind. + * @param {string} password - The password. + * @param {object} sasl - The SASL configs. + * @param {string} sasl.mechanism - The SASL mechanism e.g. sasl-gssapi. + * @param {Uint8Array} sasl.credentials - The credential token for the request. + */ + constructor(dn, password, sasl) { + super(); + let authBlock; + if (sasl) { + authBlock = new asn1js.Constructed({ + idBlock: this._getContextId(this.AUTH_SASL), + value: [ + new asn1js.OctetString({ + valueHex: new TextEncoder().encode(sasl.mechanism), + }), + new asn1js.OctetString({ + valueHex: sasl.credentials, + }), + ], + }); + } else { + authBlock = new asn1js.Primitive({ + idBlock: this._getContextId(this.AUTH_SIMPLE), + valueHex: new TextEncoder().encode(password), + }); + } + this.protocolOp = new asn1js.Constructed({ + // [APPLICATION 0] + idBlock: this._getApplicationId(BindRequest.APPLICATION), + value: [ + // version + new asn1js.Integer({ value: 3 }), + // name + new asn1js.OctetString({ + valueHex: new TextEncoder().encode(dn), + }), + // authentication + authBlock, + ], + }); + } +} + +class UnbindRequest extends LDAPMessage { + static APPLICATION = 2; + + protocolOp = new asn1js.Primitive({ + // [APPLICATION 2] + idBlock: this._getApplicationId(UnbindRequest.APPLICATION), + }); +} + +class SearchRequest extends LDAPMessage { + static APPLICATION = 3; + + // Filter CHOICE. + FILTER_AND = 0; + FILTER_OR = 1; + FILTER_NOT = 2; + FILTER_EQUALITY_MATCH = 3; + FILTER_SUBSTRINGS = 4; + FILTER_GREATER_OR_EQUAL = 5; + FILTER_LESS_OR_EQUAL = 6; + FILTER_PRESENT = 7; + FILTER_APPROX_MATCH = 8; + FILTER_EXTENSIBLE_MATCH = 9; + + // SubstringFilter SEQUENCE. + SUBSTRINGS_INITIAL = 0; + SUBSTRINGS_ANY = 1; + SUBSTRINGS_FINAL = 2; + + // MatchingRuleAssertion SEQUENCE. + MATCHING_RULE = 1; // optional + MATCHING_TYPE = 2; // optional + MATCHING_VALUE = 3; + MATCHING_DN = 4; // default to FALSE + + /** + * @param {string} dn - The name to search. + * @param {number} scope - The scope to search. + * @param {string} filter - The filter string, e.g. "(&(|(k1=v1)(k2=v2)))". + * @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. + */ + constructor(dn, scope, filter, attributes, timeout, limit) { + super(); + this.protocolOp = new asn1js.Constructed({ + // [APPLICATION 3] + idBlock: this._getApplicationId(SearchRequest.APPLICATION), + value: [ + // base DN + new asn1js.OctetString({ + valueHex: new TextEncoder().encode(dn), + }), + // scope + new asn1js.Enumerated({ + value: scope, + }), + // derefAliases + new asn1js.Enumerated({ + value: 0, + }), + // sizeLimit + new asn1js.Integer({ value: limit }), + // timeLimit + new asn1js.Integer({ value: timeout }), + // typesOnly + new asn1js.Boolean({ value: false }), + // filter + this._convertFilterToBlock(filter), + // attributes + new asn1js.Sequence({ + value: attributes + .split(",") + .filter(Boolean) + .map( + attr => + new asn1js.OctetString({ + valueHex: new TextEncoder().encode(attr), + }) + ), + }), + ], + }); + } + + /** + * Parse a single filter value "key=value" to [filterId, key, value]. + * + * @param {string} filter - A single filter value without parentheses. + * @returns {(number|string)[]} An array [filterId, key, value] as + * [number, string, string] + */ + _parseFilterValue(filter) { + for (let cond of [">=", "<=", "~=", ":=", "="]) { + let index = filter.indexOf(cond); + if (index > 0) { + let k = filter.slice(0, index); + let v = filter.slice(index + cond.length); + let filterId = { + ">=": this.FILTER_GREATER_OR_EQUAL, + "<=": this.FILTER_LESS_OR_EQUAL, + "~=": this.FILTER_APPROX_MATCH, + ":=": this.FILTER_EXTENSIBLE_MATCH, + }[cond]; + if (!filterId) { + if (v == "*") { + filterId = this.FILTER_PRESENT; + } else if (!v.includes("*")) { + filterId = this.FILTER_EQUALITY_MATCH; + } else { + filterId = this.FILTER_SUBSTRINGS; + v = v.split("*"); + } + } + return [filterId, k, v]; + } + } + throw Components.Exception( + `Invalid filter: ${filter}`, + Cr.NS_ERROR_ILLEGAL_VALUE + ); + } + + /** + * Parse a full filter string to an array of tokens. + * + * @param {string} filter - The full filter string to parse. + * @param {number} depth - The depth of a token. + * @param {object[]} tokens - The tokens to return. + * @param {"op"|"field"} tokens[].type - The token type. + * @param {number} tokens[].depth - The token depth. + * @param {string|string[]} tokens[].value - The token value. + */ + _parseFilter(filter, depth = 0, tokens = []) { + while (filter[0] == ")" && depth > 0) { + depth--; + filter = filter.slice(1); + } + if (filter.length == 0) { + // End of input. + return tokens; + } + if (filter[0] != "(") { + throw Components.Exception( + `Invalid filter: ${filter}`, + Cr.NS_ERROR_ILLEGAL_VALUE + ); + } + filter = filter.slice(1); + let nextOpen = filter.indexOf("("); + let nextClose = filter.indexOf(")"); + + if (nextOpen != -1 && nextOpen < nextClose) { + // Case: "OP(" + depth++; + tokens.push({ + type: "op", + depth, + value: { + "&": this.FILTER_AND, + "|": this.FILTER_OR, + "!": this.FILTER_NOT, + }[filter.slice(0, nextOpen)], + }); + this._parseFilter(filter.slice(nextOpen), depth, tokens); + } else if (nextClose != -1) { + // Case: "key=value)" + tokens.push({ + type: "field", + depth, + value: this._parseFilterValue(filter.slice(0, nextClose)), + }); + this._parseFilter(filter.slice(nextClose + 1), depth, tokens); + } + return tokens; + } + + /** + * Parse a filter string to a LocalBaseBlock. + * + * @param {string} filter - The filter string to parse. + * @returns {LocalBaseBlock} + */ + _convertFilterToBlock(filter) { + if (!filter.startsWith("(")) { + // Make sure filter is wrapped in parens, see rfc2254#section-4. + filter = `(${filter})`; + } + let tokens = this._parseFilter(filter); + let stack = []; + for (let { type, depth, value } of tokens) { + while (depth < stack.length) { + // We are done with the current block, go one level up. + stack.pop(); + } + if (type == "op") { + if (depth == stack.length) { + // We are done with the current block, go one level up. + stack.pop(); + } + // Found a new block, go one level down. + let parent = stack.slice(-1)[0]; + let curBlock = new asn1js.Constructed({ + idBlock: this._getContextId(value), + }); + stack.push(curBlock); + if (parent) { + parent.valueBlock.value.push(curBlock); + } + } else if (type == "field") { + let [tagNumber, field, fieldValue] = value; + let block; + let idBlock = this._getContextId(tagNumber); + if (tagNumber == this.FILTER_PRESENT) { + // A present filter. + block = new asn1js.Primitive({ + idBlock, + valueHex: new TextEncoder().encode(field), + }); + } else if (tagNumber == this.FILTER_EXTENSIBLE_MATCH) { + // An extensibleMatch filter is in the form of + // <type>:dn:<rule>:=<value>. We need to further parse the field. + let parts = field.split(":"); + let value = []; + if (parts.length == 3) { + // field is <type>:dn:<rule>. + if (parts[2]) { + value.push( + this._contextStringBlock(this.MATCHING_RULE, parts[2]) + ); + } + if (parts[0]) { + value.push( + this._contextStringBlock(this.MATCHING_TYPE, parts[0]) + ); + } + value.push( + this._contextStringBlock(this.MATCHING_VALUE, fieldValue) + ); + if (parts[1] == "dn") { + let dn = new asn1js.Boolean({ + value: true, + }); + dn.idBlock.tagClass = LDAPMessage.TAG_CLASS_CONTEXT; + dn.idBlock.tagNumber = this.MATCHING_DN; + value.push(dn); + } + } else if (parts.length == 2) { + // field is <type>:<rule>. + if (parts[1]) { + value.push( + this._contextStringBlock(this.MATCHING_RULE, parts[1]) + ); + } + + if (parts[0]) { + value.push( + this._contextStringBlock(this.MATCHING_TYPE, parts[0]) + ); + } + value.push( + this._contextStringBlock(this.MATCHING_VALUE, fieldValue) + ); + } else { + // field is <type>. + value = [ + this._contextStringBlock(this.MATCHING_TYPE, field), + this._contextStringBlock(this.MATCHING_VALUE, fieldValue), + ]; + } + block = new asn1js.Constructed({ + idBlock, + value, + }); + } else if (tagNumber != this.FILTER_SUBSTRINGS) { + // A filter that is not substrings filter. + block = new asn1js.Constructed({ + idBlock, + value: [ + new asn1js.OctetString({ + valueHex: new TextEncoder().encode(field), + }), + new asn1js.OctetString({ + valueHex: new TextEncoder().encode(fieldValue), + }), + ], + }); + } else { + // A substrings filter. + let substringsSeq = new asn1js.Sequence(); + block = new asn1js.Constructed({ + idBlock, + value: [ + new asn1js.OctetString({ + valueHex: new TextEncoder().encode(field), + }), + substringsSeq, + ], + }); + for (let i = 0; i < fieldValue.length; i++) { + let v = fieldValue[i]; + if (!v.length) { + // Case: * + continue; + } else if (i < fieldValue.length - 1) { + // Case: abc* + substringsSeq.valueBlock.value.push( + new asn1js.Primitive({ + idBlock: this._getContextId( + i == 0 ? this.SUBSTRINGS_INITIAL : this.SUBSTRINGS_ANY + ), + valueHex: new TextEncoder().encode(v), + }) + ); + } else { + // Case: *abc + substringsSeq.valueBlock.value.push( + new asn1js.Primitive({ + idBlock: this._getContextId(this.SUBSTRINGS_FINAL), + valueHex: new TextEncoder().encode(v), + }) + ); + } + } + } + let curBlock = stack.slice(-1)[0]; + if (curBlock) { + curBlock.valueBlock.value.push(block); + } else { + stack.push(block); + } + } + } + + return stack[0]; + } +} + +class AbandonRequest extends LDAPMessage { + static APPLICATION = 16; + + /** + * @param {string} messageId - The messageId to abandon. + */ + constructor(messageId) { + super(); + this.protocolOp = new asn1js.Integer({ value: messageId }); + // [APPLICATION 16] + this.protocolOp.idBlock.tagClass = LDAPMessage.TAG_CLASS_APPLICATION; + this.protocolOp.idBlock.tagNumber = AbandonRequest.APPLICATION; + } +} + +class LDAPResult { + /** + * @param {number} resultCode - The result code. + * @param {string} matchedDN - For certain result codes, matchedDN is the last entry used. + * @param {string} diagnosticMessage - A diagnostic message returned by the server. + */ + constructor(resultCode, matchedDN, diagnosticMessage) { + this.resultCode = resultCode; + this.matchedDN = matchedDN; + this.diagnosticMessage = diagnosticMessage; + } +} + +/** + * A base class for all LDAP response messages. + * + * @property {LDAPResult} result - The result of a response. + */ +class LDAPResponse extends LDAPMessage { + /** + * @param {number} messageId - The message id. + * @param {LocalBaseBlock} protocolOp - The message content. + * @param {number} byteLength - The byte size of this message in raw BER form. + */ + constructor(messageId, protocolOp, byteLength) { + super(); + this.messageId = messageId; + this.protocolOp = protocolOp; + this.byteLength = byteLength; + } + + /** + * Find the corresponding response class name from a tag number. + * + * @param {number} tagNumber - The tag number of a block. + * @returns {LDAPResponse} + */ + static _getResponseClassFromTagNumber(tagNumber) { + return [ + SearchResultEntry, + SearchResultDone, + SearchResultReference, + BindResponse, + ExtendedResponse, + ].find(x => x.APPLICATION == tagNumber); + } + + /** + * Decode a raw server response to LDAPResponse instance. + * + * @param {ArrayBuffer} buffer - The raw message received from the server. + * @returns {LDAPResponse} A concrete instance of LDAPResponse subclass. + */ + static fromBER(buffer) { + let decoded = asn1js.fromBER(buffer); + if (decoded.offset == -1 || decoded.result.error) { + throw Components.Exception( + decoded.result.error, + Cr.NS_ERROR_CANNOT_CONVERT_DATA + ); + } + let value = decoded.result.valueBlock.value; + let protocolOp = value[1]; + if (protocolOp.idBlock.tagClass != this.TAG_CLASS_APPLICATION) { + throw Components.Exception( + `Unexpected tagClass ${protocolOp.idBlock.tagClass}`, + Cr.NS_ERROR_ILLEGAL_VALUE + ); + } + let ProtocolOp = this._getResponseClassFromTagNumber( + protocolOp.idBlock.tagNumber + ); + if (!ProtocolOp) { + throw Components.Exception( + `Unexpected tagNumber ${protocolOp.idBlock.tagNumber}`, + Cr.NS_ERROR_ILLEGAL_VALUE + ); + } + let op = new ProtocolOp( + value[0].valueBlock.valueDec, + protocolOp, + decoded.offset + ); + op.parse(); + return op; + } + + /** + * Parse the protocolOp part of a LDAPMessage to LDAPResult. For LDAP + * responses that are simply LDAPResult, reuse this function. Other responses + * need to implement this function. + */ + parse() { + let value = this.protocolOp.valueBlock.value; + let resultCode = value[0].valueBlock.valueDec; + let matchedDN = new TextDecoder().decode(value[1].valueBlock.valueHex); + let diagnosticMessage = new TextDecoder().decode( + value[2].valueBlock.valueHex + ); + this.result = new LDAPResult(resultCode, matchedDN, diagnosticMessage); + } +} + +class BindResponse extends LDAPResponse { + static APPLICATION = 1; + + parse() { + super.parse(); + let serverSaslCredsBlock = this.protocolOp.valueBlock.value[3]; + if (serverSaslCredsBlock) { + this.result.serverSaslCreds = serverSaslCredsBlock.valueBlock.valueHex; + } + } +} + +class SearchResultEntry extends LDAPResponse { + static APPLICATION = 4; + + parse() { + let value = this.protocolOp.valueBlock.value; + let objectName = new TextDecoder().decode(value[0].valueBlock.valueHex); + let attributes = {}; + for (let attr of value[1].valueBlock.value) { + let attrValue = attr.valueBlock.value; + let type = new TextDecoder().decode(attrValue[0].valueBlock.valueHex); + let vals = attrValue[1].valueBlock.value.map(v => v.valueBlock.valueHex); + attributes[type] = vals; + } + this.result = { objectName, attributes }; + } +} + +class SearchResultDone extends LDAPResponse { + static APPLICATION = 5; +} + +class SearchResultReference extends LDAPResponse { + static APPLICATION = 19; + + parse() { + let value = this.protocolOp.valueBlock.value; + this.result = value.map(block => + new TextDecoder().decode(block.valueBlock.valueHex) + ); + } +} + +class ExtendedResponse extends LDAPResponse { + static APPLICATION = 24; +} |