1 |
|
2 |
|
3 |
|
4 | var assert = require('assert-plus');
|
5 |
|
6 |
|
7 |
|
8 |
|
9 | function invalidDN(name) {
|
10 | var e = new Error();
|
11 | e.name = 'InvalidDistinguishedNameError';
|
12 | e.message = name;
|
13 | return e;
|
14 | }
|
15 |
|
16 | function isAlphaNumeric(c) {
|
17 | var re = /[A-Za-z0-9]/;
|
18 | return re.test(c);
|
19 | }
|
20 |
|
21 | function isWhitespace(c) {
|
22 | var re = /\s/;
|
23 | return re.test(c);
|
24 | }
|
25 |
|
26 | function repeatChar(c, n) {
|
27 | var out = '';
|
28 | var max = n ? n : 0;
|
29 | for (var i = 0; i < max; i++)
|
30 | out += c;
|
31 | return out;
|
32 | }
|
33 |
|
34 |
|
35 |
|
36 | function RDN(obj) {
|
37 | var self = this;
|
38 | this.attrs = {};
|
39 |
|
40 | if (obj) {
|
41 | Object.keys(obj).forEach(function (k) {
|
42 | self.set(k, obj[k]);
|
43 | });
|
44 | }
|
45 | }
|
46 |
|
47 | RDN.prototype.set = function rdnSet(name, value, opts) {
|
48 | assert.string(name, 'name (string) required');
|
49 | assert.string(value, 'value (string) required');
|
50 |
|
51 | var self = this;
|
52 | var lname = name.toLowerCase();
|
53 | this.attrs[lname] = {
|
54 | value: value,
|
55 | name: name
|
56 | };
|
57 | if (opts && typeof (opts) === 'object') {
|
58 | Object.keys(opts).forEach(function (k) {
|
59 | if (k !== 'value')
|
60 | self.attrs[lname][k] = opts[k];
|
61 | });
|
62 | }
|
63 | };
|
64 |
|
65 | RDN.prototype.equals = function rdnEquals(rdn) {
|
66 | if (typeof (rdn) !== 'object')
|
67 | return false;
|
68 |
|
69 | var ourKeys = Object.keys(this.attrs);
|
70 | var theirKeys = Object.keys(rdn.attrs);
|
71 | if (ourKeys.length !== theirKeys.length)
|
72 | return false;
|
73 |
|
74 | ourKeys.sort();
|
75 | theirKeys.sort();
|
76 |
|
77 | for (var i = 0; i < ourKeys.length; i++) {
|
78 | if (ourKeys[i] !== theirKeys[i])
|
79 | return false;
|
80 | if (this.attrs[ourKeys[i]].value !== rdn.attrs[ourKeys[i]].value)
|
81 | return false;
|
82 | }
|
83 | return true;
|
84 | };
|
85 |
|
86 |
|
87 |
|
88 |
|
89 |
|
90 |
|
91 | RDN.prototype.format = function rdnFormat(options) {
|
92 | assert.optionalObject(options, 'options must be an object');
|
93 | options = options || {};
|
94 |
|
95 | var self = this;
|
96 | var str = '';
|
97 |
|
98 | function escapeValue(val, forceQuote) {
|
99 | var out = '';
|
100 | var cur = 0;
|
101 | var len = val.length;
|
102 | var quoted = false;
|
103 |
|
104 | var escaped = /[\\\"]/;
|
105 | var special = /[,=+<>#;]/;
|
106 |
|
107 |
|
108 | if (len > 0) {
|
109 |
|
110 | quoted = forceQuote || (val[0] == ' ' || val[len-1] == ' ');
|
111 | }
|
112 |
|
113 | while (cur < len) {
|
114 | if (escaped.test(val[cur]) || (!quoted && special.test(val[cur]))) {
|
115 | out += '\\';
|
116 | }
|
117 | out += val[cur++];
|
118 | }
|
119 | if (quoted)
|
120 | out = '"' + out + '"';
|
121 | return out;
|
122 | }
|
123 | function sortParsed(a, b) {
|
124 | return self.attrs[a].order - self.attrs[b].order;
|
125 | }
|
126 | function sortStandard(a, b) {
|
127 | var nameCompare = a.localeCompare(b);
|
128 | if (nameCompare === 0) {
|
129 |
|
130 | return self.attrs[a].value.localeCompare(self.attrs[b].value);
|
131 | } else {
|
132 | return nameCompare;
|
133 | }
|
134 | }
|
135 |
|
136 | var keys = Object.keys(this.attrs);
|
137 | if (options.keepOrder) {
|
138 | keys.sort(sortParsed);
|
139 | } else {
|
140 | keys.sort(sortStandard);
|
141 | }
|
142 |
|
143 | keys.forEach(function (key) {
|
144 | var attr = self.attrs[key];
|
145 | if (str.length)
|
146 | str += '+';
|
147 |
|
148 | if (options.keepCase) {
|
149 | str += attr.name;
|
150 | } else {
|
151 | if (options.upperName)
|
152 | str += key.toUpperCase();
|
153 | else
|
154 | str += key;
|
155 | }
|
156 |
|
157 | str += '=' + escapeValue(attr.value, (options.keepQuote && attr.quoted));
|
158 | });
|
159 |
|
160 | return str;
|
161 | };
|
162 |
|
163 | RDN.prototype.toString = function rdnToString() {
|
164 | return this.format();
|
165 | };
|
166 |
|
167 |
|
168 |
|
169 | function parse(name) {
|
170 | if (typeof (name) !== 'string')
|
171 | throw new TypeError('name (string) required');
|
172 |
|
173 | var cur = 0;
|
174 | var len = name.length;
|
175 |
|
176 | function parseRdn() {
|
177 | var rdn = new RDN();
|
178 | var order = 0;
|
179 | rdn.spLead = trim();
|
180 | while (cur < len) {
|
181 | var opts = {
|
182 | order: order
|
183 | };
|
184 | var attr = parseAttrType();
|
185 | trim();
|
186 | if (cur >= len || name[cur++] !== '=')
|
187 | throw invalidDN(name);
|
188 |
|
189 | trim();
|
190 |
|
191 | var value = parseAttrValue(opts);
|
192 | rdn.set(attr, value, opts);
|
193 | rdn.spTrail = trim();
|
194 | if (cur >= len || name[cur] !== '+')
|
195 | break;
|
196 | ++cur;
|
197 | ++order;
|
198 | }
|
199 | return rdn;
|
200 | }
|
201 |
|
202 |
|
203 | function trim() {
|
204 | var count = 0;
|
205 | while ((cur < len) && isWhitespace(name[cur])) {
|
206 | ++cur;
|
207 | count++;
|
208 | }
|
209 | return count;
|
210 | }
|
211 |
|
212 | function parseAttrType() {
|
213 | var beg = cur;
|
214 | while (cur < len) {
|
215 | var c = name[cur];
|
216 | if (isAlphaNumeric(c) ||
|
217 | c == '.' ||
|
218 | c == '-' ||
|
219 | c == ' ') {
|
220 | ++cur;
|
221 | } else {
|
222 | break;
|
223 | }
|
224 | }
|
225 |
|
226 | while ((cur > beg) && (name[cur - 1] == ' '))
|
227 | --cur;
|
228 |
|
229 | if (beg == cur)
|
230 | throw invalidDN(name);
|
231 |
|
232 | return name.slice(beg, cur);
|
233 | }
|
234 |
|
235 | function parseAttrValue(opts) {
|
236 | if (cur < len && name[cur] == '#') {
|
237 | opts.binary = true;
|
238 | return parseBinaryAttrValue();
|
239 | } else if (cur < len && name[cur] == '"') {
|
240 | opts.quoted = true;
|
241 | return parseQuotedAttrValue();
|
242 | } else {
|
243 | return parseStringAttrValue();
|
244 | }
|
245 | }
|
246 |
|
247 | function parseBinaryAttrValue() {
|
248 | var beg = cur++;
|
249 | while (cur < len && isAlphaNumeric(name[cur]))
|
250 | ++cur;
|
251 |
|
252 | return name.slice(beg, cur);
|
253 | }
|
254 |
|
255 | function parseQuotedAttrValue() {
|
256 | var str = '';
|
257 | ++cur;
|
258 |
|
259 | while ((cur < len) && name[cur] != '"') {
|
260 | if (name[cur] === '\\')
|
261 | cur++;
|
262 | str += name[cur++];
|
263 | }
|
264 | if (cur++ >= len)
|
265 | throw invalidDN(name);
|
266 |
|
267 | return str;
|
268 | }
|
269 |
|
270 | function parseStringAttrValue() {
|
271 | var beg = cur;
|
272 | var str = '';
|
273 | var esc = -1;
|
274 |
|
275 | while ((cur < len) && !atTerminator()) {
|
276 | if (name[cur] === '\\') {
|
277 |
|
278 |
|
279 | esc = cur++;
|
280 | }
|
281 | if (cur === len)
|
282 | throw invalidDN(name);
|
283 | str += name[cur++];
|
284 | }
|
285 |
|
286 |
|
287 |
|
288 | for (; cur > beg; cur--) {
|
289 | if (!isWhitespace(name[cur - 1]) || (esc === (cur - 1)))
|
290 | break;
|
291 | }
|
292 | return str.slice(0, cur - beg);
|
293 | }
|
294 |
|
295 | function atTerminator() {
|
296 | return (cur < len &&
|
297 | (name[cur] === ',' ||
|
298 | name[cur] === ';' ||
|
299 | name[cur] === '+'));
|
300 | }
|
301 |
|
302 | var rdns = [];
|
303 |
|
304 |
|
305 | if (len === 0)
|
306 | return new DN(rdns);
|
307 |
|
308 | rdns.push(parseRdn());
|
309 | while (cur < len) {
|
310 | if (name[cur] === ',' || name[cur] === ';') {
|
311 | ++cur;
|
312 | rdns.push(parseRdn());
|
313 | } else {
|
314 | throw invalidDN(name);
|
315 | }
|
316 | }
|
317 |
|
318 | return new DN(rdns);
|
319 | }
|
320 |
|
321 |
|
322 | function DN(rdns) {
|
323 | assert.optionalArrayOfObject(rdns, '[object] required');
|
324 |
|
325 | this.rdns = rdns ? rdns.slice() : [];
|
326 | this._format = {};
|
327 | }
|
328 | Object.defineProperties(DN.prototype, {
|
329 | length: {
|
330 | get: function getLength() { return this.rdns.length; },
|
331 | configurable: false
|
332 | }
|
333 | });
|
334 |
|
335 |
|
336 |
|
337 |
|
338 |
|
339 |
|
340 |
|
341 |
|
342 |
|
343 |
|
344 |
|
345 |
|
346 |
|
347 |
|
348 |
|
349 |
|
350 |
|
351 |
|
352 |
|
353 |
|
354 |
|
355 |
|
356 |
|
357 |
|
358 | DN.prototype.format = function dnFormat(options) {
|
359 | assert.optionalObject(options, 'options must be an object');
|
360 | options = options || this._format;
|
361 |
|
362 | var str = '';
|
363 | this.rdns.forEach(function (rdn) {
|
364 | var rdnString = rdn.format(options);
|
365 | if (str.length !== 0) {
|
366 | str += ',';
|
367 | }
|
368 | if (options.keepSpace) {
|
369 | str += (repeatChar(' ', rdn.spLead) +
|
370 | rdnString + repeatChar(' ', rdn.spTrail));
|
371 | } else if (options.skipSpace === true || str.length === 0) {
|
372 | str += rdnString;
|
373 | } else {
|
374 | str += ' ' + rdnString;
|
375 | }
|
376 | });
|
377 | return str;
|
378 | };
|
379 |
|
380 |
|
381 |
|
382 |
|
383 | DN.prototype.setFormat = function setFormat(options) {
|
384 | assert.object(options, 'options must be an object');
|
385 |
|
386 | this._format = options;
|
387 | };
|
388 |
|
389 | DN.prototype.toString = function dnToString() {
|
390 | return this.format();
|
391 | };
|
392 |
|
393 | DN.prototype.parentOf = function parentOf(dn) {
|
394 | if (typeof (dn) !== 'object')
|
395 | dn = parse(dn);
|
396 |
|
397 | if (this.rdns.length >= dn.rdns.length)
|
398 | return false;
|
399 |
|
400 | var diff = dn.rdns.length - this.rdns.length;
|
401 | for (var i = this.rdns.length - 1; i >= 0; i--) {
|
402 | var myRDN = this.rdns[i];
|
403 | var theirRDN = dn.rdns[i + diff];
|
404 |
|
405 | if (!myRDN.equals(theirRDN))
|
406 | return false;
|
407 | }
|
408 |
|
409 | return true;
|
410 | };
|
411 |
|
412 | DN.prototype.childOf = function childOf(dn) {
|
413 | if (typeof (dn) !== 'object')
|
414 | dn = parse(dn);
|
415 | return dn.parentOf(this);
|
416 | };
|
417 |
|
418 | DN.prototype.isEmpty = function isEmpty() {
|
419 | return (this.rdns.length === 0);
|
420 | };
|
421 |
|
422 | DN.prototype.equals = function dnEquals(dn) {
|
423 | if (typeof (dn) !== 'object')
|
424 | dn = parse(dn);
|
425 |
|
426 | if (this.rdns.length !== dn.rdns.length)
|
427 | return false;
|
428 |
|
429 | for (var i = 0; i < this.rdns.length; i++) {
|
430 | if (!this.rdns[i].equals(dn.rdns[i]))
|
431 | return false;
|
432 | }
|
433 |
|
434 | return true;
|
435 | };
|
436 |
|
437 | DN.prototype.parent = function dnParent() {
|
438 | if (this.rdns.length !== 0) {
|
439 | var save = this.rdns.shift();
|
440 | var dn = new DN(this.rdns);
|
441 | this.rdns.unshift(save);
|
442 | return dn;
|
443 | }
|
444 |
|
445 | return null;
|
446 | };
|
447 |
|
448 | DN.prototype.clone = function dnClone() {
|
449 | var dn = new DN(this.rdns);
|
450 | dn._format = this._format;
|
451 | return dn;
|
452 | };
|
453 |
|
454 | DN.prototype.reverse = function dnReverse() {
|
455 | this.rdns.reverse();
|
456 | return this;
|
457 | };
|
458 |
|
459 | DN.prototype.pop = function dnPop() {
|
460 | return this.rdns.pop();
|
461 | };
|
462 |
|
463 | DN.prototype.push = function dnPush(rdn) {
|
464 | assert.object(rdn, 'rdn (RDN) required');
|
465 |
|
466 | return this.rdns.push(rdn);
|
467 | };
|
468 |
|
469 | DN.prototype.shift = function dnShift() {
|
470 | return this.rdns.shift();
|
471 | };
|
472 |
|
473 | DN.prototype.unshift = function dnUnshift(rdn) {
|
474 | assert.object(rdn, 'rdn (RDN) required');
|
475 |
|
476 | return this.rdns.unshift(rdn);
|
477 | };
|
478 |
|
479 | DN.isDN = function isDN(dn) {
|
480 | if (!dn || typeof (dn) !== 'object') {
|
481 | return false;
|
482 | }
|
483 | if (dn instanceof DN) {
|
484 | return true;
|
485 | }
|
486 | if (Array.isArray(dn.rdns)) {
|
487 |
|
488 | return true;
|
489 | }
|
490 | return false;
|
491 | };
|
492 |
|
493 |
|
494 |
|
495 |
|
496 | module.exports = {
|
497 | parse: parse,
|
498 | DN: DN,
|
499 | RDN: RDN
|
500 | };
|