First commit

This commit is contained in:
2025-10-08 11:12:59 -04:00
commit b0605a28a9
820 changed files with 100317 additions and 0 deletions

11
node_modules/@ldapjs/dn/lib/deprecations.js generated vendored Normal file
View File

@ -0,0 +1,11 @@
'use strict'
const warning = require('process-warning')()
const clazz = 'LdapjsDnWarning'
warning.create(clazz, 'LDAP_DN_DEP_001', 'attribute options is deprecated and are ignored')
warning.create(clazz, 'LDAP_DN_DEP_002', '.format() is deprecated. Use .toString() instead')
warning.create(clazz, 'LDAP_DN_DEP_003', '.set() is deprecated. Use .setAttribute() instead')
warning.create(clazz, 'LDAP_DN_DEP_004', '.setFormat() is deprecated. Options will be ignored')
module.exports = warning

336
node_modules/@ldapjs/dn/lib/dn.js generated vendored Normal file
View File

@ -0,0 +1,336 @@
'use strict'
const warning = require('./deprecations')
const RDN = require('./rdn')
const parseString = require('./utils/parse-string')
/**
* Implements distinguished name strings as described in
* https://www.rfc-editor.org/rfc/rfc4514 as an object.
* This is the primary implementation for parsing and generating DN strings.
*
* @example
* const dn = new DN({rdns: [{cn: 'jdoe', givenName: 'John'}] })
* dn.toString() // 'cn=jdoe+givenName=John'
*/
class DN {
#rdns = []
/**
* @param {object} input
* @param {RDN[]} [input.rdns=[]] A set of RDN objects that define the DN.
* Remember that DNs are in reverse domain order. Thus, the target RDN must
* be the first item and the top-level RDN the last item.
*
* @throws When the provided `rdns` array is invalid.
*/
constructor ({ rdns = [] } = {}) {
if (Array.isArray(rdns) === false) {
throw Error('rdns must be an array')
}
const hasNonRdn = rdns.some(
r => RDN.isRdn(r) === false
)
if (hasNonRdn === true) {
throw Error('rdns must be an array of RDN objects')
}
Array.prototype.push.apply(
this.#rdns,
rdns.map(r => {
if (Object.prototype.toString.call(r) === '[object LdapRdn]') {
return r
}
return new RDN(r)
})
)
}
get [Symbol.toStringTag] () {
return 'LdapDn'
}
/**
* The number of RDNs that make up the DN.
*
* @returns {number}
*/
get length () {
return this.#rdns.length
}
/**
* Determine if the current instance is the child of another DN instance or
* DN string.
*
* @param {DN|string} dn
*
* @returns {boolean}
*/
childOf (dn) {
if (typeof dn === 'string') {
const parsedDn = DN.fromString(dn)
return parsedDn.parentOf(this)
}
return dn.parentOf(this)
}
/**
* Get a new instance that is a replica of the current instance.
*
* @returns {DN}
*/
clone () {
return new DN({ rdns: this.#rdns })
}
/**
* Determine if the instance is equal to another DN.
*
* @param {DN|string} dn
*
* @returns {boolean}
*/
equals (dn) {
if (typeof dn === 'string') {
const parsedDn = DN.fromString(dn)
return parsedDn.equals(this)
}
if (this.length !== dn.length) return false
for (let i = 0; i < this.length; i += 1) {
if (this.#rdns[i].equals(dn.rdnAt(i)) === false) {
return false
}
}
return true
}
/**
* @deprecated Use .toString() instead.
*
* @returns {string}
*/
format () {
warning.emit('LDAP_DN_DEP_002')
return this.toString()
}
/**
* Determine if the instance has any RDNs defined.
*
* @returns {boolean}
*/
isEmpty () {
return this.#rdns.length === 0
}
/**
* Get a DN representation of the parent of this instance.
*
* @returns {DN|undefined}
*/
parent () {
if (this.length === 0) return undefined
const save = this.shift()
const dn = new DN({ rdns: this.#rdns })
this.unshift(save)
return dn
}
/**
* Determine if the instance is the parent of a given DN instance or DN
* string.
*
* @param {DN|string} dn
*
* @returns {boolean}
*/
parentOf (dn) {
if (typeof dn === 'string') {
const parsedDn = DN.fromString(dn)
return this.parentOf(parsedDn)
}
if (this.length >= dn.length) {
// If we have more RDNs in our set then we must be a descendent at least.
return false
}
const numberOfElementsDifferent = dn.length - this.length
for (let i = this.length - 1; i >= 0; i -= 1) {
const myRdn = this.#rdns[i]
const theirRdn = dn.rdnAt(i + numberOfElementsDifferent)
if (myRdn.equals(theirRdn) === false) {
return false
}
}
return true
}
/**
* Removes the last RDN from the list and returns it. This alters the
* instance.
*
* @returns {RDN}
*/
pop () {
return this.#rdns.pop()
}
/**
* Adds a new RDN to the end of the list (i.e. the "top most" RDN in the
* directory path) and returns the new RDN count.
*
* @param {RDN} rdn
*
* @returns {number}
*
* @throws When the input is not a valid RDN.
*/
push (rdn) {
if (Object.prototype.toString.call(rdn) !== '[object LdapRdn]') {
throw Error('rdn must be a RDN instance')
}
return this.#rdns.push(rdn)
}
/**
* Return the RDN at the provided index in the list of RDNs associated with
* this instance.
*
* @param {number} index
*
* @returns {RDN}
*/
rdnAt (index) {
return this.#rdns[index]
}
/**
* Reverse the RDNs list such that the first element becomes the last, and
* the last becomes the first. This is useful when the RDNs were added in the
* opposite order of how they should have been.
*
* This is an in-place operation. The instance is changed as a result of
* this operation.
*
* @returns {DN} The current instance (i.e. this method is chainable).
*/
reverse () {
this.#rdns.reverse()
return this
}
/**
* @deprecated Formatting options are not supported.
*/
setFormat () {
warning.emit('LDAP_DN_DEP_004')
}
/**
* Remove the first RDN from the set of RDNs and return it.
*
* @returns {RDN}
*/
shift () {
return this.#rdns.shift()
}
/**
* Render the DN instance as a spec compliant DN string.
*
* @returns {string}
*/
toString () {
let result = ''
for (const rdn of this.#rdns) {
const rdnString = rdn.toString()
result += `,${rdnString}`
}
return result.substring(1)
}
/**
* Adds an RDN to the beginning of the RDN list and returns the new length.
*
* @param {RDN} rdn
*
* @returns {number}
*
* @throws When the RDN is invalid.
*/
unshift (rdn) {
if (Object.prototype.toString.call(rdn) !== '[object LdapRdn]') {
throw Error('rdn must be a RDN instance')
}
return this.#rdns.unshift(rdn)
}
/**
* Determine if an object is an instance of {@link DN} or is at least
* a DN-like object. It is safer to perform a `toString` check.
*
* @example Valid Instance
* const dn = new DN()
* DN.isDn(dn) // true
*
* @example DN-like Instance
* let dn = { rdns: [{name: 'cn', value: 'foo'}] }
* DN.isDn(dn) // true
*
* dn = { rdns: [{cn: 'foo', sn: 'bar'}, {dc: 'example'}, {dc: 'com'}]}
* DN.isDn(dn) // true
*
* @example Preferred Check
* let dn = new DN()
* Object.prototype.toString.call(dn) === '[object LdapDn]' // true
*
* dn = { rdns: [{name: 'cn', value: 'foo'}] }
* Object.prototype.toString.call(dn) === '[object LdapDn]' // false
*
* @param {object} dn
* @returns {boolean}
*/
static isDn (dn) {
if (Object.prototype.toString.call(dn) === '[object LdapDn]') {
return true
}
if (
Object.prototype.toString.call(dn) !== '[object Object]' ||
Array.isArray(dn.rdns) === false
) {
return false
}
if (dn.rdns.some(dn => RDN.isRdn(dn) === false) === true) {
return false
}
return true
}
/**
* Parses a DN string and returns a new {@link DN} instance.
*
* @example
* const dn = DN.fromString('cn=foo,dc=example,dc=com')
* DN.isDn(dn) // true
*
* @param {string} dnString
*
* @returns {DN}
*
* @throws If the string is not parseable.
*/
static fromString (dnString) {
const rdns = parseString(dnString)
return new DN({ rdns })
}
}
module.exports = DN

527
node_modules/@ldapjs/dn/lib/dn.test.js generated vendored Normal file
View File

@ -0,0 +1,527 @@
'use strict'
const tap = require('tap')
const warning = require('./deprecations')
const RDN = require('./rdn')
const DN = require('./dn')
// Silence the standard warning logs. We will test the messages explicitly.
process.removeAllListeners('warning')
tap.test('constructor', t => {
t.test('throws for non-array', async t => {
t.throws(
() => new DN({ rdns: 42 }),
Error('rdns must be an array')
)
})
t.test('throws for non-rdn in array', async t => {
const rdns = [
new RDN(),
{ 'non-string-value': 42 },
new RDN()
]
t.throws(
() => new DN({ rdns })
)
})
t.test('handles mixed array', async t => {
const rdns = [
{ cn: 'foo' },
new RDN({ dc: 'example' }),
new RDN({ dc: 'com' })
]
const dn = new DN({ rdns })
t.equal(dn.length, 3)
t.equal(dn.toString(), 'cn=foo,dc=example,dc=com')
})
t.end()
})
tap.test('childOf', t => {
t.test('false if we are shallower', async t => {
const dn = new DN({
rdns: [
new RDN({ ou: 'people' }),
new RDN({ dc: 'example' }),
new RDN({ dc: 'com' })
]
})
const target = new DN({
rdns: [
new RDN({ cn: 'foo' }),
new RDN({ ou: 'people' }),
new RDN({ dc: 'example' }),
new RDN({ dc: 'com' })
]
})
t.equal(dn.childOf(target), false)
})
t.test('false if differing path', async t => {
const dn = new DN({
rdns: [
new RDN({ ou: 'people' }),
new RDN({ dc: 'example' }),
new RDN({ dc: 'com' })
]
})
const target = new DN({
rdns: [
new RDN({ dc: 'ldapjs' }),
new RDN({ dc: 'com' })
]
})
t.equal(dn.childOf(target), false)
})
t.test('true if we are a child', async t => {
const dn = new DN({
rdns: [
new RDN({ ou: 'people' }),
new RDN({ dc: 'example' }),
new RDN({ dc: 'com' })
]
})
const target = new DN({
rdns: [
new RDN({ dc: 'example' }),
new RDN({ dc: 'com' })
]
})
t.equal(dn.childOf(target), true)
})
t.test('handles string input', async t => {
const dn = new DN({
rdns: [
new RDN({ ou: 'people' }),
new RDN({ dc: 'example' }),
new RDN({ dc: 'com' })
]
})
const target = 'dc=example,dc=com'
t.equal(dn.childOf(target), true)
})
t.end()
})
tap.test('clone', t => {
t.test('returns a copy', async t => {
const rdns = [new RDN({ cn: 'foo' })]
const src = new DN({ rdns })
const clone = src.clone()
t.equal(src.length, clone.length)
t.equal(src.toString(), clone.toString())
})
t.end()
})
tap.test('equals', t => {
t.test('false for non-equal length', async t => {
const dn = new DN({
rdns: [
new RDN({ ou: 'people' }),
new RDN({ dc: 'example' }),
new RDN({ dc: 'com' })
]
})
const target = new DN({
rdns: [
new RDN({ dc: 'example' }),
new RDN({ dc: 'com' })
]
})
t.equal(dn.equals(target), false)
})
t.test('false for non-equal paths', async t => {
const dn = new DN({
rdns: [
new RDN({ ou: 'people' }),
new RDN({ dc: 'example' }),
new RDN({ dc: 'com' })
]
})
const target = new DN({
rdns: [
new RDN({ ou: 'computers' }),
new RDN({ dc: 'example' }),
new RDN({ dc: 'com' })
]
})
t.equal(dn.equals(target), false)
})
t.test('true for equal paths', async t => {
const dn = new DN({
rdns: [
new RDN({ ou: 'people' }),
new RDN({ dc: 'example' }),
new RDN({ dc: 'com' })
]
})
const target = new DN({
rdns: [
new RDN({ ou: 'people' }),
new RDN({ dc: 'example' }),
new RDN({ dc: 'com' })
]
})
t.equal(dn.equals(target), true)
})
t.test('handles string input', async t => {
const dn = new DN({
rdns: [
new RDN({ ou: 'people' }),
new RDN({ dc: 'example' }),
new RDN({ dc: 'com' })
]
})
const target = 'ou=people,dc=example,dc=com'
t.equal(dn.equals(target), true)
})
t.end()
})
tap.test('format', t => {
t.test('emits warning', t => {
process.on('warning', handler)
t.teardown(async () => {
process.removeListener('warning', handler)
warning.emitted.set('LDAP_DN_DEP_002', false)
})
const rdns = [{ cn: 'foo' }]
const dnString = (new DN({ rdns })).format()
t.equal(dnString, 'cn=foo')
function handler (error) {
t.equal(error.message, '.format() is deprecated. Use .toString() instead')
t.end()
}
})
t.end()
})
tap.test('isEmpty', t => {
t.test('returns correct result', async t => {
let dn = new DN()
t.equal(dn.isEmpty(), true)
dn = new DN({
rdns: [new RDN({ cn: 'foo' })]
})
t.equal(dn.isEmpty(), false)
})
t.end()
})
tap.test('parent', t => {
t.test('undefined for an empty DN', async t => {
const dn = new DN()
const parent = dn.parent()
t.equal(parent, undefined)
})
t.test('returns correct DN', async t => {
const dn = new DN({
rdns: [
new RDN({ cn: 'jdoe', givenName: 'John' }),
new RDN({ ou: 'people' }),
new RDN({ dc: 'example' }),
new RDN({ dc: 'com' })
]
})
const parent = dn.parent()
t.equal(parent.toString(), 'ou=people,dc=example,dc=com')
})
t.end()
})
tap.test('parentOf', t => {
t.test('false if we are deeper', async t => {
const target = new DN({
rdns: [
new RDN({ ou: 'people' }),
new RDN({ dc: 'example' }),
new RDN({ dc: 'com' })
]
})
const dn = new DN({
rdns: [
new RDN({ cn: 'foo' }),
new RDN({ ou: 'people' }),
new RDN({ dc: 'example' }),
new RDN({ dc: 'com' })
]
})
t.equal(dn.parentOf(target), false)
})
t.test('false if differing path', async t => {
const target = new DN({
rdns: [
new RDN({ ou: 'people' }),
new RDN({ dc: 'example' }),
new RDN({ dc: 'com' })
]
})
const dn = new DN({
rdns: [
new RDN({ dc: 'ldapjs' }),
new RDN({ dc: 'com' })
]
})
t.equal(dn.parentOf(target), false)
})
t.test('true if we are a parent', async t => {
const target = new DN({
rdns: [
new RDN({ ou: 'people' }),
new RDN({ dc: 'example' }),
new RDN({ dc: 'com' })
]
})
const dn = new DN({
rdns: [
new RDN({ dc: 'example' }),
new RDN({ dc: 'com' })
]
})
t.equal(dn.parentOf(target), true)
})
t.test('handles string input', async t => {
const dn = new DN({
rdns: [
new RDN({ dc: 'example' }),
new RDN({ dc: 'com' })
]
})
const target = 'ou=people,dc=example,dc=com'
t.equal(dn.parentOf(target), true)
})
t.end()
})
tap.test('pop', t => {
t.test('returns the last element and shortens the list', async t => {
const dn = new DN({
rdns: [
new RDN({ cn: 'foo' }),
new RDN({ dc: 'example' }),
new RDN({ dc: 'com' })
]
})
t.equal(dn.toString(), 'cn=foo,dc=example,dc=com')
const rdn = dn.pop()
t.equal(rdn.toString(), 'dc=com')
t.equal(dn.toString(), 'cn=foo,dc=example')
})
t.end()
})
tap.test('push', t => {
t.test('throws for bad input', async t => {
const dn = new DN()
t.throws(
() => dn.push({ cn: 'foo' }),
Error('rdn must be a RDN instance')
)
})
t.test('adds to the front of the list', async t => {
const dn = new DN({
rdns: [
new RDN({ cn: 'foo' }),
new RDN({ dc: 'example' })
]
})
t.equal(dn.toString(), 'cn=foo,dc=example')
const newLength = dn.push(new RDN({ dc: 'com' }))
t.equal(newLength, 3)
t.equal(dn.toString(), 'cn=foo,dc=example,dc=com')
})
t.end()
})
tap.test('rdnAt', t => {
t.test('returns correct RDN', async t => {
const dn = new DN({
rdns: [
new RDN({ cn: 'jdoe', givenName: 'John' }),
new RDN({ ou: 'people' }),
new RDN({ dc: 'example' }),
new RDN({ dc: 'com' })
]
})
const rdn = dn.rdnAt(1)
t.equal(rdn.toString(), 'ou=people')
})
t.end()
})
tap.test('reverse', t => {
t.test('reverses the list', async t => {
const dn = new DN({
rdns: [
new RDN({ dc: 'com' }),
new RDN({ dc: 'example' }),
new RDN({ cn: 'foo' })
]
})
t.equal(dn.toString(), 'dc=com,dc=example,cn=foo')
const result = dn.reverse()
t.equal(dn, result)
t.equal(dn.toString(), 'cn=foo,dc=example,dc=com')
})
t.end()
})
tap.test('setFormat', t => {
t.test('emits warning', t => {
process.on('warning', handler)
t.teardown(async () => {
process.removeListener('warning', handler)
warning.emitted.set('LDAP_DN_DEP_004', false)
})
const rdns = [{ cn: 'foo' }]
new DN({ rdns }).setFormat()
function handler (error) {
t.equal(error.message, '.setFormat() is deprecated. Options will be ignored')
t.end()
}
})
t.end()
})
tap.test('shift', t => {
t.test('returns the first element and shortens the list', async t => {
const dn = new DN({
rdns: [
new RDN({ cn: 'foo' }),
new RDN({ dc: 'example' }),
new RDN({ dc: 'com' })
]
})
t.equal(dn.toString(), 'cn=foo,dc=example,dc=com')
const rdn = dn.shift()
t.equal(rdn.toString(), 'cn=foo')
t.equal(dn.toString(), 'dc=example,dc=com')
})
t.end()
})
tap.test('toString', t => {
t.test('renders correctly', async t => {
const dn = new DN({
rdns: [
new RDN({ cn: 'jdoe', givenName: 'John' }),
new RDN({ ou: 'people' }),
new RDN({ dc: 'example' }),
new RDN({ dc: 'com' })
]
})
t.equal(dn.toString(), 'cn=jdoe+givenName=John,ou=people,dc=example,dc=com')
})
t.test('empty string for empty DN', async t => {
const dn = new DN()
t.equal(dn.toString(), '')
})
t.end()
})
tap.test('unshift', t => {
t.test('throws for bad input', async t => {
const dn = new DN()
t.throws(
() => dn.unshift({ cn: 'foo' }),
Error('rdn must be a RDN instance')
)
})
t.test('adds to the front of the list', async t => {
const dn = new DN({
rdns: [
new RDN({ dc: 'example' }),
new RDN({ dc: 'com' })
]
})
t.equal(dn.toString(), 'dc=example,dc=com')
const newLength = dn.unshift(new RDN({ cn: 'foo' }))
t.equal(newLength, 3)
t.equal(dn.toString(), 'cn=foo,dc=example,dc=com')
})
t.end()
})
tap.test('#isDn', t => {
t.test('true for instance', async t => {
const dn = new DN()
t.equal(DN.isDn(dn), true)
})
t.test('false for non-object', async t => {
t.equal(DN.isDn(42), false)
})
t.test('false for non-array rdns', async t => {
const input = { rdns: 42 }
t.equal(DN.isDn(input), false)
})
t.test('false for bad rdn', async t => {
const input = { rdns: [{ bad: 'rdn', answer: 42 }] }
t.equal(DN.isDn(input), false)
})
t.test('true for dn-like', async t => {
const input = { rdns: [{ name: 'cn', value: 'foo' }] }
t.equal(DN.isDn(input), true)
})
t.end()
})
tap.test('#fromString', t => {
t.test('parses a basic string into an instance', async t => {
const input = 'cn=foo+sn=bar,dc=example,dc=com'
const dn = DN.fromString(input)
t.equal(DN.isDn(dn), true)
t.equal(dn.length, 3)
t.equal(dn.rdnAt(0).toString(), 'cn=foo+sn=bar')
})
t.end()
})

257
node_modules/@ldapjs/dn/lib/rdn.js generated vendored Normal file
View File

@ -0,0 +1,257 @@
'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

214
node_modules/@ldapjs/dn/lib/rdn.test.js generated vendored Normal file
View File

@ -0,0 +1,214 @@
'use strict'
const tap = require('tap')
const warning = require('./deprecations')
const { BerReader } = require('@ldapjs/asn1')
const RDN = require('./rdn')
// Silence the standard warning logs. We will test the messages explicitly.
process.removeAllListeners('warning')
tap.test('equals', t => {
t.test('false for non-rdn object', async t => {
const rdn = new RDN()
t.equal(rdn.equals({}), false)
})
t.test('false for size mis-match', async t => {
const rdn1 = new RDN({ cn: 'foo' })
const rdn2 = new RDN({ cn: 'foo', sn: 'bar' })
t.equal(rdn1.equals(rdn2), false)
})
t.test('false for keys mis-match', async t => {
const rdn1 = new RDN({ cn: 'foo' })
const rdn2 = new RDN({ sn: 'bar' })
t.equal(rdn1.equals(rdn2), false)
})
t.test('false for value mis-match', async t => {
const rdn1 = new RDN({ cn: 'foo' })
const rdn2 = new RDN({ cn: 'bar' })
t.equal(rdn1.equals(rdn2), false)
})
t.test('true for match', async t => {
const rdn1 = new RDN({ cn: 'foo' })
const rdn2 = new RDN({ cn: 'foo' })
t.equal(rdn1.equals(rdn2), true)
})
t.end()
})
tap.test('setAttribute', async t => {
t.test('throws for bad name', async t => {
const rdn = new RDN()
t.throws(
() => rdn.setAttribute({ name: 42 }),
Error('name must be a string')
)
t.throws(
() => rdn.setAttribute({ name: '3cn', value: 'foo' }),
Error('attribute name must start with an ASCII alpha character or be a numeric OID')
)
})
t.test('throws for bad value', async t => {
const rdn = new RDN()
t.throws(
() => rdn.setAttribute({ name: 'cn', value: 42 }),
Error('value must be a string')
)
})
t.test('throws for options', async t => {
const rdn = new RDN()
t.throws(
() => rdn.setAttribute({ name: 'cn', value: 'foo', options: 42 }),
Error('options must be an object')
)
})
t.test('sets an attribute with value', async t => {
const rdn = new RDN()
rdn.setAttribute({ name: 'cn', value: 'foo' })
t.equal(rdn.getValue('cn'), 'foo')
})
t.test('options generates warning', t => {
process.on('warning', handler)
t.teardown(async () => {
process.removeListener('warning', handler)
warning.emitted.set('LDAP_DN_DEP_001', false)
})
const rdn = new RDN()
rdn.setAttribute({ name: 'cn', value: 'foo', options: { foo: 'bar' } })
function handler (error) {
t.equal(error.message, 'attribute options is deprecated and are ignored')
t.end()
}
})
t.end()
})
tap.test('toString', t => {
t.test('basic single value', async t => {
const rdn = new RDN({ cn: 'foo' })
t.equal(rdn.toString(), 'cn=foo')
})
t.test('escaped single value', async t => {
const rdn = new RDN({ cn: ' foo, bar\n' })
t.equal(rdn.toString(), 'cn=\\20foo\\2c bar\\0a')
})
t.test('basic multi-value', async t => {
const rdn = new RDN({ cn: 'foo', sn: 'bar' })
t.equal(rdn.toString(), 'cn=foo+sn=bar')
})
t.test('escaped multi-value', async t => {
const rdn = new RDN({ cn: '#foo', sn: 'bar' })
t.equal(rdn.toString(), 'cn=\\23foo+sn=bar')
})
t.test('recognizes encoded string values', async t => {
const rdn = new RDN({
cn: '#foo',
'1.3.6.1.4.1.1466.0': '#04024869'
})
t.equal(rdn.toString(), 'cn=\\23foo+1.3.6.1.4.1.1466.0=#04024869')
})
t.test('encodes BerReader instances', async t => {
const rdn = new RDN({
cn: new BerReader(Buffer.from([0x04, 0x03, 0x66, 0x6f, 0x6f]))
})
t.equal(rdn.toString(), 'cn=#0403666f6f')
})
t.test('honors unescaped options', async t => {
const rdn = new RDN({
ou: '研发二组'
})
t.equal(rdn.toString({ unescaped: true }), 'ou=研发二组')
})
t.end()
})
tap.test('deprecations', t => {
t.test('format', t => {
process.on('warning', handler)
t.teardown(async () => {
process.removeListener('warning', handler)
warning.emitted.set('LDAP_DN_DEP_002', false)
})
const rdn = new RDN({ cn: 'foo' })
t.equal(rdn.format(), 'cn=foo')
function handler (error) {
t.equal(error.message, '.format() is deprecated. Use .toString() instead')
t.end()
}
})
t.test('set', t => {
process.on('warning', handler)
t.teardown(async () => {
process.removeListener('warning', handler)
warning.emitted.set('LDAP_DN_DEP_002', false)
})
const rdn = new RDN()
rdn.set('cn', 'foo', { value: 'ignored' })
function handler (error) {
t.equal(error.message, '.set() is deprecated. Use .setAttribute() instead')
t.end()
}
})
t.end()
})
tap.test('#isRdn', t => {
t.test('true for instance', async t => {
const rdn = new RDN()
t.equal(RDN.isRdn(rdn), true)
})
t.test('false for non-object', async t => {
t.equal(RDN.isRdn(42), false)
})
t.test('false for bad object', async t => {
const input = { bad: 'rdn', 'non-string-value': 42 }
t.equal(RDN.isRdn(input), false)
})
t.test('true for rdn-like with name+value keys', async t => {
const input = { name: 'cn', value: 'foo' }
t.equal(RDN.isRdn(input), true)
})
t.test('true for pojo representation', async t => {
const input = { cn: 'foo', sn: 'bar' }
t.equal(RDN.isRdn(input), true)
})
t.test('true for pojo with BerReader', async t => {
const input = {
foo: new BerReader(Buffer.from([0x04, 0x03, 0x66, 0x6f, 0x6f]))
}
t.equal(RDN.isRdn(input), true)
})
t.end()
})

104
node_modules/@ldapjs/dn/lib/utils/escape-value.js generated vendored Normal file
View File

@ -0,0 +1,104 @@
'use strict'
/**
* Converts an attribute value into an escaped string as described in
* https://www.rfc-editor.org/rfc/rfc4514#section-2.4.
*
* This function supports up to 4 byte unicode characters.
*
* @param {string} value
* @returns {string} The escaped string.
*/
module.exports = function escapeValue (value) {
if (typeof value !== 'string') {
throw Error('value must be a string')
}
const toEscape = Buffer.from(value, 'utf8')
const escaped = []
// We will handle the reverse solidus ('\') on its own.
const embeddedReservedChars = [
0x22, // '"'
0x2b, // '+'
0x2c, // ','
0x3b, // ';'
0x3c, // '<'
0x3e // '>'
]
for (let i = 0; i < toEscape.byteLength;) {
const charHex = toEscape[i]
// Handle leading space or #.
if (i === 0 && (charHex === 0x20 || charHex === 0x23)) {
escaped.push(toEscapedHexString(charHex))
i += 1
continue
}
// Handle trailing space.
if (i === toEscape.byteLength - 1 && charHex === 0x20) {
escaped.push(toEscapedHexString(charHex))
i += 1
continue
}
if (embeddedReservedChars.includes(charHex) === true) {
escaped.push(toEscapedHexString(charHex))
i += 1
continue
}
if (charHex >= 0xc0 && charHex <= 0xdf) {
// Represents the first byte in a 2-byte UTF-8 character.
escaped.push(toEscapedHexString(charHex))
escaped.push(toEscapedHexString(toEscape[i + 1]))
i += 2
continue
}
if (charHex >= 0xe0 && charHex <= 0xef) {
// Represents the first byte in a 3-byte UTF-8 character.
escaped.push(toEscapedHexString(charHex))
escaped.push(toEscapedHexString(toEscape[i + 1]))
escaped.push(toEscapedHexString(toEscape[i + 2]))
i += 3
continue
}
if (charHex >= 0xf0 && charHex <= 0xf7) {
// Represents the first byte in a 4-byte UTF-8 character.
escaped.push(toEscapedHexString(charHex))
escaped.push(toEscapedHexString(toEscape[i + 1]))
escaped.push(toEscapedHexString(toEscape[i + 2]))
escaped.push(toEscapedHexString(toEscape[i + 3]))
i += 4
continue
}
if (charHex <= 31) {
// Represents an ASCII control character.
escaped.push(toEscapedHexString(charHex))
i += 1
continue
}
escaped.push(String.fromCharCode(charHex))
i += 1
continue
}
return escaped.join('')
}
/**
* Given a byte, convert it to an escaped hex string.
*
* @example
* toEscapedHexString(0x20) // '\20'
*
* @param {number} char
* @returns {string}
*/
function toEscapedHexString (char) {
return '\\' + char.toString(16).padStart(2, '0')
}

62
node_modules/@ldapjs/dn/lib/utils/escape-value.test.js generated vendored Normal file
View File

@ -0,0 +1,62 @@
'use strict'
const tap = require('tap')
const escapeValue = require('./escape-value')
tap.test('throws for bad input', async t => {
t.throws(
() => escapeValue(42),
Error('value must be a string')
)
})
tap.test('reserved chars', t => {
t.test('space', async t => {
const input = ' has a leading and trailing space '
const expected = '\\20has a leading and trailing space\\20'
const result = escapeValue(input)
t.equal(result, expected)
})
t.test('leading #', async t => {
t.equal(escapeValue('#hashtag'), '\\23hashtag')
})
t.test('pompous name', async t => {
t.equal(
escapeValue('James "Jim" Smith, III'),
'James \\22Jim\\22 Smith\\2c III'
)
})
t.test('carriage return', async t => {
t.equal(escapeValue('Before\rAfter'), 'Before\\0dAfter')
})
t.end()
})
tap.test('2-byte utf-8', t => {
t.test('Lučić', async t => {
const expected = 'Lu\\c4\\8di\\c4\\87'
t.equal(escapeValue('Lučić'), expected)
})
t.end()
})
tap.test('3-byte utf-8', t => {
t.test('₠', async t => {
t.equal(escapeValue('₠'), '\\e2\\82\\a0')
})
t.end()
})
tap.test('4-byte utf-8', t => {
t.test('😀', async t => {
t.equal(escapeValue('😀'), '\\f0\\9f\\98\\80')
})
t.end()
})

19
node_modules/@ldapjs/dn/lib/utils/is-dotted-decimal.js generated vendored Normal file
View File

@ -0,0 +1,19 @@
'use strict'
const partIsNotNumeric = part => /^\d+$/.test(part) === false
/**
* Determines if a passed in string is a dotted decimal string.
*
* @param {string} value
*
* @returns {boolean}
*/
module.exports = function isDottedDecimal (value) {
if (typeof value !== 'string') return false
const parts = value.split('.')
const nonNumericParts = parts.filter(partIsNotNumeric)
return nonNumericParts.length === 0
}

View File

@ -0,0 +1,24 @@
'use strict'
const tap = require('tap')
const isDottedDecimal = require('./is-dotted-decimal')
tap.test('false for non-string', async t => {
t.equal(isDottedDecimal(), false)
})
tap.test('false for empty string', async t => {
t.equal(isDottedDecimal(''), false)
})
tap.test('false for alpha string', async t => {
t.equal(isDottedDecimal('foo'), false)
})
tap.test('false for alpha-num string', async t => {
t.equal(isDottedDecimal('foo.123'), false)
})
tap.test('true for valid string', async t => {
t.equal(isDottedDecimal('1.2.3'), true)
})

View File

@ -0,0 +1,58 @@
'use strict'
/**
* Find the ending position of the attribute type name portion of an RDN.
* This function does not verify if the name is a valid description string
* or numeric OID. It merely reads a string from the given starting position
* to the spec defined end of an attribute type string.
*
* @param {Buffer} searchBuffer A buffer representing the RDN.
* @param {number} startPos The position in the `searchBuffer` to start
* searching from.
*
* @returns {number} The position of the end of the RDN's attribute type name,
* or `-1` if an invalid character has been encountered.
*/
module.exports = function findNameEnd ({ searchBuffer, startPos }) {
let pos = startPos
while (pos < searchBuffer.byteLength) {
const char = searchBuffer[pos]
if (char === 0x20 || char === 0x3d) {
// Name ends with a space or an '=' character.
break
}
if (isValidNameChar(char) === true) {
pos += 1
continue
}
return -1
}
return pos
}
/**
* Determine if a character is a valid `attributeType` character as defined
* in RFC 4514 §3.
*
* @param {number} c The character to verify. Should be the byte representation
* of the character from a {@link Buffer} instance.
*
* @returns {boolean}
*/
function isValidNameChar (c) {
if (c >= 0x41 && c <= 0x5a) { // A - Z
return true
}
if (c >= 0x61 && c <= 0x7a) { // a - z
return true
}
if (c >= 0x30 && c <= 0x39) { // 0 - 9
return true
}
if (c === 0x2d || c === 0x2e) { // - or .
return true
}
return false
}

View File

@ -0,0 +1,28 @@
'use strict'
const tap = require('tap')
const findNameEnd = require('./find-name-end')
tap.test('stops on a space', async t => {
const input = Buffer.from('foo = bar')
const pos = findNameEnd({ searchBuffer: input, startPos: 0 })
t.equal(pos, 3)
})
tap.test('stops on an equals', async t => {
const input = Buffer.from('foo=bar')
const pos = findNameEnd({ searchBuffer: input, startPos: 0 })
t.equal(pos, 3)
})
tap.test('returns -1 for bad character', async t => {
const input = Buffer.from('føø=bar')
const pos = findNameEnd({ searchBuffer: input, startPos: 0 })
t.equal(pos, -1)
})
tap.test('recognizes all valid characters', async t => {
const input = Buffer.from('Foo.0-bar=baz')
const pos = findNameEnd({ searchBuffer: input, startPos: 0 })
t.equal(pos, 9)
})

View File

@ -0,0 +1,34 @@
'use strict'
// Attribute types must start with an ASCII alphanum character.
// https://www.rfc-editor.org/rfc/rfc4514#section-3
// https://www.rfc-editor.org/rfc/rfc4512#section-1.4
const isLeadChar = (c) => /[a-zA-Z0-9]/.test(c) === true
/**
* Find the starting position of an attribute type (name). Leading spaces and
* commas are ignored. If an invalid leading character is encountered, an
* invalid position will be returned.
*
* @param {Buffer} searchBuffer
* @param {number} startPos
*
* @returns {number} The position in the buffer where the name starts, or `-1`
* if an invalid name starting character is encountered.
*/
module.exports = function findNameStart ({ searchBuffer, startPos }) {
let pos = startPos
while (pos < searchBuffer.byteLength) {
if (searchBuffer[pos] === 0x20 || searchBuffer[pos] === 0x2c) {
// Skip leading space and comma.
pos += 1
continue
}
const char = String.fromCharCode(searchBuffer[pos])
if (isLeadChar(char) === true) {
return pos
}
break
}
return -1
}

View File

@ -0,0 +1,28 @@
'use strict'
const tap = require('tap')
const findNameStart = require('./find-name-start')
tap.test('returns correct position', async t => {
const input = Buffer.from(' foo')
t.equal(
findNameStart({ searchBuffer: input, startPos: 0 }),
3
)
})
tap.test('skips leading comma', async t => {
const input = Buffer.from(' , foo=bar')
t.equal(
findNameStart({ searchBuffer: input, startPos: 0 }),
3
)
})
tap.test('returns -1 for invalid lead char', async t => {
const input = Buffer.from(' øfoo')
t.equal(
findNameStart({ searchBuffer: input, startPos: 0 }),
-1
)
})

105
node_modules/@ldapjs/dn/lib/utils/parse-string/index.js generated vendored Normal file
View File

@ -0,0 +1,105 @@
'use strict'
const readAttributePair = require('./read-attribute-pair')
/**
* @typedef {object} ParsedPojoRdn
* @property {string} name Either the name of an RDN attribute, or the
* equivalent numeric OID.
* @property {string | import('@ldapjs/asn1').BerReader} value The attribute
* value as a plain string, or a `BerReader` if the string value was an encoded
* hex string.
*/
/**
* Parse a string into a set of plain JavaScript object representations of
* RDNs.
*
* @example A plain string with multiple RDNs and multiple attribute assertions.
* const input = 'cn=foo+sn=bar,dc=example,dc=com
* const result = parseString(input)
* // [
* // { cn: 'foo', sn: 'bar' },
* // { dc: 'example' }
* // { dc: 'com' }
* // ]
*
* @param {string} input The RDN string to parse.
*
* @returns {ParsedPojoRdn[]}
*
* @throws When there is some problem parsing the RDN string.
*/
module.exports = function parseString (input) {
if (typeof input !== 'string') {
throw Error('input must be a string')
}
if (input.length === 0) {
// Short circuit because the input is an empty DN (i.e. "root DSE").
return []
}
const searchBuffer = Buffer.from(input, 'utf8')
const length = searchBuffer.byteLength
const rdns = []
let pos = 0
let rdn = {}
readRdnLoop:
while (pos <= length) {
if (pos === length) {
const char = searchBuffer[pos - 1]
/* istanbul ignore else */
if (char === 0x2b || char === 0x2c || char === 0x3b) {
throw Error('rdn string ends abruptly with character: ' + String.fromCharCode(char))
}
}
// Find the start of significant characters by skipping over any leading
// whitespace.
while (pos < length && searchBuffer[pos] === 0x20) {
pos += 1
}
const readAttrPairResult = readAttributePair({ searchBuffer, startPos: pos })
pos = readAttrPairResult.endPos
rdn = { ...rdn, ...readAttrPairResult.pair }
if (pos >= length) {
// We've reached the end of the string. So push the current RDN and stop.
rdns.push(rdn)
break
}
// Next, we need to determine if the next set of significant characters
// denotes another attribute pair for the current RDN, or is the indication
// of another RDN.
while (pos < length) {
const char = searchBuffer[pos]
// We don't need to skip whitespace before the separator because the
// attribute pair function has already advanced us past any such
// whitespace.
if (char === 0x2b) { // +
// We need to continue adding attribute pairs to the current RDN.
pos += 1
continue readRdnLoop
}
/* istanbul ignore else */
if (char === 0x2c || char === 0x3b) { // , or ;
// The current RDN has been fully parsed, so push it to the list,
// reset the collector, and start parsing the next RDN.
rdns.push(rdn)
rdn = {}
pos += 1
continue readRdnLoop
}
}
}
return rdns
}

View File

@ -0,0 +1,214 @@
'use strict'
const tap = require('tap')
const { BerReader } = require('@ldapjs/asn1')
const parseString = require('./index')
tap.test('throws for non-string input', async t => {
const input = ['cn=foo']
t.throws(
() => parseString(input),
'input must be a string'
)
})
tap.test('short circuits for root dse', async t => {
t.same(parseString(''), [])
})
tap.test('parses basic single rdn', async t => {
const input = 'cn=foo'
const result = parseString(input)
t.same(result, [{ cn: 'foo' }])
})
tap.test('skips leading whitespace', async t => {
const input = ' cn=foo'
const result = parseString(input)
t.same(result, [{ cn: 'foo' }])
})
tap.test('parses basic multiple rdns', async t => {
let input = 'dc=example,dc=com'
let result = parseString(input)
t.same(
result,
[
{ dc: 'example' },
{ dc: 'com' }
]
)
// RFC 2253 §4 separator is supported.
input = 'dc=example;dc=com'
result = parseString(input)
t.same(
result,
[
{ dc: 'example' },
{ dc: 'com' }
]
)
})
tap.test('handles multivalued rdn', async t => {
const input = 'foo=bar+baz=bif'
const result = parseString(input)
t.same(result, [{ foo: 'bar', baz: 'bif' }])
})
tap.test('abruptly ending strings throw', async t => {
const baseError = 'rdn string ends abruptly with character: '
const tests = [
{ input: 'foo=bar+', expected: baseError + '+' },
{ input: 'foo=bar,', expected: baseError + ',' },
{ input: 'foo=bar;', expected: baseError + ';' }
]
for (const test of tests) {
t.throws(() => parseString(test.input), test.expected)
}
})
tap.test('adds rdn with trailing whitespace', async t => {
const input = 'foo=bar '
const result = parseString(input)
t.same(result, [{ foo: 'bar' }])
})
tap.test('parses rdn with attribute name in OID form', async t => {
const input = '0.9.2342.19200300.100.1.25=Example'
const result = parseString(input)
t.same(result, [{ '0.9.2342.19200300.100.1.25': 'Example' }])
})
tap.test('throws for invalid attribute type name', async t => {
let input = '3foo=bar'
t.throws(
() => parseString(input),
'invalid attribute type name: 3foo'
)
input = '1.2.3.abc=bar'
t.throws(
() => parseString(input),
'invalid attribute type name: 1.2.3.abc=bar'
)
input = 'føø=bar'
t.throws(
() => parseString(input),
'invalid attribute type name: føø'
)
})
tap.test('throws for abrupt end', async t => {
const input = 'foo=bar,'
t.throws(
() => parseString(input),
'rdn string ends abruptly with character: ,'
)
})
tap.test('rfc 4514 §4 examples', async t => {
const tests = [
{
input: 'UID=jsmith,DC=example,DC=net',
expected: [{ UID: 'jsmith' }, { DC: 'example' }, { DC: 'net' }]
},
{
input: 'OU=Sales+CN=J. Smith,DC=example,DC=net',
expected: [
{ OU: 'Sales', CN: 'J. Smith' },
{ DC: 'example' },
{ DC: 'net' }
]
},
{
input: 'CN=James \\"Jim\\" Smith\\, III,DC=example,DC=net',
expected: [{ CN: 'James "Jim" Smith, III' }, { DC: 'example' }, { DC: 'net' }]
},
{
input: 'CN=Before\\0dAfter,DC=example,DC=net',
expected: [{ CN: 'Before\rAfter' }, { DC: 'example' }, { DC: 'net' }]
},
{
checkBuffer: true,
input: '1.3.6.1.4.1.1466.0=#04024869',
expected: [{ '1.3.6.1.4.1.1466.0': new BerReader(Buffer.from([0x04, 0x02, 0x48, 0x69])) }]
},
{
input: 'CN=Lu\\C4\\8Di\\C4\\87',
expected: [{ CN: 'Lučić' }]
}
]
for (const test of tests) {
const result = parseString(test.input)
if (test.checkBuffer) {
for (const [i, rdn] of test.expected.entries()) {
for (const key of Object.keys(rdn)) {
t.equal(
rdn[key].buffer.compare(result[i][key].buffer),
0
)
}
}
} else {
t.same(result, test.expected)
}
}
})
tap.test('rfc 2253 §5 examples', async t => {
const tests = [
{
input: 'CN=Steve Kille,O=Isode Limited,C=GB',
expected: [{ CN: 'Steve Kille' }, { O: 'Isode Limited' }, { C: 'GB' }]
},
{
input: 'OU=Sales+CN=J. Smith,O=Widget Inc.,C=US',
expected: [{ OU: 'Sales', CN: 'J. Smith' }, { O: 'Widget Inc.' }, { C: 'US' }]
},
{
input: 'CN=L. Eagle,O=Sue\\, Grabbit and Runn,C=GB',
expected: [{ CN: 'L. Eagle' }, { O: 'Sue, Grabbit and Runn' }, { C: 'GB' }]
},
{
input: 'CN=Before\\0DAfter,O=Test,C=GB',
expected: [{ CN: 'Before\rAfter' }, { O: 'Test' }, { C: 'GB' }]
},
{
checkBuffer: true,
input: '1.3.6.1.4.1.1466.0=#04024869,O=Test,C=GB',
expected: [
{ '1.3.6.1.4.1.1466.0': new BerReader(Buffer.from([0x04, 0x02, 0x48, 0x69])) },
{ O: 'Test' },
{ C: 'GB' }
]
},
{
input: 'SN=Lu\\C4\\8Di\\C4\\87',
expected: [{ SN: 'Lučić' }]
}
]
for (const test of tests) {
const result = parseString(test.input)
if (test.checkBuffer) {
for (const [i, rdn] of test.expected.entries()) {
for (const key of Object.keys(rdn)) {
if (typeof rdn[key] !== 'string') {
t.equal(
rdn[key].buffer.compare(result[i][key].buffer),
0
)
} else {
t.equal(rdn[key], result[i][key])
}
}
}
} else {
t.same(result, test.expected)
}
}
})

View File

@ -0,0 +1,28 @@
'use strict'
const isDigit = c => /[0-9]/.test(c) === true
const hasKeyChars = input => /[a-zA-Z-]/.test(input) === true
const isValidLeadChar = c => /[a-zA-Z]/.test(c) === true
const hasInvalidChars = input => /[^a-zA-Z0-9-]/.test(input) === true
/**
* An attribute type name is defined by RFC 4514 §3 as a "descr" or
* "numericoid". These are defined by RFC 4512 §1.4. This function validates
* the given name as matching the spec.
*
* @param {string} name
*
* @returns {boolean}
*/
module.exports = function isValidAttributeTypeName (name) {
if (isDigit(name[0]) === true) {
// A leading digit indicates that the name should be a numericoid.
return hasKeyChars(name) === false
}
if (isValidLeadChar(name[0]) === false) {
return false
}
return hasInvalidChars(name) === false
}

View File

@ -0,0 +1,36 @@
'use strict'
const tap = require('tap')
const isValidAttributeTypeName = require('./is-valid-attribute-type-name')
tap.test('validates numericoids', async t => {
let input = '1.2.3.4'
let result = isValidAttributeTypeName(input)
t.equal(result, true)
input = '1.2.3.4.abc'
result = isValidAttributeTypeName(input)
t.equal(result, false)
})
tap.test('validates descrs', async t => {
let input = 'foo'
let result = isValidAttributeTypeName(input)
t.equal(result, true)
input = '3foo'
result = isValidAttributeTypeName(input)
t.equal(result, false)
input = 'foo-3'
result = isValidAttributeTypeName(input)
t.equal(result, true)
input = 'føø3'
result = isValidAttributeTypeName(input)
t.equal(result, false)
input = 'ƒ00'
result = isValidAttributeTypeName(input)
t.equal(result, false)
})

View File

@ -0,0 +1,73 @@
'use strict'
const findNameStart = require('./find-name-start')
const findNameEnd = require('./find-name-end')
const isValidAttributeTypeName = require('./is-valid-attribute-type-name')
const readAttributeValue = require('./read-attribute-value')
/**
* @typedef {object} AttributePair
* @property {string | import('@ldapjs/asn1').BerReader} name Property name is
* actually the property name of the attribute pair. The value will be a string,
* or, in the case of the value being a hex encoded string, an instance of
* `BerReader`.
*
* @example
* const input = 'foo=bar'
* const pair = { foo: 'bar' }
*/
/**
* @typedef {object} ReadAttributePairResult
* @property {number} endPos The ending position in the input search buffer that
* is the end of the read attribute pair.
* @property {AttributePair} pair The parsed attribute pair.
*/
/**
* Read an RDN attribute type and attribute value pair from the provided
* search buffer at the given starting position.
*
* @param {Buffer} searchBuffer
* @param {number} startPos
*
* @returns {ReadAttributePairResult}
*
* @throws When there is some problem with the input string.
*/
module.exports = function readAttributePair ({ searchBuffer, startPos }) {
let pos = startPos
const nameStartPos = findNameStart({
searchBuffer,
startPos: pos
})
if (nameStartPos < 0) {
throw Error('invalid attribute name leading character encountered')
}
const nameEndPos = findNameEnd({
searchBuffer,
startPos: nameStartPos
})
if (nameStartPos < 0) {
throw Error('invalid character in attribute name encountered')
}
const attributeName = searchBuffer.subarray(nameStartPos, nameEndPos).toString('utf8')
if (isValidAttributeTypeName(attributeName) === false) {
throw Error('invalid attribute type name: ' + attributeName)
}
const valueReadResult = readAttributeValue({
searchBuffer,
startPos: nameEndPos
})
pos = valueReadResult.endPos
const attributeValue = valueReadResult.value
return {
endPos: pos,
pair: { [attributeName]: attributeValue }
}
}

View File

@ -0,0 +1,165 @@
'use strict'
const readHexString = require('./read-hex-string')
const readEscapeSequence = require('./read-escape-sequence')
/**
* @typedef {object} ReadAttributeValueResult
* @property {number} endPos The position in the buffer that marks the end of
* the value.
* @property {string | import('@ldapjs/asn1').BerReader} value
*/
/**
* Read an attribute value string from a given {@link Buffer} and return it.
* If the value is an encoded octet string, it will be decoded and returned
* as a {@link Buffer}.
*
* @param {Buffer} searchBuffer
* @param {number} startPos
*
* @returns {ReadAttributeValueResult}
*
* @throws When there is a syntax error in the attribute value string.
*/
module.exports = function readAttributeValue ({ searchBuffer, startPos }) {
let pos = startPos
while (pos < searchBuffer.byteLength && searchBuffer[pos] === 0x20) {
// Skip over any leading whitespace before the '='.
pos += 1
}
if (pos >= searchBuffer.byteLength || searchBuffer[pos] !== 0x3d) {
throw Error('attribute value does not start with equals sign')
}
// Advance past the equals sign.
pos += 1
while (pos <= searchBuffer.byteLength && searchBuffer[pos] === 0x20) {
// Advance past any leading whitespace.
pos += 1
}
if (pos >= searchBuffer.byteLength) {
return { endPos: pos, value: '' }
}
if (searchBuffer[pos] === 0x23) {
const result = readHexString({ searchBuffer, startPos: pos + 1 })
pos = result.endPos
return { endPos: pos, value: result.berReader }
}
const readValueResult = readValueString({ searchBuffer, startPos: pos })
pos = readValueResult.endPos
return {
endPos: pos,
value: readValueResult.value.toString('utf8').trim()
}
}
/**
* @typedef {object} ReadValueStringResult
* @property {number} endPos
* @property {Buffer} value
* @private
*/
/**
* Read a series of bytes from the buffer as a plain string.
*
* @param {Buffer} searchBuffer
* @param {number} startPos
*
* @returns {ReadValueStringResult}
*
* @throws When the attribute value is malformed.
*
* @private
*/
function readValueString ({ searchBuffer, startPos }) {
let pos = startPos
let inQuotes = false
let endQuotePresent = false
const bytes = []
while (pos <= searchBuffer.byteLength) {
const char = searchBuffer[pos]
if (pos === searchBuffer.byteLength) {
if (inQuotes === true && endQuotePresent === false) {
throw Error('missing ending double quote for attribute value')
}
break
}
if (char === 0x22) {
// Handle the double quote (") character.
// RFC 2253 §4 allows for attribute values to be wrapped in double
// quotes in order to allow certain characters to be unescaped.
// We are not enforcing escaping of characters in this parser, so we only
// need to recognize that the quotes are present. Our RDN string encoder
// will escape characters as necessary.
if (inQuotes === true) {
pos += 1
endQuotePresent = true
// We should be at the end of the value.
while (pos < searchBuffer.byteLength) {
const nextChar = searchBuffer[pos]
if (isEndChar(nextChar) === true) {
break
}
if (nextChar !== 0x20) {
throw Error('significant rdn character found outside of quotes at position ' + pos)
}
pos += 1
}
break
}
if (pos !== startPos) {
throw Error('unexpected quote (") in rdn string at position ' + pos)
}
inQuotes = true
pos += 1
continue
}
if (isEndChar(char) === true && inQuotes === false) {
break
}
if (char === 0x5c) {
// We have encountered the start of an escape sequence.
const seqResult = readEscapeSequence({
searchBuffer,
startPos: pos
})
pos = seqResult.endPos
Array.prototype.push.apply(bytes, seqResult.parsed)
continue
}
bytes.push(char)
pos += 1
}
return {
endPos: pos,
value: Buffer.from(bytes)
}
}
function isEndChar (c) {
switch (c) {
case 0x2b: // +
case 0x2c: // ,
case 0x3b: // ; -- Allowed by RFC 2253 §4 in place of a comma.
return true
default:
return false
}
}

View File

@ -0,0 +1,119 @@
'use strict'
const tap = require('tap')
const readAttributeValue = require('./read-attribute-value')
const { BerReader } = require('@ldapjs/asn1')
const missingError = Error('attribute value does not start with equals sign')
tap.test('throws if missing equals sign', async t => {
let input = Buffer.from('foo')
t.throws(
() => readAttributeValue({ searchBuffer: input, startPos: 3 }),
missingError
)
input = Buffer.from('foo ≠')
t.throws(
() => readAttributeValue({ searchBuffer: input, startPos: 3 }),
missingError
)
})
tap.test('handles empty attribute value', async t => {
const input = Buffer.from('foo=')
const result = readAttributeValue({ searchBuffer: input, startPos: 3 })
t.same(result, { endPos: 4, value: '' })
})
tap.test('returns attribute value', async t => {
const input = Buffer.from('foo=bar')
const result = readAttributeValue({ searchBuffer: input, startPos: 3 })
t.same(result, { endPos: 7, value: 'bar' })
})
tap.test('quoted values', t => {
t.test('throws if quote is unexpected', async t => {
const input = Buffer.from('=foo"bar')
t.throws(
() => readAttributeValue({ searchBuffer: input, startPos: 0 }),
'unexpected quote (") in rdn string at position 4'
)
})
t.test('handles a basic quoted value', async t => {
const input = Buffer.from('="bar"')
const result = readAttributeValue({ searchBuffer: input, startPos: 0 })
t.same(result, { endPos: 6, value: 'bar' })
})
t.test('handles quote followed by end char', async t => {
const input = Buffer.from('="bar",another=rdn')
const result = readAttributeValue({ searchBuffer: input, startPos: 0 })
t.same(result, { endPos: 6, value: 'bar' })
})
t.test('significant spaces in quoted values are part of the value', async t => {
const input = Buffer.from('="foo bar "')
const result = readAttributeValue({ searchBuffer: input, startPos: 0 })
t.same(result, { endPos: 13, value: 'foo bar' })
})
t.test('throws if next significant char is not an end char', async t => {
const input = Buffer.from('="foo bar" baz')
t.throws(
() => readAttributeValue({ searchBuffer: input, startPos: 0 }),
'significant rdn character found outside of quotes at position 7'
)
})
t.test('throws if ending quote not found', async t => {
const input = Buffer.from('="foo')
t.throws(
() => readAttributeValue({ searchBuffer: input, startPos: 0 }),
'missing ending double quote for attribute value'
)
})
t.end()
})
tap.test('leading and trailing spaces are omitted', async t => {
const input = Buffer.from('= foo ')
const result = readAttributeValue({ searchBuffer: input, startPos: 0 })
t.same(result, { endPos: 9, value: 'foo' })
})
tap.test('parses escaped attribute values', async t => {
const input = Buffer.from('foo=foo\\#bar')
const result = readAttributeValue({ searchBuffer: input, startPos: 3 })
t.same(result, { endPos: 12, value: 'foo#bar' })
})
tap.test('stops reading at all ending characters', async t => {
const tests = [
{ input: '=foo,bar', expected: { endPos: 4, value: 'foo' } },
{ input: '=foo+bar', expected: { endPos: 4, value: 'foo' } },
{ input: '=foo;bar', expected: { endPos: 4, value: 'foo' } }
]
for (const test of tests) {
const result = readAttributeValue({
searchBuffer: Buffer.from(test.input),
startPos: 0
})
t.same(result, test.expected)
}
})
tap.test('reads hex encoded string', async t => {
const input = Buffer.from('=#0403666f6f')
const result = readAttributeValue({ searchBuffer: input, startPos: 0 })
const expected = {
endPos: 12,
value: new BerReader(Buffer.from([0x04, 0x03, 0x66, 0x6f, 0x6f]))
}
t.same(result, expected)
t.equal(result.value.buffer.compare(expected.value.buffer), 0)
})

View File

@ -0,0 +1,137 @@
'use strict'
/**
* @typedef ReadEscapeSequenceResult
* @property {number} endPos The position in the buffer that marks the end of
* the escape sequence.
* @property {Buffer} parsed The parsed escape sequence as a buffer of bytes.
*/
/**
* Read an escape sequence from a buffer. It reads until no escape sequences
* are found. Thus, a sequence of escape sequences will all be parsed at once
* and returned as a single result.
*
* @example A Single ASCII Sequence
* const toParse = Buffer.from('foo\\#bar', 'utf8')
* const {parsed, endPos} = readEscapeSequence({
* searchBuffer: toParse,
* startPos: 3
* })
* // => parsed = '#', endPos = 5
*
* @example Multiple ASCII Sequences In Succession
* const toParse = Buffer.from('foo\\#\\!bar', 'utf8')
* const {parsed, endPos} = readEscapeSequence({
* searchBuffer: toParse,
* startPos: 3
* })
* // => parsed = '#!', endPos = 7
*
* @param searchBuffer
* @param startPos
*
* @returns {ReadEscapeSequenceResult}
*
* @throws When an escaped sequence is not a valid hexadecimal value.
*/
module.exports = function readEscapeSequence ({ searchBuffer, startPos }) {
// This is very similar to the `readEscapedCharacters` algorithm in
// the `utils/escape-filter-value` in `@ldapjs/filter`. The difference being
// that here we want to interpret the escape sequence instead of return it
// as a string to be embedded in an "escaped" string.
// https://github.com/ldapjs/filter/blob/1423612/lib/utils/escape-filter-value.js
let pos = startPos
const buf = []
while (pos < searchBuffer.byteLength) {
const char = searchBuffer[pos]
const nextChar = searchBuffer[pos + 1]
if (char !== 0x5c) {
// End of sequence reached.
break
}
const strHexCode = String.fromCharCode(nextChar) +
String.fromCharCode(searchBuffer[pos + 2])
const hexCode = parseInt(strHexCode, 16)
if (Number.isNaN(hexCode) === true) {
if (nextChar >= 0x00 && nextChar <= 0x7f) {
// Sequence is a single escaped ASCII character
buf.push(nextChar)
pos += 2
continue
} else {
throw Error('invalid hex code in escape sequence')
}
}
if (hexCode >= 0xc0 && hexCode <= 0xdf) {
// Sequence is a 2-byte utf-8 character.
const secondByte = parseInt(
String.fromCharCode(searchBuffer[pos + 4]) +
String.fromCharCode(searchBuffer[pos + 5]),
16
)
buf.push(hexCode)
buf.push(secondByte)
pos += 6
continue
}
if (hexCode >= 0xe0 && hexCode <= 0xef) {
// Sequence is a 3-byte utf-8 character.
const secondByte = parseInt(
String.fromCharCode(searchBuffer[pos + 4]) +
String.fromCharCode(searchBuffer[pos + 5]),
16
)
const thirdByte = parseInt(
String.fromCharCode(searchBuffer[pos + 7]) +
String.fromCharCode(searchBuffer[pos + 8]),
16
)
buf.push(hexCode)
buf.push(secondByte)
buf.push(thirdByte)
pos += 9
continue
}
if (hexCode >= 0xf0 && hexCode <= 0xf7) {
// Sequence is a 4-byte utf-8 character.
const secondByte = parseInt(
String.fromCharCode(searchBuffer[pos + 4]) +
String.fromCharCode(searchBuffer[pos + 5]),
16
)
const thirdByte = parseInt(
String.fromCharCode(searchBuffer[pos + 7]) +
String.fromCharCode(searchBuffer[pos + 8]),
16
)
const fourthByte = parseInt(
String.fromCharCode(searchBuffer[pos + 10]) +
String.fromCharCode(searchBuffer[pos + 11]),
16
)
buf.push(hexCode)
buf.push(secondByte)
buf.push(thirdByte)
buf.push(fourthByte)
pos += 12
continue
}
// The escaped character should be a single hex value.
buf.push(hexCode)
pos += 3
}
return {
endPos: pos,
parsed: Buffer.from(buf)
}
}

View File

@ -0,0 +1,72 @@
'use strict'
const tap = require('tap')
const readEscapeSequence = require('./read-escape-sequence')
tap.test('throws for bad sequence', async t => {
const input = Buffer.from('foo\\ø')
t.throws(
() => readEscapeSequence({ searchBuffer: input, startPos: 3 }),
Error('invalid hex code in escape sequence')
)
})
tap.test('reads a single ascii sequence', async t => {
const input = Buffer.from('foo\\#bar', 'utf8')
const { parsed, endPos } = readEscapeSequence({
searchBuffer: input,
startPos: 3
})
t.equal(parsed.toString(), '#')
t.equal(endPos, 5)
})
tap.test('reads a sequence of ascii sequences', async t => {
const input = Buffer.from('foo\\#\\!bar', 'utf8')
const { parsed, endPos } = readEscapeSequence({
searchBuffer: input,
startPos: 3
})
t.equal(parsed.toString(), '#!')
t.equal(endPos, 7)
})
tap.test('reads a single hex sequence', async t => {
const input = Buffer.from('foo\\2abar', 'utf8')
const { parsed, endPos } = readEscapeSequence({
searchBuffer: input,
startPos: 3
})
t.equal(parsed.toString(), '*')
t.equal(endPos, 6)
})
tap.test('reads 2-byte utf-8 sequence', async t => {
const input = Buffer.from('fo\\c5\\8f bar')
const { parsed, endPos } = readEscapeSequence({
searchBuffer: input,
startPos: 2
})
t.equal(parsed.toString(), 'ŏ')
t.equal(endPos, 8)
})
tap.test('reads 3-byte utf-8 sequence', async t => {
const input = Buffer.from('fo\\e0\\b0\\b0 bar')
const { parsed, endPos } = readEscapeSequence({
searchBuffer: input,
startPos: 2
})
t.equal(parsed.toString(), 'ర')
t.equal(endPos, 11)
})
tap.test('reads 4-byte utf-8 sequence', async t => {
const input = Buffer.from('fo\\f0\\92\\84\\ad bar')
const { parsed, endPos } = readEscapeSequence({
searchBuffer: input,
startPos: 2
})
t.equal(parsed.toString(), '𒄭')
t.equal(endPos, 14)
})

View File

@ -0,0 +1,61 @@
'use strict'
const { BerReader } = require('@ldapjs/asn1')
const isValidHexCode = code => /[0-9a-fA-F]{2}/.test(code) === true
/**
* @typedef {object} ReadHexStringResult
* @property {number} endPos The position in the buffer where the end of the
* hex string was encountered.
* @property {import('@ldapjs/asn1').BerReader} berReader The parsed hex string
* as an BER object.
*/
/**
* Read a sequence of bytes as a hex encoded octet string. The sequence is
* assumed to be a spec compliant encoded BER object.
*
* @param {Buffer} searchBuffer The buffer to read.
* @param {number} startPos The position in the buffer to start reading from.
*
* @returns {ReadHexStringResult}
*
* @throws When an invalid hex pair has been encountered.
*/
module.exports = function readHexString ({ searchBuffer, startPos }) {
const bytes = []
let pos = startPos
while (pos < searchBuffer.byteLength) {
if (isEndChar(searchBuffer[pos])) {
break
}
const hexPair = String.fromCharCode(searchBuffer[pos]) +
String.fromCharCode(searchBuffer[pos + 1])
if (isValidHexCode(hexPair) === false) {
throw Error('invalid hex pair encountered: 0x' + hexPair)
}
bytes.push(parseInt(hexPair, 16))
pos += 2
}
return {
endPos: pos,
berReader: new BerReader(Buffer.from(bytes))
}
}
function isEndChar (c) {
switch (c) {
case 0x20: // space
case 0x2b: // +
case 0x2c: // ,
case 0x3b: // ;
return true
default:
return false
}
}

View File

@ -0,0 +1,52 @@
'use strict'
const tap = require('tap')
const readHexString = require('./read-hex-string')
tap.test('throws for invalid hex pair', async t => {
let input = Buffer.from('1z2f')
t.throws(
() => readHexString({ searchBuffer: input, startPos: 0 }),
'invalid hex pair encountered: 0x1z'
)
input = Buffer.from('a0b1g692')
t.throws(
() => readHexString({ searchBuffer: input, startPos: 0 }),
'invalid hex pair encountered: 0xg6'
)
})
tap.test('handles incorrect length string', async t => {
const input = Buffer.from('a1b')
t.throws(
() => readHexString({ searchBuffer: input, startPos: 0 }),
'invalid hex pair encountered: 0xb'
)
})
tap.test('reads hex string', async t => {
let input = Buffer.from('0403666f6f')
let result = readHexString({ searchBuffer: input, startPos: 0 })
t.equal(result.endPos, 10)
t.equal(result.berReader.readString(), 'foo')
input = Buffer.from('uid=#0409746573742E75736572')
result = readHexString({ searchBuffer: input, startPos: 5 })
t.equal(result.endPos, input.byteLength)
t.equal(result.berReader.readString(), 'test.user')
})
tap.test('stops on end chars', async t => {
const inputs = [
Buffer.from('0403666f6f foo'),
Buffer.from('0403666f6f+foo'),
Buffer.from('0403666f6f,foo'),
Buffer.from('0403666f6f;foo')
]
for (const input of inputs) {
const result = readHexString({ searchBuffer: input, startPos: 0 })
t.equal(result.endPos, 10)
t.equal(result.berReader.readString(), 'foo')
}
})