// Copyright 2011 Mark Cavage, Inc. All rights reserved. const assert = require('assert-plus') /// --- Helpers function invalidDN (name) { const e = new Error() e.name = 'InvalidDistinguishedNameError' e.message = name return e } function isAlphaNumeric (c) { const re = /[A-Za-z0-9]/ return re.test(c) } function isWhitespace (c) { const re = /\s/ return re.test(c) } function repeatChar (c, n) { let out = '' const max = n || 0 for (let i = 0; i < max; i++) { out += c } return out } /// --- API function RDN (obj) { const self = this this.attrs = {} if (obj) { Object.keys(obj).forEach(function (k) { self.set(k, obj[k]) }) } } RDN.prototype.set = function rdnSet (name, value, opts) { assert.string(name, 'name (string) required') assert.string(value, 'value (string) required') const self = this const lname = name.toLowerCase() this.attrs[lname] = { value: value, name: name } if (opts && typeof (opts) === 'object') { Object.keys(opts).forEach(function (k) { if (k !== 'value') { self.attrs[lname][k] = opts[k] } }) } } RDN.prototype.equals = function rdnEquals (rdn) { if (typeof (rdn) !== 'object') { return false } const ourKeys = Object.keys(this.attrs) const theirKeys = Object.keys(rdn.attrs) if (ourKeys.length !== theirKeys.length) { return false } ourKeys.sort() theirKeys.sort() for (let i = 0; i < ourKeys.length; i++) { if (ourKeys[i] !== theirKeys[i]) { return false } if (this.attrs[ourKeys[i]].value !== rdn.attrs[ourKeys[i]].value) { return false } } return true } /** * Convert RDN to string according to specified formatting options. * (see: DN.format for option details) */ RDN.prototype.format = function rdnFormat (options) { assert.optionalObject(options, 'options must be an object') options = options || {} const self = this let str = '' function escapeValue (val, forceQuote) { let out = '' let cur = 0 const len = val.length let quoted = false /* BEGIN JSSTYLED */ // TODO: figure out what this regex is actually trying to test for and // fix it to appease the linter. /* eslint-disable-next-line no-useless-escape */ const escaped = /[\\\"]/ const special = /[,=+<>#;]/ /* END JSSTYLED */ if (len > 0) { // Wrap strings with trailing or leading spaces in quotes quoted = forceQuote || (val[0] === ' ' || val[len - 1] === ' ') } while (cur < len) { if (escaped.test(val[cur]) || (!quoted && special.test(val[cur]))) { out += '\\' } out += val[cur++] } if (quoted) { out = '"' + out + '"' } return out } function sortParsed (a, b) { return self.attrs[a].order - self.attrs[b].order } function sortStandard (a, b) { const nameCompare = a.localeCompare(b) if (nameCompare === 0) { // TODO: Handle binary values return self.attrs[a].value.localeCompare(self.attrs[b].value) } else { return nameCompare } } const keys = Object.keys(this.attrs) if (options.keepOrder) { keys.sort(sortParsed) } else { keys.sort(sortStandard) } keys.forEach(function (key) { const attr = self.attrs[key] if (str.length) { str += '+' } if (options.keepCase) { str += attr.name } else { if (options.upperName) { str += key.toUpperCase() } else { str += key } } str += '=' + escapeValue(attr.value, (options.keepQuote && attr.quoted)) }) return str } RDN.prototype.toString = function rdnToString () { return this.format() } // Thank you OpenJDK! function parse (name) { if (typeof (name) !== 'string') { throw new TypeError('name (string) required') } let cur = 0 const len = name.length function parseRdn () { const rdn = new RDN() let order = 0 rdn.spLead = trim() while (cur < len) { const opts = { order: order } const attr = parseAttrType() trim() if (cur >= len || name[cur++] !== '=') { throw invalidDN(name) } trim() // Parameters about RDN value are set in 'opts' by parseAttrValue const value = parseAttrValue(opts) rdn.set(attr, value, opts) rdn.spTrail = trim() if (cur >= len || name[cur] !== '+') { break } ++cur ++order } return rdn } function trim () { let count = 0 while ((cur < len) && isWhitespace(name[cur])) { ++cur count++ } return count } function parseAttrType () { const beg = cur while (cur < len) { const c = name[cur] if (isAlphaNumeric(c) || c === '.' || c === '-' || c === ' ') { ++cur } else { break } } // Back out any trailing spaces. while ((cur > beg) && (name[cur - 1] === ' ')) { --cur } if (beg === cur) { throw invalidDN(name) } return name.slice(beg, cur) } function parseAttrValue (opts) { if (cur < len && name[cur] === '#') { opts.binary = true return parseBinaryAttrValue() } else if (cur < len && name[cur] === '"') { opts.quoted = true return parseQuotedAttrValue() } else { return parseStringAttrValue() } } function parseBinaryAttrValue () { const beg = cur++ while (cur < len && isAlphaNumeric(name[cur])) { ++cur } return name.slice(beg, cur) } function parseQuotedAttrValue () { let str = '' ++cur // Consume the first quote while ((cur < len) && name[cur] !== '"') { if (name[cur] === '\\') { cur++ } str += name[cur++] } if (cur++ >= len) { // no closing quote throw invalidDN(name) } return str } function parseStringAttrValue () { const beg = cur let str = '' let esc = -1 while ((cur < len) && !atTerminator()) { if (name[cur] === '\\') { // Consume the backslash and mark its place just in case it's escaping // whitespace which needs to be preserved. esc = cur++ } if (cur === len) { // backslash followed by nothing throw invalidDN(name) } str += name[cur++] } // Trim off (unescaped) trailing whitespace and rewind cursor to the end of // the AttrValue to record whitespace length. for (; cur > beg; cur--) { if (!isWhitespace(name[cur - 1]) || (esc === (cur - 1))) { break } } return str.slice(0, cur - beg) } function atTerminator () { return (cur < len && (name[cur] === ',' || name[cur] === ';' || name[cur] === '+')) } const rdns = [] // Short-circuit for empty DNs if (len === 0) { return new DN(rdns) } rdns.push(parseRdn()) while (cur < len) { if (name[cur] === ',' || name[cur] === ';') { ++cur rdns.push(parseRdn()) } else { throw invalidDN(name) } } return new DN(rdns) } function DN (rdns) { assert.optionalArrayOfObject(rdns, '[object] required') this.rdns = rdns ? rdns.slice() : [] this._format = {} } Object.defineProperties(DN.prototype, { length: { get: function getLength () { return this.rdns.length }, configurable: false } }) /** * Convert DN to string according to specified formatting options. * * Parameters: * - options: formatting parameters (optional, details below) * * Options are divided into two types: * - Preservation options: Using data recorded during parsing, details of the * original DN are preserved when converting back into a string. * - Modification options: Alter string formatting defaults. * * Preservation options _always_ take precedence over modification options. * * Preservation Options: * - keepOrder: Order of multi-value RDNs. * - keepQuote: RDN values which were quoted will remain so. * - keepSpace: Leading/trailing spaces will be output. * - keepCase: Parsed attr name will be output instead of lowercased version. * * Modification Options: * - upperName: RDN names will be uppercased instead of lowercased. * - skipSpace: Disable trailing space after RDN separators */ DN.prototype.format = function dnFormat (options) { assert.optionalObject(options, 'options must be an object') options = options || this._format let str = '' this.rdns.forEach(function (rdn) { const rdnString = rdn.format(options) if (str.length !== 0) { str += ',' } if (options.keepSpace) { str += (repeatChar(' ', rdn.spLead) + rdnString + repeatChar(' ', rdn.spTrail)) } else if (options.skipSpace === true || str.length === 0) { str += rdnString } else { str += ' ' + rdnString } }) return str } /** * Set default string formatting options. */ DN.prototype.setFormat = function setFormat (options) { assert.object(options, 'options must be an object') this._format = options } DN.prototype.toString = function dnToString () { return this.format() } DN.prototype.parentOf = function parentOf (dn) { if (typeof (dn) !== 'object') { dn = parse(dn) } if (this.rdns.length >= dn.rdns.length) { return false } const diff = dn.rdns.length - this.rdns.length for (let i = this.rdns.length - 1; i >= 0; i--) { const myRDN = this.rdns[i] const theirRDN = dn.rdns[i + diff] if (!myRDN.equals(theirRDN)) { return false } } return true } DN.prototype.childOf = function childOf (dn) { if (typeof (dn) !== 'object') { dn = parse(dn) } return dn.parentOf(this) } DN.prototype.isEmpty = function isEmpty () { return (this.rdns.length === 0) } DN.prototype.equals = function dnEquals (dn) { if (typeof (dn) !== 'object') { dn = parse(dn) } if (this.rdns.length !== dn.rdns.length) { return false } for (let i = 0; i < this.rdns.length; i++) { if (!this.rdns[i].equals(dn.rdns[i])) { return false } } return true } DN.prototype.parent = function dnParent () { if (this.rdns.length !== 0) { const save = this.rdns.shift() const dn = new DN(this.rdns) this.rdns.unshift(save) return dn } return null } DN.prototype.clone = function dnClone () { const dn = new DN(this.rdns) dn._format = this._format return dn } DN.prototype.reverse = function dnReverse () { this.rdns.reverse() return this } DN.prototype.pop = function dnPop () { return this.rdns.pop() } DN.prototype.push = function dnPush (rdn) { assert.object(rdn, 'rdn (RDN) required') return this.rdns.push(rdn) } DN.prototype.shift = function dnShift () { return this.rdns.shift() } DN.prototype.unshift = function dnUnshift (rdn) { assert.object(rdn, 'rdn (RDN) required') return this.rdns.unshift(rdn) } DN.isDN = function isDN (dn) { if (!dn || typeof (dn) !== 'object') { return false } if (dn instanceof DN) { return true } if (Array.isArray(dn.rdns)) { // Really simple duck-typing for now return true } return false } /// --- Exports module.exports = { parse: parse, DN: DN, RDN: RDN }