From 22b04f3a945cb39cedc0eab315053a4b9df405d6 Mon Sep 17 00:00:00 2001 From: Patrick Mooney Date: Thu, 16 Oct 2014 10:36:52 -0500 Subject: [PATCH] Move filter parsing into separate module --- lib/filters/and_filter.js | 63 +---- lib/filters/approx_filter.js | 53 +--- lib/filters/equality_filter.js | 73 +----- lib/filters/ext_filter.js | 123 +-------- lib/filters/filter.js | 115 +++------ lib/filters/ge_filter.js | 53 +--- lib/filters/index.js | 446 ++++----------------------------- lib/filters/le_filter.js | 53 +--- lib/filters/not_filter.js | 44 +--- lib/filters/or_filter.js | 62 +---- lib/filters/presence_filter.js | 38 +-- lib/filters/substr_filter.js | 102 +------- package.json | 1 + test/filters/approx.test.js | 34 --- test/filters/ext.test.js | 23 -- test/filters/filter.test.js | 62 ----- 16 files changed, 130 insertions(+), 1215 deletions(-) delete mode 100644 test/filters/filter.test.js diff --git a/lib/filters/and_filter.js b/lib/filters/and_filter.js index f72524c..2b7e73c 100644 --- a/lib/filters/and_filter.js +++ b/lib/filters/and_filter.js @@ -3,75 +3,22 @@ var assert = require('assert'); var util = require('util'); -var Filter = require('./filter'); +var parents = require('ldap-filter'); -var Protocol = require('../protocol'); +var Filter = require('./filter'); ///--- API function AndFilter(options) { - if (typeof (options) === 'object') { - if (!options.filters || !Array.isArray(options.filters)) - throw new TypeError('options.filters ([Filter]) required'); - this.filters = options.filters.slice(); - } else { - options = {}; - } - - options.type = Protocol.FILTER_AND; - Filter.call(this, options); - - if (!this.filters) - this.filters = []; - - var self = this; - this.__defineGetter__('json', function () { - return { - type: 'And', - filters: self.filters || [] - }; - }); + parents.AndFilter.call(this, options); } -util.inherits(AndFilter, Filter); +util.inherits(AndFilter, parents.AndFilter); +Filter.mixin(AndFilter); module.exports = AndFilter; -AndFilter.prototype.toString = function () { - var str = '(&'; - this.filters.forEach(function (f) { - str += f.toString(); - }); - str += ')'; - - return str; -}; - - -AndFilter.prototype.matches = function (target) { - if (typeof (target) !== 'object') - throw new TypeError('target (object) required'); - - var matches = this.filters.length ? true : false; - - for (var i = 0; i < this.filters.length; i++) { - if (!this.filters[i].matches(target)) - return false; - } - - return matches; -}; - - -AndFilter.prototype.addFilter = function (filter) { - if (!filter || typeof (filter) !== 'object') - throw new TypeError('filter (object) required'); - - this.filters.push(filter); -}; - - AndFilter.prototype._toBer = function (ber) { assert.ok(ber); diff --git a/lib/filters/approx_filter.js b/lib/filters/approx_filter.js index ebc081f..2a7a1f1 100644 --- a/lib/filters/approx_filter.js +++ b/lib/filters/approx_filter.js @@ -3,67 +3,22 @@ var assert = require('assert'); var util = require('util'); -var escape = require('./escape').escape; +var parents = require('ldap-filter'); var Filter = require('./filter'); -var Protocol = require('../protocol'); - ///--- API function ApproximateFilter(options) { - if (typeof (options) === 'object') { - if (!options.attribute || typeof (options.attribute) !== 'string') - throw new TypeError('options.attribute (string) required'); - if (!options.value || typeof (options.value) !== 'string') - throw new TypeError('options.value (string) required'); - this.attribute = options.attribute; - this.value = options.value; - } else { - options = {}; - } - options.type = Protocol.FILTER_APPROX; - Filter.call(this, options); - - var self = this; - this.__defineGetter__('json', function () { - return { - type: 'ApproximateMatch', - attribute: self.attribute || undefined, - value: self.value || undefined - }; - }); + parents.ApproximateFilter.call(this, options); } -util.inherits(ApproximateFilter, Filter); +util.inherits(ApproximateFilter, parents.ApproximateFilter); +Filter.mixin(ApproximateFilter); module.exports = ApproximateFilter; -ApproximateFilter.prototype.toString = function () { - return '(' + escape(this.attribute) + '~=' + escape(this.value) + ')'; -}; - - -ApproximateFilter.prototype.matches = function (target) { - if (typeof (target) !== 'object') - throw new TypeError('target (object) required'); - - var matches = false; - var tv = Filter.get_attr_caseless(target, this.attribute); - - if (tv !== null) { - if (Array.isArray(tv)) { - matches = (tv.indexOf(this.value) != -1); - } else { - matches = (this.value === tv); - } - } - - return matches; -}; - - ApproximateFilter.prototype.parse = function (ber) { assert.ok(ber); diff --git a/lib/filters/equality_filter.js b/lib/filters/equality_filter.js index 151290b..a72ac94 100644 --- a/lib/filters/equality_filter.js +++ b/lib/filters/equality_filter.js @@ -4,87 +4,22 @@ var assert = require('assert'); var util = require('util'); var ASN1 = require('asn1').Ber; - -var escape = require('./escape').escape; +var parents = require('ldap-filter'); var Filter = require('./filter'); -var Protocol = require('../protocol'); - ///--- API function EqualityFilter(options) { - if (typeof (options) === 'object') { - if (!options.attribute || typeof (options.attribute) !== 'string') - throw new TypeError('options.attribute (string) required'); - if (!options.value) - throw new TypeError('options.value required'); - } else { - this.raw = new Buffer(0); - options = {}; - } - options.type = Protocol.FILTER_EQUALITY; - Filter.call(this, options); - - var self = this; - this.__defineGetter__('value', function () { - return self.raw.toString(); - }); - this.__defineSetter__('value', function (data) { - if (typeof (data) === 'string') { - self.raw = new Buffer(data); - } else if (Buffer.isBuffer(data)) { - self.raw = new Buffer(data.length); - data.copy(self.raw); - } else { - throw new TypeError('value (string|buffer) required'); - } - }); - this.__defineGetter__('json', function () { - return { - type: 'EqualityMatch', - attribute: self.attribute || undefined, - value: self.value || undefined - }; - }); - if (options.attribute !== undefined && options.value !== undefined) { - this.attribute = options.attribute; - this.value = options.value; - } + parents.EqualityFilter.call(this, options); } -util.inherits(EqualityFilter, Filter); +util.inherits(EqualityFilter, parents.EqualityFilter); +Filter.mixin(EqualityFilter); module.exports = EqualityFilter; -EqualityFilter.prototype.toString = function () { - return '(' + escape(this.attribute) + '=' + escape(this.value) + ')'; -}; - - -EqualityFilter.prototype.matches = function (target) { - if (typeof (target) !== 'object') - throw new TypeError('target (object) required'); - - var self = this; - var tv = Filter.get_attr_caseless(target, this.attribute); - - if (tv !== null) { - var value = this.value; - return Filter.multi_test( - function (v) { - if (self.attribute === 'objectclass') - v = v.toLowerCase(); - return value === v; - }, - tv); - } - - return false; -}; - - EqualityFilter.prototype.parse = function (ber) { assert.ok(ber); diff --git a/lib/filters/ext_filter.js b/lib/filters/ext_filter.js index 59ebf62..c3ffe7e 100644 --- a/lib/filters/ext_filter.js +++ b/lib/filters/ext_filter.js @@ -3,132 +3,27 @@ var assert = require('assert'); var util = require('util'); +var parents = require('ldap-filter'); + var Filter = require('./filter'); -var Protocol = require('../protocol'); -var SubstringFilter = require('./substr_filter'); +// THIS IS A STUB! +// +// ldapjs does not support server side extensible matching. +// This class exists only for the client to send them. ///--- API function ExtensibleFilter(options) { - if (typeof (options) === 'object') { - if (options.rule && typeof (options.rule) !== 'string') - throw new TypeError('options.rule must be a string'); - if (options.matchType && typeof (options.matchType) !== 'string') - throw new TypeError('options.type must be a string'); - if (options.value && typeof (options.value) !== 'string') - throw new TypeError('options.value (string) required'); - } else { - options = {}; - } - - this.any = options.any || null; - this.attribute = options.matchType || null; - this['final'] = options['final'] || null; - this.initial = options.initial || null; - this.matchType = options.matchType || null; - this.rule = options.rule || null; - this.value = options.value || ''; - this.dnAttributes = options.dnAttributes || false; - options.type = Protocol.FILTER_EXT; - Filter.call(this, options); - - var self = this; - this.__defineGetter__('json', function () { - return { - type: 'ExtensibleMatch', - matchRule: self.rule, - matchType: self.matchType, - matchValue: self.value, - dnAttributes: self.dnAttributes - }; - }); - this.__defineGetter__('matchingRule', function () { - return self.rule; - }); - this.__defineGetter__('matchValue', function () { - return self.value; - }); + parents.ExtensibleFilter.call(this, options); } -util.inherits(ExtensibleFilter, Filter); +util.inherits(ExtensibleFilter, parents.ExtensibleFilter); +Filter.mixin(ExtensibleFilter); module.exports = ExtensibleFilter; -ExtensibleFilter.prototype.toString = function () { - var str = '('; - - if (this.matchType) - str += this.matchType; - - str += ':'; - - if (this.dnAttributes) - str += 'dn:'; - - if (this.rule) - str += this.rule + ':'; - - return (str + '=' + this.value + ')'); -}; - - -/** - * THIS IS A STUB! - * - * ldapjs does not support server side extensible matching. This class exists - * only for the client to send them. - * - * @param {Object} target the target object. - * @return {Boolean} false always. - */ -ExtensibleFilter.prototype.matches = function (target) { - if (typeof (target) !== 'object') - throw new TypeError('target (object) required'); - - if (this.dnAttribute) - throw new Error('ExtensibleMatch dnAttributes not supported'); - - if (!this.value) - return false; - - var self = this; - var tv = Filter.get_attr_caseless(target, this.matchType); - - if (this.matchType && tv !== null) { - - if (self.rule === '2.5.13.4' || self.rule === 'caseIgnoreSubstringsMatch') { - var f = { - attribute: self.matchType, - initial: self.initial ? self.initial.toLowerCase() : undefined, - any: self.any ? self.any.map(function (a) { - return a.toLowerCase(); - }) : undefined, - 'final': self['final'] ? self['final'].toLowerCase() : undefined - }; - } - - return Filter.multi_test(function (v) { - if (self.rule === '2.5.13.2' || self.rule === 'caseIgnoreMatch') { - return self.value.toLowerCase() === v.toLowerCase(); - } else if (self.rule === '2.5.13.4' || - self.rule === 'caseIgnoreSubstringsMatch') { - - var t = {}; - t[self.matchType] = v.toLowerCase(); - - return SubstringFilter.prototype.matches.call(f, t); - } - - return self.value === v; - }, tv); - } - - return false; -}; - - ExtensibleFilter.prototype.parse = function (ber) { var end = ber.offset + ber.length; while (ber.offset < end) { diff --git a/lib/filters/filter.js b/lib/filters/filter.js index fbbdcea..60e2551 100644 --- a/lib/filters/filter.js +++ b/lib/filters/filter.js @@ -4,106 +4,55 @@ var assert = require('assert'); var asn1 = require('asn1'); - var Protocol = require('../protocol'); - ///--- Globals var BerWriter = asn1.BerWriter; +var TYPES = { + 'and': Protocol.FILTER_AND, + 'or': Protocol.FILTER_OR, + 'not': Protocol.FILTER_NOT, + 'equal': Protocol.FILTER_EQUALITY, + 'substring': Protocol.FILTER_SUBSTRINGS, + 'ge': Protocol.FILTER_GE, + 'le': Protocol.FILTER_LE, + 'present': Protocol.FILTER_PRESENT, + 'approx': Protocol.FILTER_APPROX, + 'ext': Protocol.FILTER_EXT +}; ///--- API -function Filter(options) { - if (!options || typeof (options) !== 'object') - throw new TypeError('options (object) required'); - if (typeof (options.type) !== 'number') - throw new TypeError('options.type (number) required'); - - this._type = options.type; - - var self = this; - this.__defineGetter__('type', function () { - switch (self._type) { - case Protocol.FILTER_AND: return 'and'; - case Protocol.FILTER_OR: return 'or'; - case Protocol.FILTER_NOT: return 'not'; - case Protocol.FILTER_EQUALITY: return 'equal'; - case Protocol.FILTER_SUBSTRINGS: return 'substring'; - case Protocol.FILTER_GE: return 'ge'; - case Protocol.FILTER_LE: return 'le'; - case Protocol.FILTER_PRESENT: return 'present'; - case Protocol.FILTER_APPROX: return 'approx'; - case Protocol.FILTER_EXT: return 'ext'; - default: - throw new Error('0x' + self._type.toString(16) + - ' is an invalid search filter'); - } - }); -} - -Filter.isFilter = function isFilter(filter) { +function isFilter(filter) { if (!filter || typeof (filter) !== 'object') { return false; } // Do our best to duck-type it - if ((filter instanceof Filter) || ( - typeof (filter.toBer) === 'functin' && + if (typeof (filter.toBer) === 'function' && typeof (filter.matches) === 'function' && - typeof (filter._type) === 'number')) { + TYPES[filter.type] !== undefined) { return true; } return false; -}; - -module.exports = Filter; - - -Filter.prototype.toBer = function (ber) { - if (!ber || !(ber instanceof BerWriter)) - throw new TypeError('ber (BerWriter) required'); - - ber.startSequence(this._type); - ber = this._toBer(ber); - ber.endSequence(); - return ber; -}; - - -// Test a rule against one or more values. -Filter.multi_test = function (rule, value) { - if (Array.isArray(value)) { - var response = false; - for (var i = 0; i < value.length; i++) { - if (rule(value[i])) { - response = true; - break; - } - } - return response; - } else { - return rule(value); - } -}; - -// Search object for attribute, insensitive to case -Filter.get_attr_caseless = function (target, attr) { - // Check for exact case match first - if (target.hasOwnProperty(attr)) { - return target[attr]; - } - // Perform case-insensitive enumeration after that - var lower = attr.toLowerCase(); - var result = null; - Object.getOwnPropertyNames(target).some(function (item) { - if (item.toLowerCase() == lower) { - result = target[item]; - return true; - } - return false; - }); - return result; +} + +function mixin(target) { + target.prototype.toBer = function toBer(ber) { + if (!ber || !(ber instanceof BerWriter)) + throw new TypeError('ber (BerWriter) required'); + + ber.startSequence(TYPES[this.type]); + ber = this._toBer(ber); + ber.endSequence(); + return ber; + }; +} + +module.exports = { + isFilter: isFilter, + mixin: mixin }; diff --git a/lib/filters/ge_filter.js b/lib/filters/ge_filter.js index 26c62a9..6f10c70 100644 --- a/lib/filters/ge_filter.js +++ b/lib/filters/ge_filter.js @@ -3,66 +3,21 @@ var assert = require('assert'); var util = require('util'); -var escape = require('./escape').escape; +var parents = require('ldap-filter'); var Filter = require('./filter'); -var Protocol = require('../protocol'); - - ///--- API function GreaterThanEqualsFilter(options) { - if (typeof (options) === 'object') { - if (!options.attribute || typeof (options.attribute) !== 'string') - throw new TypeError('options.attribute (string) required'); - if (!options.value || typeof (options.value) !== 'string') - throw new TypeError('options.value (string) required'); - this.attribute = options.attribute; - this.value = options.value; - } else { - options = {}; - } - - options.type = Protocol.FILTER_GE; - Filter.call(this, options); - - var self = this; - this.__defineGetter__('json', function () { - return { - type: 'GreaterThanEqualsMatch', - attribute: self.attribute || undefined, - value: self.value || undefined - }; - }); + parents.GreaterThanEqualsFilter.call(this, options); } -util.inherits(GreaterThanEqualsFilter, Filter); +util.inherits(GreaterThanEqualsFilter, parents.GreaterThanEqualsFilter); +Filter.mixin(GreaterThanEqualsFilter); module.exports = GreaterThanEqualsFilter; -GreaterThanEqualsFilter.prototype.toString = function () { - return '(' + escape(this.attribute) + '>=' + escape(this.value) + ')'; -}; - - -GreaterThanEqualsFilter.prototype.matches = function (target) { - if (typeof (target) !== 'object') - throw new TypeError('target (object) required'); - - var tv = Filter.get_attr_caseless(target, this.attribute); - - if (tv !== null) { - var value = this.value; - return Filter.multi_test( - function (v) { return value <= v; }, - tv); - } - - return false; -}; - - GreaterThanEqualsFilter.prototype.parse = function (ber) { assert.ok(ber); diff --git a/lib/filters/index.js b/lib/filters/index.js index d4bf21b..feb0717 100644 --- a/lib/filters/index.js +++ b/lib/filters/index.js @@ -4,6 +4,8 @@ var assert = require('assert'); var asn1 = require('asn1'); +var parents = require('ldap-filter'); + var Protocol = require('../protocol'); var Filter = require('./filter'); @@ -25,400 +27,8 @@ var SubstringFilter = require('./substr_filter'); var BerReader = asn1.BerReader; - ///--- Internal Parsers -// expression parsing -// returns the index of the closing parenthesis matching the open paren -// specified by openParenIndex -function matchParens(str, openParenIndex) { - var stack = []; - var esc = false; - for (var i = openParenIndex || 0; i < str.length; i++) { - var c = str[i]; - - if (c === '\\') { - if (!esc) - esc = true; - continue; - } else if (c === '(' && !esc) { - stack.push(1); - } else if (c === ')' && !esc) { - stack.pop(); - if (stack.length === 0) - return i; - } - - esc = false; - } - - var ndx = str.length - 1; - if (str.charAt(ndx) !== ')') - throw new Error(str + ' has unbalanced parentheses'); - - return ndx; -} - - -function parse_substr(tree) { - // Effectively a hand-rolled .shift() to support \* sequences - var clean = true; - var esc = false; - var obj = {}; - var split = []; - var substrNdx = 0; - - split[substrNdx] = ''; - - for (var i = 0; i < tree.value.length; i++) { - var c = tree.value[i]; - if (esc) { - split[substrNdx] += c; - esc = false; - } else if (c === '*') { - split[++substrNdx] = ''; - } else if (c === '\\') { - esc = true; - } else { - split[substrNdx] += c; - } - } - - if (split.length > 1) { - obj.tag = 'substrings'; - clean = true; - - // if the value string doesn't start with a * then theres no initial - // value else split will have an empty string in its first array - // index... - // we need to remove that empty string - if (tree.value.indexOf('*') !== 0) { - obj.initial = split.shift(); - } else { - split.shift(); - } - - // if the value string doesn't end with a * then theres no final - // value also same split stuff as the initial stuff above - if (tree.value.lastIndexOf('*') !== tree.value.length - 1) { - obj['final'] = split.pop(); - } else { - split.pop(); - } - obj.any = split; - } else { - obj.value = split[0]; // pick up the cleaned version - } - - obj.clean = clean; - obj.esc = esc; - - return obj; -} - -// recursive function that builds a filter tree from a string expression -// the filter tree is an intermediary step between the incoming expression and -// the outgoing Filter Class structure. -function _buildFilterTree(expr) { - var c; - var child; - var clean = false; - var endParen; - var esc = false; - var i = 0; - var obj; - var tree = {}; - var split; - var val = ''; - - if (expr.length === 0) - return tree; - - // Chop the parens (the call to matchParens below gets rid of the trailer) - if (expr.charAt(0) == '(') - expr = expr.substring(1, expr.length - 1); - - //store prefix operator - if (expr.charAt(0) === '&') { - tree.op = 'and'; - expr = expr.substring(1); - } else if (expr.charAt(0) === '|') { - tree.op = 'or'; - expr = expr.substring(1); - } else if (expr.charAt(0) === '!') { - tree.op = 'not'; - expr = expr.substring(1); - } else { - tree.op = 'expr'; - } - - if (tree.op != 'expr') { - tree.children = []; - - // logical operators are k-ary, so we go until our expression string runs - // out (at least for this recursion level) - while (expr.length !== 0) { - endParen = matchParens(expr); - - if (endParen == expr.length - 1) { - tree.children[i] = _buildFilterTree(expr); - expr = ''; - } else { - child = expr.slice(0, endParen + 1); - expr = expr.substring(endParen + 1); - tree.children[i] = _buildFilterTree(child); - } - i++; - } - } else { - //else its some sort of non-logical expression, parse and return as such - var operatorStr = ''; - tree.name = ''; - tree.value = ''; - - - // This parses and enforces filter syntax, which is an AttributeDescription - // plus a filter operator, followed by (for ldapjs), anything. Note - // that ldapjs additionally allows the '_' character in the AD, as many - // users rely on it, even though it's non-standard - // - // From 4.1.5 of RFC251 - // - // AttributeDescription ::= LDAPString - // - // A value of AttributeDescription is based on the following BNF: - // - // ::= [ ";" ] - // - // ::=