123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257 |
- '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<string>}
- */
- 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
|