First commit
This commit is contained in:
11
node_modules/@ldapjs/dn/lib/deprecations.js
generated
vendored
Normal file
11
node_modules/@ldapjs/dn/lib/deprecations.js
generated
vendored
Normal 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
336
node_modules/@ldapjs/dn/lib/dn.js
generated
vendored
Normal 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
527
node_modules/@ldapjs/dn/lib/dn.test.js
generated
vendored
Normal 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
257
node_modules/@ldapjs/dn/lib/rdn.js
generated
vendored
Normal 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
214
node_modules/@ldapjs/dn/lib/rdn.test.js
generated
vendored
Normal 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
104
node_modules/@ldapjs/dn/lib/utils/escape-value.js
generated
vendored
Normal 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
62
node_modules/@ldapjs/dn/lib/utils/escape-value.test.js
generated
vendored
Normal 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
19
node_modules/@ldapjs/dn/lib/utils/is-dotted-decimal.js
generated
vendored
Normal 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
|
||||
}
|
||||
24
node_modules/@ldapjs/dn/lib/utils/is-dotted-decimal.test.js
generated
vendored
Normal file
24
node_modules/@ldapjs/dn/lib/utils/is-dotted-decimal.test.js
generated
vendored
Normal 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)
|
||||
})
|
||||
58
node_modules/@ldapjs/dn/lib/utils/parse-string/find-name-end.js
generated
vendored
Normal file
58
node_modules/@ldapjs/dn/lib/utils/parse-string/find-name-end.js
generated
vendored
Normal 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
|
||||
}
|
||||
28
node_modules/@ldapjs/dn/lib/utils/parse-string/find-name-end.test.js
generated
vendored
Normal file
28
node_modules/@ldapjs/dn/lib/utils/parse-string/find-name-end.test.js
generated
vendored
Normal 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)
|
||||
})
|
||||
34
node_modules/@ldapjs/dn/lib/utils/parse-string/find-name-start.js
generated
vendored
Normal file
34
node_modules/@ldapjs/dn/lib/utils/parse-string/find-name-start.js
generated
vendored
Normal 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
|
||||
}
|
||||
28
node_modules/@ldapjs/dn/lib/utils/parse-string/find-name-start.test.js
generated
vendored
Normal file
28
node_modules/@ldapjs/dn/lib/utils/parse-string/find-name-start.test.js
generated
vendored
Normal 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
105
node_modules/@ldapjs/dn/lib/utils/parse-string/index.js
generated
vendored
Normal 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
|
||||
}
|
||||
214
node_modules/@ldapjs/dn/lib/utils/parse-string/index.test.js
generated
vendored
Normal file
214
node_modules/@ldapjs/dn/lib/utils/parse-string/index.test.js
generated
vendored
Normal 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)
|
||||
}
|
||||
}
|
||||
})
|
||||
28
node_modules/@ldapjs/dn/lib/utils/parse-string/is-valid-attribute-type-name.js
generated
vendored
Normal file
28
node_modules/@ldapjs/dn/lib/utils/parse-string/is-valid-attribute-type-name.js
generated
vendored
Normal 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
|
||||
}
|
||||
36
node_modules/@ldapjs/dn/lib/utils/parse-string/is-valid-attribute-type-name.test.js
generated
vendored
Normal file
36
node_modules/@ldapjs/dn/lib/utils/parse-string/is-valid-attribute-type-name.test.js
generated
vendored
Normal 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)
|
||||
})
|
||||
73
node_modules/@ldapjs/dn/lib/utils/parse-string/read-attribute-pair.js
generated
vendored
Normal file
73
node_modules/@ldapjs/dn/lib/utils/parse-string/read-attribute-pair.js
generated
vendored
Normal 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 }
|
||||
}
|
||||
}
|
||||
165
node_modules/@ldapjs/dn/lib/utils/parse-string/read-attribute-value.js
generated
vendored
Normal file
165
node_modules/@ldapjs/dn/lib/utils/parse-string/read-attribute-value.js
generated
vendored
Normal 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
|
||||
}
|
||||
}
|
||||
119
node_modules/@ldapjs/dn/lib/utils/parse-string/read-attribute-value.test.js
generated
vendored
Normal file
119
node_modules/@ldapjs/dn/lib/utils/parse-string/read-attribute-value.test.js
generated
vendored
Normal 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)
|
||||
})
|
||||
137
node_modules/@ldapjs/dn/lib/utils/parse-string/read-escape-sequence.js
generated
vendored
Normal file
137
node_modules/@ldapjs/dn/lib/utils/parse-string/read-escape-sequence.js
generated
vendored
Normal 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)
|
||||
}
|
||||
}
|
||||
72
node_modules/@ldapjs/dn/lib/utils/parse-string/read-escape-sequence.test.js
generated
vendored
Normal file
72
node_modules/@ldapjs/dn/lib/utils/parse-string/read-escape-sequence.test.js
generated
vendored
Normal 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)
|
||||
})
|
||||
61
node_modules/@ldapjs/dn/lib/utils/parse-string/read-hex-string.js
generated
vendored
Normal file
61
node_modules/@ldapjs/dn/lib/utils/parse-string/read-hex-string.js
generated
vendored
Normal 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
|
||||
}
|
||||
}
|
||||
52
node_modules/@ldapjs/dn/lib/utils/parse-string/read-hex-string.test.js
generated
vendored
Normal file
52
node_modules/@ldapjs/dn/lib/utils/parse-string/read-hex-string.test.js
generated
vendored
Normal 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')
|
||||
}
|
||||
})
|
||||
Reference in New Issue
Block a user