rdn.js 6.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257
  1. 'use strict'
  2. const warning = require('./deprecations')
  3. const escapeValue = require('./utils/escape-value')
  4. const isDottedDecimal = require('./utils/is-dotted-decimal')
  5. /**
  6. * Implements a relative distinguished name as described in
  7. * https://www.rfc-editor.org/rfc/rfc4514.
  8. *
  9. * @example
  10. * const rdn = new RDN({cn: 'jdoe', givenName: 'John'})
  11. * rdn.toString() // 'cn=jdoe+givenName=John'
  12. */
  13. class RDN {
  14. #attributes = new Map()
  15. /**
  16. * @param {object} rdn An object of key-values to use as RDN attribute
  17. * types and attribute values. Attribute values should be strings.
  18. */
  19. constructor (rdn = {}) {
  20. for (const [key, val] of Object.entries(rdn)) {
  21. this.setAttribute({ name: key, value: val })
  22. }
  23. }
  24. get [Symbol.toStringTag] () {
  25. return 'LdapRdn'
  26. }
  27. /**
  28. * The number attributes associated with the RDN.
  29. *
  30. * @returns {number}
  31. */
  32. get size () {
  33. return this.#attributes.size
  34. }
  35. /**
  36. * Very naive equality check against another RDN instance. In short, if they
  37. * do not have the exact same key names with the exact same values, then
  38. * this check will return `false`.
  39. *
  40. * @param {RDN} rdn
  41. *
  42. * @returns {boolean}
  43. *
  44. * @todo Should implement support for the attribute types listed in https://www.rfc-editor.org/rfc/rfc4514#section-3
  45. */
  46. equals (rdn) {
  47. if (Object.prototype.toString.call(rdn) !== '[object LdapRdn]') {
  48. return false
  49. }
  50. if (this.size !== rdn.size) {
  51. return false
  52. }
  53. for (const key of this.keys()) {
  54. if (rdn.has(key) === false) return false
  55. if (this.getValue(key) !== rdn.getValue(key)) return false
  56. }
  57. return true
  58. }
  59. /**
  60. * The value associated with the given attribute name.
  61. *
  62. * @param {string} name An attribute name associated with the RDN.
  63. *
  64. * @returns {*}
  65. */
  66. getValue (name) {
  67. return this.#attributes.get(name)?.value
  68. }
  69. /**
  70. * Determine if the RDN has a specific attribute assigned.
  71. *
  72. * @param {string} name The name of the attribute.
  73. *
  74. * @returns {boolean}
  75. */
  76. has (name) {
  77. return this.#attributes.has(name)
  78. }
  79. /**
  80. * All attribute names associated with the RDN.
  81. *
  82. * @returns {IterableIterator<string>}
  83. */
  84. keys () {
  85. return this.#attributes.keys()
  86. }
  87. /**
  88. * Define an attribute type and value on the RDN.
  89. *
  90. * @param {string} name
  91. * @param {string | import('@ldapjs/asn1').BerReader} value
  92. * @param {object} options Deprecated. All options will be ignored.
  93. *
  94. * @throws If any parameter is invalid.
  95. */
  96. setAttribute ({ name, value, options = {} }) {
  97. if (typeof name !== 'string') {
  98. throw Error('name must be a string')
  99. }
  100. const valType = Object.prototype.toString.call(value)
  101. if (typeof value !== 'string' && valType !== '[object BerReader]') {
  102. throw Error('value must be a string or BerReader')
  103. }
  104. if (Object.prototype.toString.call(options) !== '[object Object]') {
  105. throw Error('options must be an object')
  106. }
  107. const startsWithAlpha = str => /^[a-zA-Z]/.test(str) === true
  108. if (startsWithAlpha(name) === false && isDottedDecimal(name) === false) {
  109. throw Error('attribute name must start with an ASCII alpha character or be a numeric OID')
  110. }
  111. const attr = { value, name }
  112. for (const [key, val] of Object.entries(options)) {
  113. warning.emit('LDAP_DN_DEP_001')
  114. if (key === 'value') continue
  115. attr[key] = val
  116. }
  117. this.#attributes.set(name, attr)
  118. }
  119. /**
  120. * Convert the RDN to a string representation. If an attribute value is
  121. * an instance of `BerReader`, the value will be encoded appropriately.
  122. *
  123. * @example Dotted Decimal Type
  124. * const rdn = new RDN({
  125. * cn: '#foo',
  126. * '1.3.6.1.4.1.1466.0': '#04024869'
  127. * })
  128. * rnd.toString()
  129. * // => 'cn=\23foo+1.3.6.1.4.1.1466.0=#04024869'
  130. *
  131. * @example Unescaped Value
  132. * const rdn = new RDN({
  133. * cn: '#foo'
  134. * })
  135. * rdn.toString({ unescaped: true })
  136. * // => 'cn=#foo'
  137. *
  138. * @param {object} [options]
  139. * @param {boolean} [options.unescaped=false] Return the unescaped version
  140. * of the RDN string.
  141. *
  142. * @returns {string}
  143. */
  144. toString ({ unescaped = false } = {}) {
  145. let result = ''
  146. const isHexEncodedValue = val => /^#([0-9a-fA-F]{2})+$/.test(val) === true
  147. for (const entry of this.#attributes.values()) {
  148. result += entry.name + '='
  149. if (isHexEncodedValue(entry.value)) {
  150. result += entry.value
  151. } else if (Object.prototype.toString.call(entry.value) === '[object BerReader]') {
  152. let encoded = '#'
  153. for (const byte of entry.value.buffer) {
  154. encoded += Number(byte).toString(16).padStart(2, '0')
  155. }
  156. result += encoded
  157. } else {
  158. result += unescaped === false ? escapeValue(entry.value) : entry.value
  159. }
  160. result += '+'
  161. }
  162. return result.substring(0, result.length - 1)
  163. }
  164. /**
  165. * @returns {string}
  166. *
  167. * @deprecated Use {@link toString}.
  168. */
  169. format () {
  170. // If we decide to add back support for this, we should do it as
  171. // `.toStringWithFormatting(options)`.
  172. warning.emit('LDAP_DN_DEP_002')
  173. return this.toString()
  174. }
  175. /**
  176. * @param {string} name
  177. * @param {string} value
  178. * @param {object} options
  179. *
  180. * @deprecated Use {@link setAttribute}.
  181. */
  182. set (name, value, options) {
  183. warning.emit('LDAP_DN_DEP_003')
  184. this.setAttribute({ name, value, options })
  185. }
  186. /**
  187. * Determine if an object is an instance of {@link RDN} or is at least
  188. * a RDN-like object. It is safer to perform a `toString` check.
  189. *
  190. * @example Valid Instance
  191. * const Rdn = new RDN()
  192. * RDN.isRdn(rdn) // true
  193. *
  194. * @example RDN-like Instance
  195. * const rdn = { name: 'cn', value: 'foo' }
  196. * RDN.isRdn(rdn) // true
  197. *
  198. * @example Preferred Check
  199. * let rdn = new RDN()
  200. * Object.prototype.toString.call(rdn) === '[object LdapRdn]' // true
  201. *
  202. * dn = { name: 'cn', value: 'foo' }
  203. * Object.prototype.toString.call(dn) === '[object LdapRdn]' // false
  204. *
  205. * @param {object} rdn
  206. * @returns {boolean}
  207. */
  208. static isRdn (rdn) {
  209. if (Object.prototype.toString.call(rdn) === '[object LdapRdn]') {
  210. return true
  211. }
  212. const isObject = Object.prototype.toString.call(rdn) === '[object Object]'
  213. if (isObject === false) {
  214. return false
  215. }
  216. if (typeof rdn.name === 'string' && typeof rdn.value === 'string') {
  217. return true
  218. }
  219. for (const value of Object.values(rdn)) {
  220. if (
  221. typeof value !== 'string' &&
  222. Object.prototype.toString.call(value) !== '[object BerReader]'
  223. ) return false
  224. }
  225. return true
  226. }
  227. }
  228. module.exports = RDN