index.js 7.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344
  1. 'use strict'
  2. const { core: { LBER_SET } } = require('@ldapjs/protocol')
  3. const {
  4. BerTypes,
  5. BerReader,
  6. BerWriter
  7. } = require('@ldapjs/asn1')
  8. const warning = require('./lib/deprecations')
  9. /**
  10. * Represents an LDAP attribute and its associated values as defined by
  11. * https://www.rfc-editor.org/rfc/rfc4512#section-2.5.
  12. */
  13. class Attribute {
  14. #buffers = []
  15. #type
  16. /**
  17. * @param {object} options
  18. * @param {string} [options.type=''] The name of the attribute, e.g. "cn" for
  19. * the common name attribute. For binary attributes, include the `;binary`
  20. * option, e.g. `foo;binary`.
  21. * @param {string|string[]} [options.values] Either a single value for the
  22. * attribute, or a set of values for the attribute.
  23. */
  24. constructor (options = {}) {
  25. if (options.type && typeof (options.type) !== 'string') {
  26. throw TypeError('options.type must be a string')
  27. }
  28. this.type = options.type || ''
  29. const values = options.values || options.vals || []
  30. if (options.vals) {
  31. warning.emit('LDAP_ATTRIBUTE_DEP_001')
  32. }
  33. this.values = values
  34. }
  35. get [Symbol.toStringTag] () {
  36. return 'LdapAttribute'
  37. }
  38. /**
  39. * A copy of the buffers that represent the values for the attribute.
  40. *
  41. * @returns {Buffer[]}
  42. */
  43. get buffers () {
  44. return this.#buffers.slice(0)
  45. }
  46. /**
  47. * Serializes the attribute to a plain JavaScript object representation.
  48. *
  49. * @returns {object}
  50. */
  51. get pojo () {
  52. return {
  53. type: this.type,
  54. values: this.values
  55. }
  56. }
  57. /**
  58. * The attribute name as provided during construction.
  59. *
  60. * @returns {string}
  61. */
  62. get type () {
  63. return this.#type
  64. }
  65. /**
  66. * Set the attribute name.
  67. *
  68. * @param {string} name
  69. */
  70. set type (name) {
  71. this.#type = name
  72. }
  73. /**
  74. * The set of attribute values as strings.
  75. *
  76. * @returns {string[]}
  77. */
  78. get values () {
  79. const encoding = _bufferEncoding(this.#type)
  80. return this.#buffers.map(function (v) {
  81. return v.toString(encoding)
  82. })
  83. }
  84. /**
  85. * Set the attribute's associated values. This will replace any values set
  86. * at construction time.
  87. *
  88. * @param {string|string[]} vals
  89. */
  90. set values (vals) {
  91. if (Array.isArray(vals) === false) {
  92. return this.addValue(vals)
  93. }
  94. for (const value of vals) {
  95. this.addValue(value)
  96. }
  97. }
  98. /**
  99. * Use {@link values} instead.
  100. *
  101. * @deprecated
  102. * @returns {string[]}
  103. */
  104. get vals () {
  105. warning.emit('LDAP_ATTRIBUTE_DEP_003')
  106. return this.values
  107. }
  108. /**
  109. * Use {@link values} instead.
  110. *
  111. * @deprecated
  112. * @param {string|string[]} values
  113. */
  114. set vals (values) {
  115. warning.emit('LDAP_ATTRIBUTE_DEP_003')
  116. this.values = values
  117. }
  118. /**
  119. * Append a new value, or set of values, to the current set of values
  120. * associated with the attributes.
  121. *
  122. * @param {string|string[]} value
  123. */
  124. addValue (value) {
  125. if (Buffer.isBuffer(value)) {
  126. this.#buffers.push(value)
  127. } else {
  128. this.#buffers.push(
  129. Buffer.from(value + '', _bufferEncoding(this.#type))
  130. )
  131. }
  132. }
  133. /**
  134. * Replaces instance properties with those found in a given BER.
  135. *
  136. * @param {import('@ldapjs/asn1').BerReader} ber
  137. *
  138. * @deprecated Use {@link fromBer} instead.
  139. */
  140. parse (ber) {
  141. const attr = Attribute.fromBer(ber)
  142. this.#type = attr.type
  143. this.values = attr.values
  144. }
  145. /**
  146. * Convert the {@link Attribute} instance to a {@link BerReader} capable of
  147. * being used in an LDAP message.
  148. *
  149. * @returns {BerReader}
  150. */
  151. toBer () {
  152. const ber = new BerWriter()
  153. ber.startSequence()
  154. ber.writeString(this.type)
  155. ber.startSequence(LBER_SET)
  156. if (this.#buffers.length > 0) {
  157. for (const buffer of this.#buffers) {
  158. ber.writeByte(BerTypes.OctetString)
  159. ber.writeLength(buffer.length)
  160. ber.appendBuffer(buffer)
  161. }
  162. } else {
  163. ber.writeStringArray([])
  164. }
  165. ber.endSequence()
  166. ber.endSequence()
  167. return new BerReader(ber.buffer)
  168. }
  169. toJSON () {
  170. return this.pojo
  171. }
  172. /**
  173. * Given two {@link Attribute} instances, determine if they are equal or
  174. * different.
  175. *
  176. * @param {Attribute} attr1 The first object to compare.
  177. * @param {Attribute} attr2 The second object to compare.
  178. *
  179. * @returns {number} `0` if the attributes are equal in value, `-1` if
  180. * `attr1` should come before `attr2` when sorted, and `1` if `attr2` should
  181. * come before `attr1` when sorted.
  182. *
  183. * @throws When either input object is not an {@link Attribute}.
  184. */
  185. static compare (attr1, attr2) {
  186. if (Attribute.isAttribute(attr1) === false || Attribute.isAttribute(attr2) === false) {
  187. throw TypeError('can only compare Attribute instances')
  188. }
  189. if (attr1.type < attr2.type) return -1
  190. if (attr1.type > attr2.type) return 1
  191. const aValues = attr1.values
  192. const bValues = attr2.values
  193. if (aValues.length < bValues.length) return -1
  194. if (aValues.length > bValues.length) return 1
  195. for (let i = 0; i < aValues.length; i++) {
  196. if (aValues[i] < bValues[i]) return -1
  197. if (aValues[i] > bValues[i]) return 1
  198. }
  199. return 0
  200. }
  201. /**
  202. * Read a BER representation of an attribute, and its values, and
  203. * create a new {@link Attribute} instance. The BER must start
  204. * at the beginning of a sequence.
  205. *
  206. * @param {import('@ldapjs/asn1').BerReader} ber
  207. *
  208. * @returns {Attribute}
  209. */
  210. static fromBer (ber) {
  211. ber.readSequence()
  212. const type = ber.readString()
  213. const values = []
  214. // If the next byte represents a BER "SET" sequence...
  215. if (ber.peek() === LBER_SET) {
  216. // .. read that sequence ...
  217. /* istanbul ignore else */
  218. if (ber.readSequence(LBER_SET)) {
  219. const end = ber.offset + ber.length
  220. // ... and read all values in that set.
  221. while (ber.offset < end) {
  222. values.push(
  223. ber.readString(BerTypes.OctetString, true)
  224. )
  225. }
  226. }
  227. }
  228. const result = new Attribute({
  229. type,
  230. values
  231. })
  232. return result
  233. }
  234. /**
  235. * Given an object of attribute types mapping to attribute values, construct
  236. * a set of Attributes.
  237. *
  238. * @param {object} obj Each key is an attribute type, and each value is an
  239. * attribute value or set of values.
  240. *
  241. * @returns {Attribute[]}
  242. *
  243. * @throws If an attribute cannot be constructed correctly.
  244. */
  245. static fromObject (obj) {
  246. const attributes = []
  247. for (const [key, value] of Object.entries(obj)) {
  248. if (Array.isArray(value) === true) {
  249. attributes.push(new Attribute({
  250. type: key,
  251. values: value
  252. }))
  253. } else {
  254. attributes.push(new Attribute({
  255. type: key,
  256. values: [value]
  257. }))
  258. }
  259. }
  260. return attributes
  261. }
  262. /**
  263. * Determine if an object represents an {@link Attribute}.
  264. *
  265. * @param {object} attr The object to check. It can be an instance of
  266. * {@link Attribute} or a plain JavaScript object that looks like an
  267. * {@link Attribute} and can be passed to the constructor to create one.
  268. *
  269. * @returns {boolean}
  270. */
  271. static isAttribute (attr) {
  272. if (typeof attr !== 'object') {
  273. return false
  274. }
  275. if (Object.prototype.toString.call(attr) === '[object LdapAttribute]') {
  276. return true
  277. }
  278. const typeOk = typeof attr.type === 'string'
  279. let valuesOk = Array.isArray(attr.values)
  280. if (valuesOk === true) {
  281. for (const val of attr.values) {
  282. if (typeof val !== 'string' && Buffer.isBuffer(val) === false) {
  283. valuesOk = false
  284. break
  285. }
  286. }
  287. }
  288. if (typeOk === true && valuesOk === true) {
  289. return true
  290. }
  291. return false
  292. }
  293. }
  294. module.exports = Attribute
  295. /**
  296. * Determine the encoding for values based upon whether the binary
  297. * option is set on the attribute.
  298. *
  299. * @param {string} type
  300. *
  301. * @returns {string} Either "utf8" for a plain string value, or "base64" for
  302. * a binary attribute.
  303. *
  304. * @private
  305. */
  306. function _bufferEncoding (type) {
  307. return /;binary$/.test(type) ? 'base64' : 'utf8'
  308. }