'use strict' const warning = require('./deprecations') const escapeValue = require('./utils/escape-value') const isDottedDecimal = require('./utils/is-dotted-decimal') /** * Implements a relative distinguished name as described in * https://www.rfc-editor.org/rfc/rfc4514. * * @example * const rdn = new RDN({cn: 'jdoe', givenName: 'John'}) * rdn.toString() // 'cn=jdoe+givenName=John' */ class RDN { #attributes = new Map() /** * @param {object} rdn An object of key-values to use as RDN attribute * types and attribute values. Attribute values should be strings. */ constructor (rdn = {}) { for (const [key, val] of Object.entries(rdn)) { this.setAttribute({ name: key, value: val }) } } get [Symbol.toStringTag] () { return 'LdapRdn' } /** * The number attributes associated with the RDN. * * @returns {number} */ get size () { return this.#attributes.size } /** * Very naive equality check against another RDN instance. In short, if they * do not have the exact same key names with the exact same values, then * this check will return `false`. * * @param {RDN} rdn * * @returns {boolean} * * @todo Should implement support for the attribute types listed in https://www.rfc-editor.org/rfc/rfc4514#section-3 */ equals (rdn) { if (Object.prototype.toString.call(rdn) !== '[object LdapRdn]') { return false } if (this.size !== rdn.size) { return false } for (const key of this.keys()) { if (rdn.has(key) === false) return false if (this.getValue(key) !== rdn.getValue(key)) return false } return true } /** * The value associated with the given attribute name. * * @param {string} name An attribute name associated with the RDN. * * @returns {*} */ getValue (name) { return this.#attributes.get(name)?.value } /** * Determine if the RDN has a specific attribute assigned. * * @param {string} name The name of the attribute. * * @returns {boolean} */ has (name) { return this.#attributes.has(name) } /** * All attribute names associated with the RDN. * * @returns {IterableIterator} */ keys () { return this.#attributes.keys() } /** * Define an attribute type and value on the RDN. * * @param {string} name * @param {string | import('@ldapjs/asn1').BerReader} value * @param {object} options Deprecated. All options will be ignored. * * @throws If any parameter is invalid. */ setAttribute ({ name, value, options = {} }) { if (typeof name !== 'string') { throw Error('name must be a string') } const valType = Object.prototype.toString.call(value) if (typeof value !== 'string' && valType !== '[object BerReader]') { throw Error('value must be a string or BerReader') } if (Object.prototype.toString.call(options) !== '[object Object]') { throw Error('options must be an object') } const startsWithAlpha = str => /^[a-zA-Z]/.test(str) === true if (startsWithAlpha(name) === false && isDottedDecimal(name) === false) { throw Error('attribute name must start with an ASCII alpha character or be a numeric OID') } const attr = { value, name } for (const [key, val] of Object.entries(options)) { warning.emit('LDAP_DN_DEP_001') if (key === 'value') continue attr[key] = val } this.#attributes.set(name, attr) } /** * Convert the RDN to a string representation. If an attribute value is * an instance of `BerReader`, the value will be encoded appropriately. * * @example Dotted Decimal Type * const rdn = new RDN({ * cn: '#foo', * '1.3.6.1.4.1.1466.0': '#04024869' * }) * rnd.toString() * // => 'cn=\23foo+1.3.6.1.4.1.1466.0=#04024869' * * @example Unescaped Value * const rdn = new RDN({ * cn: '#foo' * }) * rdn.toString({ unescaped: true }) * // => 'cn=#foo' * * @param {object} [options] * @param {boolean} [options.unescaped=false] Return the unescaped version * of the RDN string. * * @returns {string} */ toString ({ unescaped = false } = {}) { let result = '' const isHexEncodedValue = val => /^#([0-9a-fA-F]{2})+$/.test(val) === true for (const entry of this.#attributes.values()) { result += entry.name + '=' if (isHexEncodedValue(entry.value)) { result += entry.value } else if (Object.prototype.toString.call(entry.value) === '[object BerReader]') { let encoded = '#' for (const byte of entry.value.buffer) { encoded += Number(byte).toString(16).padStart(2, '0') } result += encoded } else { result += unescaped === false ? escapeValue(entry.value) : entry.value } result += '+' } return result.substring(0, result.length - 1) } /** * @returns {string} * * @deprecated Use {@link toString}. */ format () { // If we decide to add back support for this, we should do it as // `.toStringWithFormatting(options)`. warning.emit('LDAP_DN_DEP_002') return this.toString() } /** * @param {string} name * @param {string} value * @param {object} options * * @deprecated Use {@link setAttribute}. */ set (name, value, options) { warning.emit('LDAP_DN_DEP_003') this.setAttribute({ name, value, options }) } /** * Determine if an object is an instance of {@link RDN} or is at least * a RDN-like object. It is safer to perform a `toString` check. * * @example Valid Instance * const Rdn = new RDN() * RDN.isRdn(rdn) // true * * @example RDN-like Instance * const rdn = { name: 'cn', value: 'foo' } * RDN.isRdn(rdn) // true * * @example Preferred Check * let rdn = new RDN() * Object.prototype.toString.call(rdn) === '[object LdapRdn]' // true * * dn = { name: 'cn', value: 'foo' } * Object.prototype.toString.call(dn) === '[object LdapRdn]' // false * * @param {object} rdn * @returns {boolean} */ static isRdn (rdn) { if (Object.prototype.toString.call(rdn) === '[object LdapRdn]') { return true } const isObject = Object.prototype.toString.call(rdn) === '[object Object]' if (isObject === false) { return false } if (typeof rdn.name === 'string' && typeof rdn.value === 'string') { return true } for (const value of Object.values(rdn)) { if ( typeof value !== 'string' && Object.prototype.toString.call(value) !== '[object BerReader]' ) return false } return true } } module.exports = RDN