commit ca1443f1023acf74c724c70269e9849d6e558744 Author: Mark Cavage Date: Thu Aug 4 13:32:01 2011 -0700 Initial working client/server version diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..61d1d99 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +node_modules +*.log +*.ldif diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..9b5dcdb --- /dev/null +++ b/LICENSE @@ -0,0 +1,19 @@ +Copyright (c) 2011 Mark Cavage, All rights reserved. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE diff --git a/README.md b/README.md new file mode 100644 index 0000000..00197b4 --- /dev/null +++ b/README.md @@ -0,0 +1,13 @@ +node-ldapjs will blow your mind. Docs coming soon. + +## Installation + + npm install ldapjs + +## License + +MIT. + +## Bugs + +See . diff --git a/lib/attribute.js b/lib/attribute.js new file mode 100644 index 0000000..d6b25fa --- /dev/null +++ b/lib/attribute.js @@ -0,0 +1,104 @@ +// Copyright 2011 Mark Cavage, Inc. All rights reserved. + +var assert = require('assert'); + +var Protocol = require('./protocol'); + + + +///--- API + +function Attribute(options) { + if (options) { + if (typeof(options) !== 'object') + throw new TypeError('options must be an object'); + if (options.type && typeof(options.type) !== 'string') + throw new TypeError('options.type must be a string'); + if (options.vals && !Array.isArray(options.vals)) + throw new TypeErrr('options.vals must be an array[string]'); + if (options.vals && options.vals.length) { + options.vals.forEach(function(v) { + if (typeof(v) !== 'string') + throw new TypeErrr('options.vals must be an array[string]'); + }); + } + } else { + options = {}; + } + + this.type = options.type || ''; + this.vals = options.vals ? options.vals.slice(0) : []; + + var self = this; + this.__defineGetter__('json', function() { + return { + type: self.type, + vals: self.vals + }; + }); +} +module.exports = Attribute; + + +Attribute.prototype.addValue = function(val) { + if (typeof(val) !== 'string') + throw new TypeError('val (string) required'); + + this.vals.push(val); +}; + + +Attribute.prototype.parse = function(ber) { + assert.ok(ber); + + ber.readSequence(); + this.type = ber.readString(); + + + if (ber.readSequence(Protocol.LBER_SET)) { + var end = ber.offset + ber.length; + while (ber.offset < end) { + var val = ber.readString(); + this.vals.push(val); + } + } + + return true; +}; + + +Attribute.prototype.toBer = function(ber) { + assert.ok(ber); + + ber.startSequence(); + ber.writeString(this.type); + if (this.vals && this.vals.length) { + ber.startSequence(Protocol.LBER_SET); + ber.writeStringArray(this.vals); + ber.endSequence(); + } + ber.endSequence(); + + return ber; +}; + +Attribute.toBer = function(attr, ber) { + return Attribute.prototype.toBer.call(attr, ber); +}; + + +Attribute.isAttribute = function(attr) { + if (typeof(attr) !== 'object') return false; + if (attr instanceof Attribute) return true; + if (!attr.type || typeof(attr.type) !== 'string') return false; + if (!attr.vals || !Array.isArray(attr.vals)) return false; + for (var i = 0; i < attr.vals.length; i++) + if (typeof(attr.vals[i]) !== 'string') return false; + + return true; +}; + + +Attribute.prototype.toString = function() { + return JSON.stringify(this.json); +}; diff --git a/lib/change.js b/lib/change.js new file mode 100644 index 0000000..a650237 --- /dev/null +++ b/lib/change.js @@ -0,0 +1,86 @@ +// Copyright 2011 Mark Cavage, Inc. All rights reserved. + +var assert = require('assert'); + +var Attribute = require('./attribute'); +var Protocol = require('./protocol'); + + + +///--- API + +function Change(options) { + if (options) { + if (typeof(options) !== 'object') + throw new TypeError('options must be an object'); + if (options.operation && typeof(options.operation) !== 'string') + throw new TypeError('options.operation must be a string'); + if (options.modification && !(options.modification instanceof Attribute)) + throw new TypeErrr('options.modification must be an Attribute'); + } else { + options = {}; + } + + var self = this; + this.__defineGetter__('operation', function() { + switch (self._operation) { + case 0x00: return 'Add'; + case 0x01: return 'Delete'; + case 0x02: return 'Replace'; + default: return 'Invalid'; + } + }); + this.__defineSetter__('operation', function(val) { + if (typeof(val) !== 'string') + throw new TypeError('operation must be a string'); + + switch (val.toLowerCase()) { + case 'add': + self._operation = 0x00; + break; + case 'delete': + self._operation = 0x01; + break; + case 'replace': + self._operation = 0x02; + break; + default: + throw new Error('Invalid operation type: 0x' + val.toString(16)); + } + }); + + this.__defineGetter__('json', function() { + return { + operation: self.operation, + modification: self.modification ? self.modification.json : {} + }; + }); + + this.operation = options.operation || 'add'; + this.modification = options.modification || null; +} +module.exports = Change; + + +Change.prototype.parse = function(ber) { + assert.ok(ber); + + ber.readSequence(); + this._operation = ber.readEnumeration(); + this.modification = new Attribute(); + this.modification.parse(ber); + + return true; +}; + + +Change.prototype.toBer = function(ber) { + assert.ok(ber); + + ber.startSequence(); + ber.writeEnumeration(this._operation); + ber = this.modification.toBer(ber); + ber.endSequence(); + + return ber; +}; diff --git a/lib/client.js b/lib/client.js new file mode 100644 index 0000000..a00fd40 --- /dev/null +++ b/lib/client.js @@ -0,0 +1,726 @@ +// Copyright 2011 Mark Cavage, Inc. All rights reserved. + +var assert = require('assert'); +var EventEmitter = require('events').EventEmitter; +var net = require('net'); +var tls = require('tls'); +var util = require('util'); + +var Attribute = require('./attribute'); +var Change = require('./change'); +var Control = require('./control'); +var Protocol = require('./protocol'); +var dn = require('./dn'); +var errors = require('./errors'); +var filters = require('./filters'); +var logStub = require('./log_stub'); +var messages = require('./messages'); +var url = require('./url'); + + + +///--- Globals + +var AddRequest = messages.AddRequest; +var BindRequest = messages.BindRequest; +var CompareRequest = messages.CompareRequest; +var DeleteRequest = messages.DeleteRequest; +var ExtendedRequest = messages.ExtendedRequest; +var ModifyRequest = messages.ModifyRequest; +var ModifyDNRequest = messages.ModifyDNRequest; +var SearchRequest = messages.SearchRequest; +var UnbindRequest = messages.UnbindRequest; + +var LDAPResult = messages.LDAPResult; +var SearchEntry = messages.SearchEntry; +var SearchResponse = messages.SearchResponse; +var Parser = messages.Parser; + + +var Filter = filters.Filter; +var PresenceFilter = filters.PresenceFilter; + + +var MAX_MSGID = Math.pow(2, 31) - 1; + + + +///--- Internal Helpers + +function xor() { + var b = false; + for (var i = 0; i < arguments.length; i++) { + if (arguments[i] && !b) b = true; + else if (arguments[i] && b) return false; + } + return b; +} + + +function validateControls(controls) { + if (Array.isArray(controls)) { + controls.forEach(function(c) { + if (!(c instanceof Control)) + throw new TypeError('controls must be [Control]'); + }); + } else if (controls instanceof Control) { + controls = [controls]; + } else { + throw new TypeError('controls must be [Control]'); + } + + return controls; +} + + +function DisconnectedError(message) { + Error.call(this, message); + + if (Error.captureStackTrace) + Error.captureStackTrace(this, DisconnectedError); +} +util.inherits(DisconnectedError, Error); + +///--- API + +/** + * Constructs a new client. + * + * The options object is required, and must contain either a URL (string) or + * a socketPath (string); the socketPath is only if you want to talk to an LDAP + * server over a Unix Domain Socket. Additionally, you can pass in a log4js + * option that is the result of `require('log4js')`, presumably after you've + * configured it. + * + * @param {Object} options must have either url or socketPath. + * @throws {TypeError} on bad input. + */ +function Client(options) { + if (!options || typeof(options) !== 'object') + throw new TypeError('options (object) required'); + if (options.url && typeof(options.url) !== 'string') + throw new TypeError('options.url (string) required'); + if (options.socketPath && typeof(options.socketPath) !== 'string') + throw new TypeError('options.socketPath must be a string'); + if (options.log4js && typeof(options.log4js) !== 'object') + throw new TypeError('options.log4s must be an object'); + if (options.numConnections && typeof(options.numConnections) !== 'number') + throw new TypeError('options.numConnections must be a number'); + if (!xor(options.url, options.socketPath)) + throw new TypeError('options.url ^ options.socketPath required'); + + EventEmitter.call(this, options); + + var self = this; + this.secure = false; + if (options.url) { + this.url = url.parse(options.url); + this.secure = this.url.secure; + } + + this.log4js = options.log4js || logStub; + this.numConnections = Math.abs(options.numConnections) || 3; + this.connections = []; + this.currentConnection = 0; + this.connectOptions = options.socketPath ? options.socketPath : { + port: self.url.port, + host: self.url.hostname + }; + this.shutdown = false; + + this.__defineGetter__('log', function() { + if (!self._log) + self._log = self.log4js.getLogger('LDAPClient'); + + return self._log; + }); + + // Build the connection pool + function newConnection() { + var c; + if (self.secure) { + c = tls.createConnection(self.connectOptions); + } else { + c = net.createConnection(self.connectOptions); + } + assert.ok(c); + + c.parser = new Parser({ + log4js: self.log4js + }); + + // Wrap the events + c.ldap = { + id: options.socketPath || self.url.hostname, + connected: true, // lie, but node queues for us + messageID: 0, + messages: {} + }; + c.ldap.__defineGetter__('nextMessageID', function() { + if (++c.ldap.messageID >= MAX_MSGID) + c.ldap.messageID = 1; + return c.ldap.messageID; + }); + c.on('connect', function() { + c.ldap.connected = true; + c.ldap.id += ':' + (c.type !== 'unix' ? c.remotePort : c.fd); + self.emit('connect', c.ldap.id); + }); + c.on('end', function() { + self.log.trace('%s end', c.ldap.id); + c.ldap.connected = false; + if (!self.shutdown) + c.connect(); + }); + c.addListener('close', function(had_err) { + self.log.trace('%s close; had_err=%j', c.ldap.id, had_err); + c.ldap.connected = false; + if (!self.shutdown) + c.connect(); + }); + c.on('error', function(err) { + self.log.warn('%s unexpected connection error %s', c.ldap.id, err); + self.emit('error', err, c.ldap.id); + c.ldap.connected = false; + if (!self.shutdown) { + c.end(); + c.connect(); + } + }); + c.on('timeout', function() { + self.log.trace('%s timed out', c.ldap.id); + c.ldap.connected = false; + if (!self.shutdown) { + c.end(); + c.connect(); + } + }); + c.on('data', function(data) { + if (self.log.isTraceEnabled()) + self.log.trace('data on %s: %s', c.ldap.id, util.inspect(data)); + c.parser.write(data); + }); + + // The "router" + c.parser.on('message', function(message) { + message.connection = c; + + var callback = c.ldap.messages[message.messageID]; + if (!callback) { + self.log.error('%s: received unsolicited message: %j', + c.ldap.id, message.json); + return; + } + + return callback(message); + }); + return c; + } + + for (var i = 0; i < this.numConnections; i++) { + self.connections.push(newConnection()); + } +} +util.inherits(Client, EventEmitter); +module.exports = Client; + + +/** + * Performs a simple authentication against the server. + * + * @param {String} name the DN to bind as. + * @param {String} credentials the userPassword associated with name. + * @param {Control} controls (optional) either a Control or [Control]. + * @param {Function} callback of the form f(err, res). + * @throws {TypeError} on invalid input. + */ +Client.prototype.bind = function(name, credentials, controls, callback) { + if (typeof(name) !== 'string') + throw new TypeError('name (string) required'); + if (typeof(credentials) !== 'string') + throw new TypeError('credentials (string) required'); + if (typeof(controls) === 'function') { + callback = controls; + controls = []; + } else { + control = validateControls(controls); + } + if (typeof(callback) !== 'function') + throw new TypeError('callback (function) required'); + + var self = this; + + var req = new BindRequest({ + name: dn.parse(name), + authentication: 'Simple', + credentials: credentials, + controls: controls + }); + + var cbIssued = false; + var finished = 0; + function _callback(err, res) { + if (err) { + if (!cbIssued) { + cbIssued = true; + return callback(err); + } + } + + if (++finished >= self.connections.length && !cbIssued) { + cbIssued = true; + return callback(null, res); + } + } + + this.connections.forEach(function(c) { + return self._send(req, [errors.LDAP_SUCCESS], _callback, c); + }); +}; + + +/** + * Adds an entry to the LDAP server. + * + * @param {String} name the DN of the entry to add. + * @param {Array} attributes an array of Attributes to be added. + * @param {Control} controls (optional) either a Control or [Control]. + * @param {Function} callback of the form f(err, res). + * @throws {TypeError} on invalid input. + */ +Client.prototype.add = function(name, attributes, controls, callback) { + if (typeof(name) !== 'string') + throw new TypeError('name (string) required'); + if (!Array.isArray(attributes)) + throw new TypeError('attributes ([Attribute]) required'); + attributes.forEach(function(a) { + if (!Attribute.isAttribute(a)) + throw new TypeError('attributes ([Attribute]) required'); + }); + if (typeof(controls) === 'function') { + callback = controls; + controls = []; + } else { + control = validateControls(controls); + } + if (typeof(callback) !== 'function') + throw new TypeError('callback (function) required'); + + var req = new AddRequest({ + entry: dn.parse(name), + attributes: attributes, + controls: controls + }); + + return this._send(req, [errors.LDAP_SUCCESS], callback); +}; + + +/** + * Compares an attribute/value pair with an entry on the LDAP server. + * + * @param {String} name the DN of the entry to compare attributes with. + * @param {String} attribute name of an attribute to check. + * @param {String} value value of an attribute to check. + * @param {Control} controls (optional) either a Control or [Control]. + * @param {Function} callback of the form f(err, boolean, res). + * @throws {TypeError} on invalid input. + */ +Client.prototype.compare = function(name, + attribute, + value, + controls, + callback) { + if (typeof(name) !== 'string') + throw new TypeError('name (string) required'); + if (typeof(attribute) !== 'string') + throw new TypeError('attribute (string) required'); + if (typeof(value) !== 'string') + throw new TypeError('value (string) required'); + if (typeof(controls) === 'function') { + callback = controls; + controls = []; + } else { + control = validateControls(controls); + } + if (typeof(callback) !== 'function') + throw new TypeError('callback (function) required'); + + var req = new CompareRequest({ + entry: dn.parse(name), + attribute: attribute, + value: value, + controls: controls + }); + + function _callback(err, res) { + if (err) + return callback(err); + + return callback(null, (res.status === errors.LDAP_COMPARE_TRUE), res); + } + + return this._send(req, + [errors.LDAP_COMPARE_TRUE, errors.LDAP_COMPARE_FALSE], + _callback); +}; + + +/** + * Deletes an entry from the LDAP server. + * + * @param {String} name the DN of the entry to delete. + * @param {Control} controls (optional) either a Control or [Control]. + * @param {Function} callback of the form f(err, res). + * @throws {TypeError} on invalid input. + */ +Client.prototype.del = function(name, controls, callback) { + if (typeof(name) !== 'string') + throw new TypeError('name (string) required'); + if (typeof(controls) === 'function') { + callback = controls; + controls = []; + } else { + control = validateControls(controls); + } + if (typeof(callback) !== 'function') + throw new TypeError('callback (function) required'); + + var req = new DeleteRequest({ + entry: dn.parse(name), + controls: controls + }); + + return this._send(req, [errors.LDAP_SUCCESS], callback); +}; + + +/** + * Performs an extended operation on the LDAP server. + * + * Pretty much none of the LDAP extended operations return an OID + * (responseName), so I just don't bother giving it back in the callback. + * It's on the third param in `res` if you need it. + * + * @param {String} name the OID of the extended operation to perform. + * @param {String} value value to pass in for this operation. + * @param {Control} controls (optional) either a Control or [Control]. + * @param {Function} callback of the form f(err, value, res). + * @throws {TypeError} on invalid input. + */ +Client.prototype.exop = function(name, value, controls, callback) { + if (typeof(name) !== 'string') + throw new TypeError('name (string) required'); + if (typeof(value) === 'function') { + callback = value; + controls = []; + value = ''; + } + if (typeof(value) !== 'string') + throw new TypeError('value (string) required'); + if (typeof(controls) === 'function') { + callback = controls; + controls = []; + } else { + control = validateControls(controls); + } + if (typeof(callback) !== 'function') + throw new TypeError('callback (function) required'); + + var req = new ExtendedRequest({ + requestName: name, + requestValue: value, + controls: controls + }); + + function _callback(err, res) { + if (err) + return callback(err); + + return callback(null, res.responseValue || '', res); + } + + return this._send(req, [errors.LDAP_SUCCESS], _callback); +}; + + +/** + * Performs an LDAP modify against the server. + * + * @param {String} name the DN of the entry to modify. + * @param {Change} change update to perform (can be [Change]). + * @param {Control} controls (optional) either a Control or [Control]. + * @param {Function} callback of the form f(err, res). + * @throws {TypeError} on invalid input. + */ +Client.prototype.modify = function(name, change, controls, callback) { + if (typeof(name) !== 'string') + throw new TypeError('name (string) required'); + if (!Array.isArray(change) && !(change instanceof Change)) + throw new TypeError('change (Change) required'); + if (!Array.isArray(change)) { + var save = change; + change = []; + change.push(save); + } + change.forEach(function(c) { + if (!(c instanceof Change)) + throw new TypeError('change ([Change]) required'); + }); + if (typeof(controls) === 'function') { + callback = controls; + controls = []; + } else { + control = validateControls(controls); + } + if (typeof(callback) !== 'function') + throw new TypeError('callback (function) required'); + + var req = new ModifyRequest({ + object: dn.parse(name), + changes: change, + controls: controls + }); + + return this._send(req, [errors.LDAP_SUCCESS], callback); +}; + + +/** + * Performs an LDAP modifyDN against the server. + * + * This does not allow you to keep the old DN, as while the LDAP protocol + * has a facility for that, it's stupid. Just Search/Add. + * + * This will automatically deal with "new superior" logic. + * + * @param {String} name the DN of the entry to modify. + * @param {String} newName the new DN to move this entry to. + * @param {Control} controls (optional) either a Control or [Control]. + * @param {Function} callback of the form f(err, res). + * @throws {TypeError} on invalid input. + */ +Client.prototype.modifyDN = function(name, newName, controls, callback) { + if (typeof(name) !== 'string') + throw new TypeError('name (string) required'); + if (typeof(newName) !== 'string') + throw new TypeError('newName (string) required'); + if (typeof(controls) === 'function') { + callback = controls; + controls = []; + } else { + control = validateControls(controls); + } + if (typeof(callback) !== 'function') + throw new TypeError('callback (function) required'); + + var DN = dn.parse(name); + var newDN = dn.parse(newName); + + var req = new ModifyDNRequest({ + entry: DN, + deleteOldRdn: true, + controls: controls + }); + + if (newDN.length !== 1) { + req.newRdn = dn.parse(newDN.rdns.shift().toString()); + req.newSuperior = newDN; + } else { + req.newRdn = newDN; + } + + return this._send(req, [errors.LDAP_SUCCESS], callback); +}; + + +/** + * Performs an LDAP search against the server. + * + * Note that the defaults for options are a 'base' search, if that's what + * you want you can just pass in a string for options and it will be treated + * as the search filter. Also, you can either pass in programatic Filter + * objects or a filter string as the filter option. + * + * Note that this method is 'special' in that the callback 'res' param will + * have two important events on it, namely 'entry' and 'end' that you can hook + * to. The former will emit a SearchEntry object for each record that comes + * back, and the latter will emit a normal LDAPResult object. + * + * @param {String} base the DN in the tree to start searching at. + * @param {Object} options parameters: + * - {String} scope default of 'base'. + * - {String} filter default of '(objectclass=*)'. + * - {Array} attributes [string] to return. + * - {Boolean} attrsOnly whether to return values. + * @param {Control} controls (optional) either a Control or [Control]. + * @param {Function} callback of the form f(err, res). + * @throws {TypeError} on invalid input. + */ +Client.prototype.search = function(base, options, controls, callback) { + if (typeof(base) !== 'string') + throw new TypeError('base (string) required'); + if (Array.isArray(options) || (options instanceof Control)) { + controls = options; + options = {}; + } else if (typeof(options) === 'function') { + callback = options; + controls = []; + options = { + filter: new PresenceFilter({attribute: 'objectclass'}) + }; + } else if (typeof(options) === 'string') { + options = {filter: filters.parseString(options)}; + } else if (typeof(options) !== 'object') { + throw new TypeError('options (object) required'); + } + if (!(options.filter instanceof Filter)) + throw new TypeError('options.filter (Filter) required'); + if (typeof(controls) === 'function') { + callback = controls; + controls = []; + } else { + control = validateControls(controls); + } + if (typeof(callback) !== 'function') + throw new TypeError('callback (function) required'); + + var req = new SearchRequest({ + baseObject: dn.parse(base), + scope: options.scope || 'base', + filter: options.filter, + derefAliases: Protocol.NEVER_DEREF_ALIASES, + sizeLimit: options.sizeLimit || 0, + timeLimit: options.timeLimit || 10, + typesOnly: options.typesOnly || false, + attributes: options.attributes || [] + }); + + var res = new EventEmitter(); + this._send(req, [errors.LDAP_SUCCESS], res); + return callback(null, res); +}; + + +/** + * Unbinds this client from the LDAP server. + * + * Note that unbind does not have a response, so this callback is actually + * optional; either way, the client is disconnected. + * + * @param {Function} callback of the form f(err). + * @throws {TypeError} if you pass in callback as not a function. + */ +Client.prototype.unbind = function(callback) { + if (callback && typeof(callback) !== 'function') + throw new TypeError('callback must be a function'); + + var self = this; + + if (!callback) + callback = function defUnbindCb() { self.log.trace('disconnected'); }; + + this.shutdown = true; + var req = new UnbindRequest(); + var finished = 0; + var cbIssued = false; + function _callback(err, res) { + if (err) { + if (!cbIssued) { + cbIssued = true; + return callback(err); + } + } + + if (++finished >= self.connections.length && !cbIssued) { + cbIssued = true; + return callback(null); + } + } + + this.connections.forEach(function(c) { + return self._send(req, 'unbind', _callback, c); + }); +}; + + + +Client.prototype._send = function(message, expect, callback, conn) { + assert.ok(message); + assert.ok(expect); + assert.ok(callback); + + var self = this; + + // First select a connection + // Note bind and unbind are special in that they will pass in the + // connection since they iterate over the whole pool + if (!conn) { + function nextConn() { + if (++self.currentConnection >= self.connections.length) + self.currentConnection = 0; + + return self.connections[self.currentConnection]; + } + + var save = this.currentConnection; + while ((conn = nextConn()) && save !== this.currentConnection); + + if (!conn) { + self.emit('error', new DisconnectedError('No connections available')); + return; + } + } + assert.ok(conn); + + // Now set up the callback in the messages table + message.messageID = conn.ldap.nextMessageID; + conn.ldap.messages[message.messageID] = function(res) { + if (self.log.isDebugEnabled()) + self.log.debug('%s: response received: %j', conn.ldap.id, res.json); + + var err = null; + if (res instanceof LDAPResult) { + delete conn.ldap.messages[message.messageID]; + + if (expect.indexOf(res.status) === -1) { + err = errors.getError(res); + if (typeof(callback) === 'function') + return callback(err); + + return callback.emit('error', err); + } + + if (typeof(callback) === 'function') + return callback(null, res); + + callback.emit('end', res); + } else if (res instanceof SearchEntry) { + assert.ok(callback instanceof EventEmitter); + callback.emit('searchEntry', res); + } else { + delete conn.ldap.messages[message.messageID]; + + err = new errors.ProtocolError(res.type); + if (typeof(callback) === 'function') + return callback(err); + + callback.emit('error', err); + } + }; + + // Finally send some data + if (this.log.isDebugEnabled()) + this.log.debug('%s: sending request: %j', conn.ldap.id, message.json); + + // Note if this was an unbind, we just go ahead and end, since there + // will never be a response + return conn.write(message.toBer(), (expect === 'unbind' ? function() { + conn.on('end', function() { + self.emit('unbind'); + return callback(); + }); + conn.end(); + } : null)); +}; + diff --git a/lib/control.js b/lib/control.js new file mode 100644 index 0000000..dc6ce89 --- /dev/null +++ b/lib/control.js @@ -0,0 +1,74 @@ +// Copyright 2011 Mark Cavage, Inc. All rights reserved. + +var assert = require('assert'); +var util = require('util'); + +var asn1 = require('asn1'); + +var Protocol = require('./protocol'); + + + +///--- Globals + +var Ber = asn1.Ber; + + + +///--- API + +function Control(options) { + if (options) { + if (typeof(options) !== 'object') + throw new TypeError('options must be an object'); + if (options.type && typeof(options.type) !== 'string') + throw new TypeError('options.type must be a string'); + if (options.criticality !== undefined && + typeof(options.criticality) !== 'boolean') + throw new TypeError('options.criticality must be a boolean'); + if (options.value && typeof(options.value) !== 'string') + throw new TypeError('options.value must be a string'); + } else { + options = {}; + } + + this.type = options.type || ''; + this.criticality = options.criticality || false; + this.value = options.value || undefined; + + var self = this; + this.__defineGetter__('json', function() { + return { + controlType: self.type, + criticality: self.criticality, + controlValue: self.value + }; + }); +} +module.exports = Control; + + +Control.prototype.toString = function() { + return this.json; +}; + + +Control.prototype.parse = function(ber) { + assert.ok(ber); + + if (ber.readSequence() === null) + return false; + + var end = ber.offset + ber.length; + + if (ber.length) { + this.type = ber.readString(); + if (ber.offset < end) + this.criticality = ber.readBoolean(); + + if (ber.offset < end) + this.value = ber.readString(); + } + + return true; +}; diff --git a/lib/dn.js b/lib/dn.js new file mode 100644 index 0000000..6c87926 --- /dev/null +++ b/lib/dn.js @@ -0,0 +1,244 @@ +// Copyright 2011 Mark Cavage, Inc. All rights reserved. + + + +function invalidDN(name) { + var e = new Error(); + e.name = 'InvalidDistinguishedNameError'; + e.message = name; + return e; +} + + +function isAlphaNumeric(c) { + var re = /[A-Za-z0-9]/; + return re.test(c); +} + + +function isWhitespace(c) { + var re = /\s/; + return re.test(c); +} + +function RDN() {} +RDN.prototype.toString = function() { + var self = this; + + var str = ''; + Object.keys(this).forEach(function(k) { + if (str.length) + str += '+'; + str += k + '=' + self[k]; + }); + + return str; +}; + +function parse(name) { + if (typeof(name) !== 'string') + throw new TypeError('name (string) required'); + + var cur = 0; + var len = name.length; + + function parseRdn() { + var rdn = new RDN(); + while (cur < len) { + trim(); + var attr = parseAttrType(); + trim(); + if (cur >= len || name[cur++] !== '=') + throw invalidDN(name); + + trim(); + var value = parseAttrValue(); + trim(); + rdn[attr] = value; + if (cur >= len || name[cur] !== '+') + break; + ++cur; + } + + return rdn; + } + + + function trim() { + while ((cur < len) && isWhitespace(name[cur])) + ++cur; + } + + function parseAttrType() { + var beg = cur; + while (cur < len) { + var 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() { + if (cur < len && name[cur] == '#') { + return parseBinaryAttrValue(); + } else if (cur < len && name[cur] == '"') { + return parseQuotedAttrValue(); + } else { + return parseStringAttrValue(); + } + } + + function parseBinaryAttrValue() { + var beg = cur++; + while (cur < len && isAlphaNumeric(name[cur])) + ++cur; + + return name.slice(beg, cur); + } + + function parseQuotedAttrValue() { + var beg = cur++; + + while ((cur < len) && name[cur] != '"') { + if (name[cur] === '\\') + ++cur; // consume backslash, then what follows + + ++cur; + } + if (cur++ >= len) // no closing quote + throw invalidDN(name); + + return name.slice(beg, cur); + } + + function parseStringAttrValue() { + var beg = cur; + var esc = -1; + + while ((cur < len) && !atTerminator()) { + if (name[cur] === '\\') { + ++cur; // consume backslash, then what follows + esc = cur; + } + ++cur; + } + if (cur > len) // backslash followed by nothing + throw invalidDN(name); + + // Trim off (unescaped) trailing whitespace. + var end; + for (end = cur; end > beg; end--) { + if (!isWhitespace(name[end - 1]) || (esc === (end - 1))) + break; + } + return name.slice(beg, end); + } + + function atTerminator() { + return (cur < len && + (name[cur] === ',' || + name[cur] === ';' || + name[cur] === '+')); + } + + var rdns = []; + + rdns.push(parseRdn()); + while (cur < len) { + if (name[cur] === ',' || name[cur] === ';') { + ++cur; + rdns.push(parseRdn()); + } else { + throw invalidDN(name); + } + } + + return new DN(rdns); +} + + + +///--- API + + +function DN(rdns) { + if (!Array.isArray(rdns)) + throw new TypeError('rdns ([object]) required'); + rdns.forEach(function(rdn) { + if (typeof(rdn) !== 'object') + throw new TypeError('rdns ([object]) required'); + }); + + this.rdns = rdns.slice(); + + this.__defineGetter__('length', function() { + return this.rdns.length; + }); +} + + +DN.prototype.toString = function() { + var _dn = []; + this.rdns.forEach(function(rdn) { + _dn.push(rdn.toString()); + }); + return _dn.join(', '); +}; + + +DN.prototype.childOf = function(dn) { + if (!(dn instanceof DN)) + dn = parse(dn); + + if (this.rdns.length < dn.rdns.length) + return false; + + var diff = this.rdns.length - dn.rdns.length; + for (var i = dn.rdns.length - 1; i >= 0; i--) { + var rdn = dn.rdns[i]; + for (var k in rdn) { + if (rdn.hasOwnProperty(k)) { + var ourRdn = this.rdns[i + diff]; + if (ourRdn[k] !== rdn[k]) + return false; + } + } + } + + return true; +}; + + +DN.prototype.parentOf = function(dn) { + if (!(dn instanceof DN)) + dn = parse(dn); + + var parent = DN.prototype.childOf.call(dn, this); + + return parent; +}; + + +module.exports = { + + parse: parse, + + DN: DN, + + RDN: RDN + +}; diff --git a/lib/errors/index.js b/lib/errors/index.js new file mode 100644 index 0000000..d3f1584 --- /dev/null +++ b/lib/errors/index.js @@ -0,0 +1,140 @@ +// Copyright 2011 Mark Cavage, Inc. All rights reserved. + +var util = require('util'); + +var LDAPResult = require('../messages').LDAPResult; + + +///--- Globals + +var CODES = { + LDAP_SUCCESS: 0, + LDAP_OPERATIONS_ERROR: 1, + LDAP_PROTOCOL_ERROR: 2, + LDAP_TIME_LIMIT_EXCEEDED: 3, + LDAP_SIZE_LIMIT_EXCEEDED: 4, + LDAP_COMPARE_FALSE: 5, + LDAP_COMPARE_TRUE: 6, + LDAP_AUTH_METHOD_NOT_SUPPORTED: 7, + LDAP_STRONG_AUTH_REQUIRED: 8, + LDAP_REFERRAL: 10, + LDAP_ADMIN_LIMIT_EXCEEDED: 11, + LDAP_UNAVAILABLE_CRITICAL_EXTENSION: 12, + LDAP_CONFIDENTIALITY_REQUIRED: 13, + LDAP_SASL_BIND_IN_PROGRESS: 14, + LDAP_NO_SUCH_ATTRIBUTE: 16, + LDAP_UNDEFINED_ATTRIBUTE_TYPE: 17, + LDAP_INAPPROPRIATE_MATCHING: 18, + LDAP_CONSTRAINT_VIOLATION: 19, + LDAP_ATTRIBUTE_OR_VALUE_EXISTS: 20, + LDAP_INVALID_ATTRIUBTE_SYNTAX: 21, + LDAP_NO_SUCH_OBJECT: 32, + LDAP_ALIAS_PROBLEM: 33, + LDAP_INVALID_DN_SYNTAX: 34, + LDAP_ALIAS_DEREF_PROBLEM: 36, + LDAP_INAPPROPRIATE_AUTHENTICATION: 48, + LDAP_INVALID_CREDENTIALS: 49, + LDAP_INSUFFICIENT_ACCESS_RIGHTS: 50, + LDAP_BUSY: 51, + LDAP_UNAVAILABLE: 52, + LDAP_UNWILLING_TO_PERFORM: 53, + LDAP_LOOP_DETECT: 54, + LDAP_NAMING_VIOLATION: 64, + LDAP_OBJECTCLASS_VIOLATION: 65, + LDAP_NOT_ALLOWED_ON_NON_LEAF: 66, + LDAP_NOT_ALLOWED_ON_RDN: 67, + LDAP_ENTRY_ALREADY_EXISTS: 68, + LDAP_OBJECTCLASS_MODS_PROHIBITED: 69, + LDAP_AFFECTS_MULTIPLE_DSAS: 71, + LDAP_OTHER: 80 +}; + +var ERRORS = []; + + + +///--- Error Base class + +function LDAPError(errorName, errorCode, msg, dn, caller) { + if (Error.captureStackTrace) + Error.captureStackTrace(this, caller || LDAPError); + + this.__defineGetter__('dn', function() { + return (dn ? (dn.toString() || '') : ''); + }); + this.__defineGetter__('code', function() { + return errorCode; + }); + this.__defineGetter__('name', function() { + return errorName; + }); + this.__defineGetter__('message', function() { + return msg || errorName; + }); +} +util.inherits(LDAPError, Error); + + + +///--- Exported API +// Some whacky games here to make sure all the codes are exported + +module.exports = {}; + +Object.keys(CODES).forEach(function(code) { + module.exports[code] = CODES[code]; + if (code === 'LDAP_SUCCESS') + return; + + var err = ''; + var msg = ''; + var pieces = code.split('_').slice(1); + for (var i = 0; i < pieces.length; i++) { + var lc = pieces[i].toLowerCase(); + var key = lc.charAt(0).toUpperCase() + lc.slice(1); + err += key; + msg += key + ((i + 1) < pieces.length ? ' ' : ''); + } + + if (!/\w+Error$/.test(err)) + err += 'Error'; + + // At this point LDAP_OPERATIONS_ERROR is now OperationsError in $err + // and 'Operations Error' in $msg + module.exports[err] = function(message, dn, caller) { + LDAPError.call(this, + err, + CODES[code], + message || msg, + dn || null, + caller || module.exports[err]); + } + module.exports[err].constructor = module.exports[err]; + util.inherits(module.exports[err], LDAPError); + + ERRORS[CODES[code]] = { + err: err, + message: msg + }; +}); + + +module.exports.getError = function(res) { + if (!(res instanceof LDAPResult)) + throw new TypeError('res (LDAPResult) required'); + + var errObj = ERRORS[res.status]; + var E = module.exports[errObj.err]; + return new E(res.errorMessage || errObj.message, + res.matchedDN || null, + module.exports.getError); +}; + + +module.exports.getMessage = function(code) { + if (typeof(code) !== 'number') + throw new TypeError('code (number) required'); + + var errObj = ERRORS[res.status]; + return (errObj && errObj.message ? errObj.message : ''); +}; diff --git a/lib/filters/and_filter.js b/lib/filters/and_filter.js new file mode 100644 index 0000000..2d3a5c0 --- /dev/null +++ b/lib/filters/and_filter.js @@ -0,0 +1,82 @@ +// Copyright 2011 Mark Cavage, Inc. All rights reserved. + +var assert = require('assert'); +var util = require('util'); + +var Filter = require('./filter'); + +var Protocol = require('../protocol'); + + + +///--- 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 || [] + }; + }); +} +util.inherits(AndFilter, Filter); +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); + + this.filters.forEach(function(f) { + ber = f.toBer(ber); + }); + + return ber; +}; diff --git a/lib/filters/approx_filter.js b/lib/filters/approx_filter.js new file mode 100644 index 0000000..0074adb --- /dev/null +++ b/lib/filters/approx_filter.js @@ -0,0 +1,75 @@ +// Copyright 2011 Mark Cavage, Inc. All rights reserved. + +var assert = require('assert'); +var util = require('util'); + +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 + }; + }); +} +util.inherits(ApproximateFilter, Filter); +module.exports = ApproximateFilter; + + +ApproximateFilter.prototype.toString = function() { + return '(' + this.attribute + '~=' + this.value + ')'; +}; + + +ApproximateFilter.prototype.matches = function(target) { + if (typeof(target) !== 'object') + throw new TypeError('target (object) required'); + + var matches = false; + if (target.hasOwnProperty(this.attribute)) + matches = (this.value === target[this.attribute]); + + return matches; +}; + + +ApproximateFilter.prototype.parse = function(ber) { + assert.ok(ber); + + this.attribute = ber.readString(); + this.value = ber.readString(); + + return true; +}; + + +ApproximateFilter.prototype._toBer = function(ber) { + assert.ok(ber); + + ber.writeString(this.attribute); + ber.writeString(this.value); + + return ber; +}; diff --git a/lib/filters/equality_filter.js b/lib/filters/equality_filter.js new file mode 100644 index 0000000..9899dd5 --- /dev/null +++ b/lib/filters/equality_filter.js @@ -0,0 +1,75 @@ +// Copyright 2011 Mark Cavage, Inc. All rights reserved. + +var assert = require('assert'); +var util = require('util'); + +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 || 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_EQUALITY; + Filter.call(this, options); + + var self = this; + this.__defineGetter__('json', function() { + return { + type: 'EqualityMatch', + attribute: self.attribute || undefined, + value: self.value || undefined + }; + }); +} +util.inherits(EqualityFilter, Filter); +module.exports = EqualityFilter; + + +EqualityFilter.prototype.toString = function() { + return '(' + this.attribute + '=' + this.value + ')'; +}; + + +EqualityFilter.prototype.matches = function(target) { + if (typeof(target) !== 'object') + throw new TypeError('target (object) required'); + + var matches = false; + if (target.hasOwnProperty(this.attribute)) + matches = (this.value === target[this.attribute]); + + return matches; +}; + + +EqualityFilter.prototype.parse = function(ber) { + assert.ok(ber); + + this.attribute = ber.readString(); + this.value = ber.readString(); + + return true; +}; + + +EqualityFilter.prototype._toBer = function(ber) { + assert.ok(ber); + + ber.writeString(this.attribute); + ber.writeString(this.value); + + return ber; +}; diff --git a/lib/filters/filter.js b/lib/filters/filter.js new file mode 100644 index 0000000..8556835 --- /dev/null +++ b/lib/filters/filter.js @@ -0,0 +1,38 @@ +// Copyright 2011 Mark Cavage, Inc. All rights reserved. + +var assert = require('assert'); + +var asn1 = require('asn1'); + + +///--- Globals + +var BerWriter = asn1.BerWriter; + +///--- 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() { + return '0x' + self._type.toString(16); + }); +} +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; +}; diff --git a/lib/filters/ge_filter.js b/lib/filters/ge_filter.js new file mode 100644 index 0000000..cbb5a5a --- /dev/null +++ b/lib/filters/ge_filter.js @@ -0,0 +1,76 @@ +// Copyright 2011 Mark Cavage, Inc. All rights reserved. + +var assert = require('assert'); +var util = require('util'); + +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 + }; + }); +} +util.inherits(GreaterThanEqualsFilter, Filter); +module.exports = GreaterThanEqualsFilter; + + +GreaterThanEqualsFilter.prototype.toString = function() { + return '(' + this.attribute + '>=' + this.value + ')'; +}; + + +GreaterThanEqualsFilter.prototype.matches = function(target) { + if (typeof(target) !== 'object') + throw new TypeError('target (object) required'); + + var matches = false; + if (target.hasOwnProperty(this.attribute)) + matches = (target[this.attribute] >= this.value); + + return matches; +}; + + +GreaterThanEqualsFilter.prototype.parse = function(ber) { + assert.ok(ber); + + this.attribute = ber.readString(); + this.value = ber.readString(); + + return true; +}; + + +GreaterThanEqualsFilter.prototype._toBer = function(ber) { + assert.ok(ber); + + ber.writeString(this.attribute); + ber.writeString(this.value); + + return ber; +}; diff --git a/lib/filters/index.js b/lib/filters/index.js new file mode 100644 index 0000000..a491a3c --- /dev/null +++ b/lib/filters/index.js @@ -0,0 +1,320 @@ +// Copyright 2011 Mark Cavage, Inc. All rights reserved. + +var assert = require('assert'); + +var asn1 = require('asn1'); + +var Protocol = require('../protocol'); + +var Filter = require('./filter'); +var AndFilter = require('./and_filter'); +var ApproximateFilter = require('./approx_filter'); +var EqualityFilter = require('./equality_filter'); +var GreaterThanEqualsFilter = require('./ge_filter'); +var LessThanEqualsFilter = require('./le_filter'); +var NotFilter = require('./not_filter'); +var OrFilter = require('./or_filter'); +var PresenceFilter = require('./presence_filter'); +var SubstringFilter = require('./substr_filter'); + + + +///--- Globals + +var BerReader = asn1.BerReader; + + + +///--- Internal Parsers + + +/* + * This is a pretty naive approach to parsing, but it's relatively short amount + * of code. Basically, we just build a stack as we go. + */ +function _filterStringToStack(str) { + assert.ok(str); + + var tmp = ''; + var esc = false; + var stack = []; + var depth = -1; + var open = false; + for (var i = 0; i < str.length; i++) { + var c = str[i]; + + if (esc) { + esc = false; + tmp += c; + continue; + } + + switch (c) { + case '(': + open = true; + tmp = ''; + stack[++depth] = ''; + break; + case ')': + if (open) { + stack[depth].value = tmp; + tmp = ''; + } + open = false; + break; + case '&': + case '|': + case '!': + stack[depth] = c; + break; + case '=': + stack[depth] = { attribute: tmp, op: c }; + tmp = ''; + break; + case '>': + case '<': + case '~': + if (!(str[++i] === '=')) + throw new Error('Invalid filter: ' + tmp + c + str[i]); + + stack[depth] = {attribute: tmp, op: c}; + tmp = ''; + break; + case '\\': + esc = true; + default: + tmp += c; + break; + } + } + + if (open) + throw new Error('Invalid filter: ' + str); + + return stack; +} + + +function _parseString(str) { + assert.ok(str); + + var stack = _filterStringToStack(str); + + if (!stack || !stack.length) + throw new Error('Invalid filter: ' + str); + + debugger; + var f; + var filters = []; + for (var i = stack.length - 1; i >= 0; i--) { + if (stack[i] === '&') { + filters.unshift(new AndFilter({ + filters: filters + })); + filters.length = 1; + } else if (stack[i] === '|') { + filters.unshift(new OrFilter({ + filters: filters + })); + filters.length = 1; + } else if (stack[i] === '!') { + filters.push(new NotFilter({ + filter: filters.pop() + })); + } else { + switch (stack[i].op) { + case '=': // could be presence, equality or substr + if (stack[i].value === '*') { + filters.push(new PresenceFilter(stack[i])); + } else { + var vals = ['']; + var ndx = 0; + var esc = false; + for (var j = 0; j < stack[i].value.length; j++) { + var c = stack[i].value[j]; + if (c === '\\') { + if (esc) { + esc = true; + } else { + vals[ndx] += c; + esc = false; + } + } else if (c === '*') { + if (esc) { + vals[ndx] = c; + } else { + vals[++ndx] = ''; + } + } else { + vals[ndx] += c; + } + } + if (vals.length === 1) { + filters.push(new EqualityFilter(stack[i])); + } else { + filters.push(new SubstringFilter({ + attribute: stack[i].attribute, + initial: vals.shift(), + 'final': vals.pop(), + any: vals + })); + } + } + break; + case '~': + filters.push(new ApproximateFilter(stack[i])); + break; + case '>': + filters.push(new GreaterThanEqualsFilter(stack[i])); + break; + case '<': + filters.push(new LessThanEqualsFilter(stack[i])); + break; + default: + throw new Error('Invalid filter (op=' + stack[i].op + '): ' + str); + } + } + } + + if (filters.length !== 1) + throw new Error('Invalid filter: ' + str); + + return filters.pop(); +} + + +/* + * A filter looks like this coming in: + * Filter ::= CHOICE { + * and [0] SET OF Filter, + * or [1] SET OF Filter, + * not [2] Filter, + * equalityMatch [3] AttributeValueAssertion, + * substrings [4] SubstringFilter, + * greaterOrEqual [5] AttributeValueAssertion, + * lessOrEqual [6] AttributeValueAssertion, + * present [7] AttributeType, + * approxMatch [8] AttributeValueAssertion, + * extensibleMatch [9] MatchingRuleAssertion --v3 only + * } + * + * SubstringFilter ::= SEQUENCE { + * type AttributeType, + * SEQUENCE OF CHOICE { + * initial [0] IA5String, + * any [1] IA5String, + * final [2] IA5String + * } + * } + * + * The extensibleMatch was added in LDAPv3: + * + * MatchingRuleAssertion ::= SEQUENCE { + * matchingRule [1] MatchingRuleID OPTIONAL, + * type [2] AttributeDescription OPTIONAL, + * matchValue [3] AssertionValue, + * dnAttributes [4] BOOLEAN DEFAULT FALSE + * } + */ +function _parse(ber) { + assert.ok(ber); + + function parseSet(f) { + var end = ber.offset + ber.length; + while (ber.offset < end) + f.addFilter(_parse(ber)); + } + + var f; + + var type = ber.readSequence(); + switch (type) { + + case Protocol.FILTER_AND: + f = new AndFilter(); + parseSet(f); + break; + + case Protocol.FILTER_APPROX: + f = new ApproximateFilter(); + f.parse(ber); + break; + + case Protocol.FILTER_EQUALITY: + f = new EqualityFilter(); + f.parse(ber); + return f; + + case Protocol.FILTER_GE: + f = new GreaterThanEqualsFilter(); + f.parse(ber); + return f; + + case Protocol.FILTER_LE: + f = new LessThanEqualsFilter(); + f.parse(ber); + return f; + + case Protocol.FILTER_NOT: + var _f = _parse(ber); + f = new NotFilter({ + filter: _f + }); + break; + + case Protocol.FILTER_OR: + f = new OrFilter(); + parseSet(f); + break; + + case Protocol.FILTER_PRESENT: + f = new PresenceFilter(); + f.parse(ber); + break; + + case Protocol.FILTER_SUBSTRINGS: + f = new SubstringFilter(); + f.parse(ber); + break; + + default: + throw new Error('Invalid search filter type: 0x' + type.toString(16)); + } + + + assert.ok(f); + return f; +} + + + +///--- API + +module.exports = { + + parse: function(ber) { + if (!ber || !(ber instanceof BerReader)) + throw new TypeError('ber (BerReader) required'); + + return _parse(ber); + }, + + parseString: function(filter) { + if (!filter || typeof(filter) !== 'string') + throw new TypeError('filter (string) required'); + + return _parseString(filter); + }, + + AndFilter: AndFilter, + ApproximateFilter: ApproximateFilter, + EqualityFilter: EqualityFilter, + GreaterThanEqualsFilter: GreaterThanEqualsFilter, + LessThanEqualsFilter: LessThanEqualsFilter, + NotFilter: NotFilter, + OrFilter: OrFilter, + PresenceFilter: PresenceFilter, + SubstringFilter: SubstringFilter, + Filter: Filter +}; + diff --git a/lib/filters/le_filter.js b/lib/filters/le_filter.js new file mode 100644 index 0000000..86d1fa1 --- /dev/null +++ b/lib/filters/le_filter.js @@ -0,0 +1,76 @@ +// Copyright 2011 Mark Cavage, Inc. All rights reserved. + +var assert = require('assert'); +var util = require('util'); + +var Filter = require('./filter'); + +var Protocol = require('../protocol'); + + + +///--- API + +function LessThanEqualsFilter(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_LE; + Filter.call(this, options); + + var self = this; + this.__defineGetter__('json', function() { + return { + type: 'LessThanEqualsMatch', + attribute: self.attribute || undefined, + value: self.value || undefined + }; + }); +} +util.inherits(LessThanEqualsFilter, Filter); +module.exports = LessThanEqualsFilter; + + +LessThanEqualsFilter.prototype.toString = function() { + return '(' + this.attribute + '<=' + this.value + ')'; +}; + + +LessThanEqualsFilter.prototype.matches = function(target) { + if (typeof(target) !== 'object') + throw new TypeError('target (object) required'); + + var matches = false; + if (target.hasOwnProperty(this.attribute)) + matches = (target[this.attribute] <= this.value); + + return matches; +}; + + +LessThanEqualsFilter.prototype.parse = function(ber) { + assert.ok(ber); + + this.attribute = ber.readString(); + this.value = ber.readString(); + + return true; +}; + + +LessThanEqualsFilter.prototype._toBer = function(ber) { + assert.ok(ber); + + ber.writeString(this.attribute); + ber.writeString(this.value); + + return ber; +}; diff --git a/lib/filters/not_filter.js b/lib/filters/not_filter.js new file mode 100644 index 0000000..e973412 --- /dev/null +++ b/lib/filters/not_filter.js @@ -0,0 +1,51 @@ +// Copyright 2011 Mark Cavage, Inc. All rights reserved. + +var assert = require('assert'); +var util = require('util'); + +var Filter = require('./filter'); + +var Protocol = require('../protocol'); + + + +///--- API + +function NotFilter(options) { + if (typeof(options) !== 'object') + throw new TypeError('options (object) required'); + if (!options.filter || !(options.filter instanceof Filter)) + throw new TypeError('options.filter (Filter) required'); + + options.type = Protocol.FILTER_NOT; + Filter.call(this, options); + + this.filter = options.filter; + + var self = this; + this.__defineGetter__('json', function() { + return { + type: 'Not', + filter: self.filter + }; + }); +} +util.inherits(NotFilter, Filter); +module.exports = NotFilter; + + +NotFilter.prototype.toString = function() { + return '(!' + this.filter.toString() + ')'; +}; + + +NotFilter.prototype.matches = function(target) { + return !this.filter.matches(target); +}; + + +NotFilter.prototype._toBer = function(ber) { + assert.ok(ber); + + return this.filter.toBer(ber); +}; diff --git a/lib/filters/or_filter.js b/lib/filters/or_filter.js new file mode 100644 index 0000000..b2b28ed --- /dev/null +++ b/lib/filters/or_filter.js @@ -0,0 +1,80 @@ +// Copyright 2011 Mark Cavage, Inc. All rights reserved. + +var assert = require('assert'); +var util = require('util'); + +var Filter = require('./filter'); + +var Protocol = require('../protocol'); + + + +///--- API + +function OrFilter(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_OR; + Filter.call(this, options); + + if (!this.filters) + this.filters = []; + + var self = this; + this.__defineGetter__('json', function() { + return { + type: 'Or', + filters: self.filters || [] + }; + }); +} +util.inherits(OrFilter, Filter); +module.exports = OrFilter; + + +OrFilter.prototype.toString = function() { + var str = '(|'; + this.filters.forEach(function(f) { + str += f.toString(); + }); + str += ')'; + + return str; +}; + + +OrFilter.prototype.matches = function(target) { + if (typeof(target) !== 'object') + throw new TypeError('target (object) required'); + + for (var i = 0; i < this.filters.length; i++) + if (this.filters[i].matches(target)) + return true; + + return false; +}; + + +OrFilter.prototype.addFilter = function(filter) { + if (!filter || typeof(filter) !== 'object') + throw new TypeError('filter (object) required'); + + this.filters.push(filter); +}; + + +OrFilter.prototype._toBer = function(ber) { + assert.ok(ber); + + this.filters.forEach(function(f) { + ber = f.toBer(ber); + }); + + return ber; +}; diff --git a/lib/filters/presence_filter.js b/lib/filters/presence_filter.js new file mode 100644 index 0000000..465805b --- /dev/null +++ b/lib/filters/presence_filter.js @@ -0,0 +1,67 @@ +// Copyright 2011 Mark Cavage, Inc. All rights reserved. + +var assert = require('assert'); +var util = require('util'); + +var Filter = require('./filter'); + +var Protocol = require('../protocol'); + + +///--- API + +function PresenceFilter(options) { + if (typeof(options) === 'object') { + if (!options.attribute || typeof(options.attribute) !== 'string') + throw new TypeError('options.attribute (string) required'); + this.attribute = options.attribute; + } else { + options = {}; + } + options.type = Protocol.FILTER_PRESENT; + Filter.call(this, options); + + var self = this; + this.__defineGetter__('json', function() { + return { + type: 'PresenceMatch', + attribute: self.attribute || undefined + }; + }); +} +util.inherits(PresenceFilter, Filter); +module.exports = PresenceFilter; + + +PresenceFilter.prototype.toString = function() { + return '(' + this.attribute + '=*)'; +}; + + +PresenceFilter.prototype.matches = function(target) { + if (typeof(target) !== 'object') + throw new TypeError('target (object) required'); + + var matches = false; + if (target.hasOwnProperty(this.attribute)) + matches = true; + + return matches; +}; + + +PresenceFilter.prototype.parse = function(ber) { + assert.ok(ber); + + this.attribute = ber.readString(); + return true; +}; + + +PresenceFilter.prototype._toBer = function(ber) { + assert.ok(ber); + + ber.writeString(this.attribute); + + return ber; +}; diff --git a/lib/filters/substr_filter.js b/lib/filters/substr_filter.js new file mode 100644 index 0000000..0ec4499 --- /dev/null +++ b/lib/filters/substr_filter.js @@ -0,0 +1,133 @@ +// Copyright 2011 Mark Cavage, Inc. All rights reserved. + +var assert = require('assert'); +var util = require('util'); + +var Filter = require('./filter'); + +var Protocol = require('../protocol'); + + + +///--- API + +function SubstringFilter(options) { + if (typeof(options) === 'object') { + if (!options.attribute || typeof(options.attribute) !== 'string') + throw new TypeError('options.attribute (string) required'); + this.attribute = options.attribute; + this.initial = options.initial; + this.any = options.any ? options.any.slice(0) : []; + this['final'] = options['final']; + } else { + options = {}; + } + + if (!this.any) + this.any = []; + + options.type = Protocol.FILTER_SUBSTRINGS; + Filter.call(this, options); + + var self = this; + this.__defineGetter__('json', function() { + return { + type: 'SubstringMatch', + initial: self.initial || undefined, + any: self.any || undefined, + 'final': self['final'] || undefined + }; + }); +} +util.inherits(SubstringFilter, Filter); +module.exports = SubstringFilter; + + +SubstringFilter.prototype.toString = function() { + var str = '(' + this.attribute + '='; + if (this.initial) + str += this.initial + '*'; + this.any.forEach(function(s) { + str += s + '*'; + }); + if (this['final']) + str += this['final']; + str += ')'; + + return str; +}; + + +SubstringFilter.prototype.matches = function(target) { + if (typeof(target) !== 'object') + throw new TypeError('target (object) required'); + + if (target.hasOwnProperty(this.attribute)) { + var re = ''; + if (this.initial) + re += '^' + this.initial + '.*'; + + this.any.forEach(function(s) { + re += s + '.*'; + }); + + if (this['final']) + re += this['final'] + '$'; + + var matcher = new RegExp(re); + return matcher.test(target[this.attribute]); + } + + return true; +}; + + +SubstringFilter.prototype.parse = function(ber) { + assert.ok(ber); + + this.attribute = ber.readString(); + ber.readSequence(); + var end = ber.offset + ber.length; + + while (ber.offset < end) { + var tag = ber.peek(); + switch (tag) { + case 0x80: // Initial + this.initial = ber.readString(tag); + break; + case 0x81: // Any + this.any.push(ber.readString(tag)); + break; + case 0x82: // Final + this['final'] = ber.readString(tag); + break; + default: + throw new Error('Invalid substrings filter type: 0x' + tag.toString(16)); + } + } + + return true; +}; + + +SubstringFilter.prototype._toBer = function(ber) { + assert.ok(ber); + + ber.writeString(this.attribute); + ber.startSequence(); + + if (this.initial) + ber.writeString(this.initial, 0x80); + + if (this.any && this.any.length) + this.any.forEach(function(s) { + ber.writeString(s, 0x81); + }); + + if (this['final']) + ber.writeString(this['final'], 0x82); + + ber.endSequence(); + + return ber; +}; diff --git a/lib/index.js b/lib/index.js new file mode 100644 index 0000000..55af1db --- /dev/null +++ b/lib/index.js @@ -0,0 +1,93 @@ +// Copyright 2011 Mark Cavage, Inc. All rights reserved. + +var Client = require('./client'); +var dn = require('./dn'); +var errors = require('./errors'); +var filters = require('./filters'); +var messages = require('./messages'); +var server = require('./server'); +var logStub = require('./log_stub'); +var url = require('./url'); + +var Attribute = require('./attribute'); +var Change = require('./change'); +var Control = require('./control'); +var Protocol = require('./protocol'); + + +/// Hack a few things we need (i.e., "monkey patch" the prototype) + +if (!String.prototype.startsWith) { + String.prototype.startsWith = function(str) { + var re = new RegExp('^' + str); + return re.test(this); + }; +} + + +if (!String.prototype.endsWith) { + String.prototype.endsWith = function(str) { + var re = new RegExp(str + '$'); + return re.test(this); + }; +} + + + +///--- API + +module.exports = { + + Client: Client, + createClient: function(options) { + if (typeof(options) !== 'object') + throw new TypeError('options (object) required'); + + return new Client(options); + }, + + createServer: server.createServer, + + dn: dn, + DN: dn.DN, + DN: dn.RDN, + parseDN: dn.parse, + + filters: filters, + parseFilter: filters.parseString, + + Attribute: Attribute, + Change: Change, + Control: Control, + + log4js: logStub, + url: url +}; + + +///--- Export all the childrenz + +var k; + +for (k in Protocol) { + if (Protocol.hasOwnProperty(k)) + module.exports[k] = Protocol[k]; +} + +for (k in messages) { + if (messages.hasOwnProperty(k)) + module.exports[k] = messages[k]; +} + +for (k in filters) { + if (filters.hasOwnProperty(k)) { + if (k !== 'parse' && k !== 'parseString') + module.exports[k] = filters[k]; + } +} + +for (k in errors) { + if (errors.hasOwnProperty(k)) { + module.exports[k] = errors[k]; + } +} diff --git a/lib/log_stub.js b/lib/log_stub.js new file mode 100644 index 0000000..8d1d00b --- /dev/null +++ b/lib/log_stub.js @@ -0,0 +1,146 @@ +// Copyright 2011 Mark Cavage, Inc. All rights reserved. + + + +///--- Globals + +var FMT_STR = '%d-%s-%s %s:%s:%sZ %s - %s: '; + +var _i = 0; +var LEVELS = { + Trace: _i++, + Debug: _i++, + Info: _i++, + Warn: _i++, + Error: _i++, + Fatal: _i++ +}; + +var level = 'Info'; + + + +// --- Helpers + +function pad(val) { + if (parseInt(val, 10) < 10) { + val = '0' + val; + } + return val; +} + + +function format(level, name, args) { + var d = new Date(); + var fmtStr = args.shift(); + var fmtArgs = [ + d.getUTCFullYear(), + pad(d.getUTCMonth()), + pad(d.getUTCDate()), + pad(d.getUTCHours()), + pad(d.getUTCMinutes()), + pad(d.getUTCSeconds()), + level, + name + ]; + + args = fmtArgs.concat(args); + + var output = (FMT_STR + fmtStr).replace(/%[sdj]/g, function(match) { + switch (match) { + case '%s': return new String(args.shift()); + case '%d': return new Number(args.shift()); + case '%j': return JSON.stringify(args.shift()); + default: + return match; + } + }); + + return output; +} + + + +///--- API + +function Log(name) { + this.name = name; +} + +Log.prototype._write = function(level, args) { + var data = format(level, this.name, args); + console.error(data); +}; + +Log.prototype.isTraceEnabled = function() { + return (LEVELS.Trace >= LEVELS[level]); +}; + +Log.prototype.trace = function() { + if (this.isTraceEnabled()) + this._write('TRACE', Array.prototype.slice.call(arguments)); +}; + +Log.prototype.isDebugEnabled = function() { + return (LEVELS.Debug >= LEVELS[level]); +}; + +Log.prototype.debug = function() { + if (this.isDebugEnabled()) + this._write('DEBUG', Array.prototype.slice.call(arguments)); +}; + +Log.prototype.isInfoEnabled = function() { + return (LEVELS.Info >= LEVELS[level]); +}; + +Log.prototype.info = function() { + if (this.isInfoEnabled()) + this._write('INFO', Array.prototype.slice.call(arguments)); +}; + +Log.prototype.isWarnEnabled = function() { + return (LEVELS.Warn >= LEVELS[level]); +}; + +Log.prototype.warn = function() { + if (this.isWarnEnabled()) + this._write('WARN', Array.prototype.slice.call(arguments)); +}; + +Log.prototype.isErrorEnabled = function() { + return (LEVELS.Error >= LEVELS[level]); +}; + +Log.prototype.error = function() { + if (this.isErrorEnabled()) + this._write('ERROR', Array.prototype.slice.call(arguments)); +}; + +Log.prototype.isFatalEnabled = function() { + return (LEVELS.Fatal >= LEVELS[level]); +}; + +Log.prototype.fatal = function() { + if (this.isFatalEnabled()) + this._write('FATAL', Array.prototype.slice.call(arguments)); +}; + + +module.exports = { + + setLevel: function(l) { + if (LEVELS[l] !== undefined) + level = l; + + return level; + }, + + getLogger: function(name) { + if (!name || typeof(name) !== 'string') + throw new TypeError('name (string) required'); + + return new Log(name); + } + +}; diff --git a/lib/messages/add_request.js b/lib/messages/add_request.js new file mode 100644 index 0000000..e795a2c --- /dev/null +++ b/lib/messages/add_request.js @@ -0,0 +1,98 @@ +// Copyright 2011 Mark Cavage, Inc. All rights reserved. + +var assert = require('assert'); +var util = require('util'); + +var asn1 = require('asn1'); + +var LDAPMessage = require('./message'); +var LDAPResult = require('./result'); + +var dn = require('../dn'); +var Attribute = require('../attribute'); +var Protocol = require('../protocol'); + +///--- Globals + +var Ber = asn1.Ber; + + + +///--- API + +function AddRequest(options) { + if (options) { + if (typeof(options) !== 'object') + throw new TypeError('options must be an object'); + if (options.entry && !(options.entry instanceof dn.DN)) + throw new TypeError('options.entry must be a DN'); + if (options.attributes) { + if (!Array.isArray(options.attributes)) + throw new TypeError('options.attributes must be [Attribute]'); + options.attributes.forEach(function(a) { + if (!Attribute.isAttribute(a)) + throw new TypeError('options.attributes must be [Attribute]'); + }); + } + } else { + options = {}; + } + + options.protocolOp = Protocol.LDAP_REQ_ADD; + LDAPMessage.call(this, options); + + this.entry = options.entry || null; + this.attributes = options.attributes ? options.attributes.slice(0) : []; + + var self = this; + this.__defineGetter__('type', function() { return 'AddRequest'; }); + this.__defineGetter__('_dn', function() { return self.entry; }); +} +util.inherits(AddRequest, LDAPMessage); +module.exports = AddRequest; + + +AddRequest.prototype._parse = function(ber) { + assert.ok(ber); + + this.entry = dn.parse(ber.readString()); + + ber.readSequence(); + + var end = ber.offset + ber.length; + while (ber.offset < end) { + var a = new Attribute(); + a.parse(ber); + this.attributes.push(a); + } + + return true; +}; + + +AddRequest.prototype._toBer = function(ber) { + assert.ok(ber); + + ber.writeString(this.entry.toString()); + ber.startSequence(); + this.attributes.forEach(function(a) { + a.toBer(ber); + }); + ber.endSequence(); + + return ber; +}; + + +AddRequest.prototype._json = function(j) { + assert.ok(j); + + j.entry = this.entry.toString(); + j.attributes = []; + + this.attributes.forEach(function(a) { + j.attributes.push(a.json); + }); + + return j; +}; diff --git a/lib/messages/add_response.js b/lib/messages/add_response.js new file mode 100644 index 0000000..23fd242 --- /dev/null +++ b/lib/messages/add_response.js @@ -0,0 +1,23 @@ +// Copyright 2011 Mark Cavage, Inc. All rights reserved. + +var util = require('util'); + +var LDAPResult = require('./result'); +var Protocol = require('../protocol'); + + +///--- API + +function AddResponse(options) { + if (options) { + if (typeof(options) !== 'object') + throw new TypeError('options must be an object'); + } else { + options = {}; + } + + options.protocolOp = Protocol.LDAP_REP_ADD; + LDAPResult.call(this, options); +} +util.inherits(AddResponse, LDAPResult); +module.exports = AddResponse; diff --git a/lib/messages/bind_request.js b/lib/messages/bind_request.js new file mode 100644 index 0000000..0fdbe40 --- /dev/null +++ b/lib/messages/bind_request.js @@ -0,0 +1,93 @@ +// Copyright 2011 Mark Cavage, Inc. All rights reserved. + +var assert = require('assert'); +var util = require('util'); + +var asn1 = require('asn1'); + +var LDAPMessage = require('./message'); +var LDAPResult = require('./result'); + + +var dn = require('../dn'); +var Protocol = require('../protocol'); + +///--- Globals + +var Ber = asn1.Ber; + +var LDAP_BIND_SIMPLE = 'Simple'; +var LDAP_BIND_SASL = 'Sasl'; + + + +///--- API + +function BindRequest(options) { + if (options) { + if (typeof(options) !== 'object') + throw new TypeError('options must be an object'); + if (options.name && !(options.name instanceof dn.DN)) + throw new TypeError('options.entry must be a DN'); + + } else { + options = {}; + } + + options.protocolOp = Protocol.LDAP_REQ_BIND; + LDAPMessage.call(this, options); + + this.version = options.version || 0x03; + this.name = options.name || null; + this.authentication = options.authentication || LDAP_BIND_SIMPLE; + this.credentials = options.credentials || ''; + + var self = this; + this.__defineGetter__('type', function() { return 'BindRequest'; }); + this.__defineGetter__('_dn', function() { return self.name.toString(); }); +} +util.inherits(BindRequest, LDAPMessage); +module.exports = BindRequest; + + +BindRequest.prototype._parse = function(ber) { + assert.ok(ber); + + this.version = ber.readInt(); + this.name = dn.parse(ber.readString()); + + var t = ber.peek(); + + // TODO add support for SASL et al + if (t !== Ber.Context) + throw new Error('authentication 0x' + t.toString(16) + ' not supported'); + + this.authentication = LDAP_BIND_SIMPLE; + this.credentials = ber.readString(Ber.Context); + + return true; +}; + + +BindRequest.prototype._toBer = function(ber) { + assert.ok(ber); + + ber.writeInt(this.version); + ber.writeString(this.name.toString()); + // TODO add support for SASL et al + ber.writeString(this.credentials, Ber.Context); + + return ber; +}; + + +BindRequest.prototype._json = function(j) { + assert.ok(j); + + j.version = this.version; + j.name = this.name; + j.authenticationType = this.authentication; + j.credentials = this.credentials; + + return j; +}; diff --git a/lib/messages/bind_response.js b/lib/messages/bind_response.js new file mode 100644 index 0000000..1c9f9f8 --- /dev/null +++ b/lib/messages/bind_response.js @@ -0,0 +1,23 @@ +// Copyright 2011 Mark Cavage, Inc. All rights reserved. + +var util = require('util'); + +var LDAPResult = require('./result'); +var Protocol = require('../protocol'); + + +///--- API + +function BindResponse(options) { + if (options) { + if (typeof(options) !== 'object') + throw new TypeError('options must be an object'); + } else { + options = {}; + } + + options.protocolOp = Protocol.LDAP_REP_BIND; + LDAPResult.call(this, options); +} +util.inherits(BindResponse, LDAPResult); +module.exports = BindResponse; diff --git a/lib/messages/compare_request.js b/lib/messages/compare_request.js new file mode 100644 index 0000000..8153466 --- /dev/null +++ b/lib/messages/compare_request.js @@ -0,0 +1,82 @@ +// Copyright 2011 Mark Cavage, Inc. All rights reserved. + +var assert = require('assert'); +var util = require('util'); + +var LDAPMessage = require('./message'); +var LDAPResult = require('./result'); + +var dn = require('../dn'); +var Attribute = require('../attribute'); +var Protocol = require('../protocol'); + + + +///--- API + +function CompareRequest(options) { + if (options) { + if (typeof(options) !== 'object') + throw new TypeError('options must be an object'); + if (options.entry && !(options.entry instanceof dn.DN)) + throw new TypeError('options.entry must be a DN'); + if (options.attribute && typeof(options.attribute) !== 'string') + throw new TypeError('options.attribute must be a string'); + if (options.value && typeof(options.value) !== 'string') + throw new TypeError('options.value must be a string'); + } else { + options = {}; + } + + options.protocolOp = Protocol.LDAP_REQ_COMPARE; + LDAPMessage.call(this, options); + + this.entry = options.entry || null; + this.attribute = options.attribute || ''; + this.value = options.value || ''; + + var self = this; + this.__defineGetter__('type', function() { return 'CompareRequest'; }); + this.__defineGetter__('_dn', function() { + return self.entry ? self.entry.toString() : ''; + }); +} +util.inherits(CompareRequest, LDAPMessage); +module.exports = CompareRequest; + + +CompareRequest.prototype._parse = function(ber) { + assert.ok(ber); + + this.entry = dn.parse(ber.readString()); + + ber.readSequence(); + this.attribute = ber.readString(); + this.value = ber.readString(); + + return true; +}; + + +CompareRequest.prototype._toBer = function(ber) { + assert.ok(ber); + + ber.writeString(this.entry.toString()); + ber.startSequence(); + ber.writeString(this.attribute); + ber.writeString(this.value); + ber.endSequence(); + + return ber; +}; + + +CompareRequest.prototype._json = function(j) { + assert.ok(j); + + j.entry = this.entry.toString(); + j.attribute = this.attribute; + j.value = this.value; + + return j; +}; diff --git a/lib/messages/compare_response.js b/lib/messages/compare_response.js new file mode 100644 index 0000000..7e79dd5 --- /dev/null +++ b/lib/messages/compare_response.js @@ -0,0 +1,23 @@ +// Copyright 2011 Mark Cavage, Inc. All rights reserved. + +var util = require('util'); + +var LDAPResult = require('./result'); +var Protocol = require('../protocol'); + + +///--- API + +function CompareResponse(options) { + if (options) { + if (typeof(options) !== 'object') + throw new TypeError('options must be an object'); + } else { + options = {}; + } + + options.protocolOp = Protocol.LDAP_REP_COMPARE; + LDAPResult.call(this, options); +} +util.inherits(CompareResponse, LDAPResult); +module.exports = CompareResponse; diff --git a/lib/messages/del_request.js b/lib/messages/del_request.js new file mode 100644 index 0000000..b7843d8 --- /dev/null +++ b/lib/messages/del_request.js @@ -0,0 +1,78 @@ +// Copyright 2011 Mark Cavage, Inc. All rights reserved. + +var assert = require('assert'); +var util = require('util'); + +var asn1 = require('asn1'); + +var LDAPMessage = require('./message'); +var LDAPResult = require('./result'); + +var dn = require('../dn'); +var Attribute = require('../attribute'); +var Protocol = require('../protocol'); + +///--- Globals + +var Ber = asn1.Ber; + + + +///--- API + +function DeleteRequest(options) { + if (options) { + if (typeof(options) !== 'object') + throw new TypeError('options must be an object'); + if (options.entry && !(options.entry instanceof dn.DN)) + throw new TypeError('options.entry must be a DN'); + } else { + options = {}; + } + + options.protocolOp = Protocol.LDAP_REQ_DELETE; + LDAPMessage.call(this, options); + + this.entry = options.entry || null; + + var self = this; + this.__defineGetter__('type', function() { return 'DeleteRequest'; }); + this.__defineGetter__('_dn', function() { + return self.entry ? self.entry.toString() : ''; + }); +} +util.inherits(DeleteRequest, LDAPMessage); +module.exports = DeleteRequest; + + +DeleteRequest.prototype._parse = function(ber, length) { + assert.ok(ber); + + // What a hack; LDAP is so annoying with its decisions of what to + // shortcut, so this is totally a hack to work around the way the delete + // message is structured + this.entry = dn.parse(ber.buffer.slice(0, length).toString()); + ber._offset += length; + + return true; +}; + + +DeleteRequest.prototype._toBer = function(ber) { + assert.ok(ber); + + var buf = new Buffer(this.entry.toString()); + for (var i = 0; i < buf.length; i++) + ber.writeByte(buf[i]); + + return ber; +}; + + +DeleteRequest.prototype._json = function(j) { + assert.ok(j); + + j.entry = this.entry; + + return j; +}; diff --git a/lib/messages/del_response.js b/lib/messages/del_response.js new file mode 100644 index 0000000..3da42c7 --- /dev/null +++ b/lib/messages/del_response.js @@ -0,0 +1,23 @@ +// Copyright 2011 Mark Cavage, Inc. All rights reserved. + +var util = require('util'); + +var LDAPResult = require('./result'); +var Protocol = require('../protocol'); + + +///--- API + +function DeleteResponse(options) { + if (options) { + if (typeof(options) !== 'object') + throw new TypeError('options must be an object'); + } else { + options = {}; + } + + options.protocolOp = Protocol.LDAP_REP_DELETE; + LDAPResult.call(this, options); +} +util.inherits(DeleteResponse, LDAPResult); +module.exports = DeleteResponse; diff --git a/lib/messages/ext_request.js b/lib/messages/ext_request.js new file mode 100644 index 0000000..e8ccd1d --- /dev/null +++ b/lib/messages/ext_request.js @@ -0,0 +1,97 @@ +// Copyright 2011 Mark Cavage, Inc. All rights reserved. + +var assert = require('assert'); +var util = require('util'); + +var asn1 = require('asn1'); + +var LDAPMessage = require('./message'); +var LDAPResult = require('./result'); + +var dn = require('../dn'); +var Attribute = require('../attribute'); +var Protocol = require('../protocol'); + + + +///--- Globals + +var Ber = asn1.Ber; + + + +///--- API + +function ExtendedRequest(options) { + if (options) { + if (typeof(options) !== 'object') + throw new TypeError('options must be an object'); + if (options.requestName && typeof(options.requestName) !== 'string') + throw new TypeError('options.requestName must be a string'); + if (options.requestValue && typeof(options.requestValue) !== 'string') + throw new TypeError('options.requestValue must be a string'); + } else { + options = {}; + } + + options.protocolOp = Protocol.LDAP_REQ_EXTENSION; + LDAPMessage.call(this, options); + + this.requestName = options.requestName || ''; + this.requestValue = options.requestValue || undefined; + + this.__defineGetter__('type', function() { return 'ExtendedRequest'; }); + this.__defineGetter__('_dn', function() { return this.requestName; }); + this.__defineGetter__('name', function() { + return this.requestName; + }); + this.__defineGetter__('value', function() { + return this.requestValue; + }); + this.__defineSetter__('name', function(name) { + if (typeof(name) !== 'string') + throw new TypeError('name must be a string'); + + this.requestName = name; + }); + this.__defineSetter__('value', function(val) { + if (typeof(val) !== 'string') + throw new TypeError('value must be a string'); + + this.requestValue = val; + }); +} +util.inherits(ExtendedRequest, LDAPMessage); +module.exports = ExtendedRequest; + + +ExtendedRequest.prototype._parse = function(ber) { + assert.ok(ber); + + this.requestName = ber.readString(0x80); + if (ber.peek() === 0x81) + this.requestValue = ber.readString(0x81); + + return true; +}; + + +ExtendedRequest.prototype._toBer = function(ber) { + assert.ok(ber); + + ber.writeString(this.requestName, 0x80); + if (this.requestValue) + ber.writeString(this.requestValue, 0x81); + + return ber; +}; + + +ExtendedRequest.prototype._json = function(j) { + assert.ok(j); + + j.requestName = this.requestName; + j.requestValue = this.requestValue; + + return j; +}; diff --git a/lib/messages/ext_response.js b/lib/messages/ext_response.js new file mode 100644 index 0000000..d325f5e --- /dev/null +++ b/lib/messages/ext_response.js @@ -0,0 +1,92 @@ +// Copyright 2011 Mark Cavage, Inc. All rights reserved. + +var assert = require('assert'); +var util = require('util'); + +var LDAPResult = require('./result'); +var Protocol = require('../protocol'); + + +///--- API + +function ExtendedResponse(options) { + if (options) { + if (typeof(options) !== 'object') + throw new TypeError('options must be an object'); + if (options.responseName && typeof(options.responseName) !== 'string') + throw new TypeError('options.responseName must be a string'); + if (options.responseValue && typeof(options.responseValue) !== 'string') + throw new TypeError('options.responseValue must be a string'); + } else { + options = {}; + } + + this.responseName = options.responseName || undefined; + this.responseValue = options.responseValue || undefined; + + options.protocolOp = Protocol.LDAP_REP_EXTENSION; + LDAPResult.call(this, options); + + this.__defineGetter__('name', function() { + return this.responseName; + }); + this.__defineGetter__('value', function() { + return this.responseValue; + }); + this.__defineSetter__('name', function(name) { + if (typeof(name) !== 'string') + throw new TypeError('name must be a string'); + + this.responseName = name; + }); + this.__defineSetter__('value', function(val) { + if (typeof(val) !== 'string') + throw new TypeError('value must be a string'); + + this.responseValue = val; + }); +} +util.inherits(ExtendedResponse, LDAPResult); +module.exports = ExtendedResponse; + + +ExtendedResponse.prototype._parse = function(ber) { + assert.ok(ber); + + if (!LDAPResult.prototype._parse.call(this, ber)) + return false; + + if (ber.peek() === 0x8a) + this.responseName = ber.readString(0x8a); + if (ber.peek() === 0x8b) + this.responseValue = ber.readString(0x8b); + + return true; +}; + + +ExtendedResponse.prototype._toBer = function(ber) { + assert.ok(ber); + + if (!LDAPResult.prototype._toBer.call(this, ber)) + return false; + + if (this.responseName) + ber.writeString(this.responseName, 0x8a); + if (this.responseValue) + ber.writeString(this.responseValue, 0x8b); + + return ber; +}; + + +ExtendedResponse.prototype._json = function(j) { + assert.ok(j); + + j = LDAPResult.prototype._json.call(this, j); + + j.responseName = this.responseName; + j.responseValue = this.responseValue; + + return j; +}; diff --git a/lib/messages/index.js b/lib/messages/index.js new file mode 100644 index 0000000..cec67e0 --- /dev/null +++ b/lib/messages/index.js @@ -0,0 +1,57 @@ +// Copyright 2011 Mark Cavage, Inc. All rights reserved. + +var LDAPMessage = require('./message'); +var LDAPResult = require('./result'); +var Parser = require('./parser'); + +var AddRequest = require('./add_request'); +var AddResponse = require('./add_response'); +var BindRequest = require('./bind_request'); +var BindResponse = require('./bind_response'); +var CompareRequest = require('./compare_request'); +var CompareResponse = require('./compare_response'); +var DeleteRequest = require('./del_request'); +var DeleteResponse = require('./del_response'); +var ExtendedRequest = require('./ext_request'); +var ExtendedResponse = require('./ext_response'); +var ModifyRequest = require('./modify_request'); +var ModifyResponse = require('./modify_response'); +var ModifyDNRequest = require('./moddn_request'); +var ModifyDNResponse = require('./moddn_response'); +var SearchRequest = require('./search_request'); +var SearchEntry = require('./search_entry'); +var SearchResponse = require('./search_response'); +var UnbindRequest = require('./unbind_request'); +var UnbindResponse = require('./unbind_response'); + + + +///--- API + +module.exports = { + + LDAPMessage: LDAPMessage, + LDAPResult: LDAPResult, + Parser: Parser, + + AddRequest: AddRequest, + AddResponse: AddResponse, + BindRequest: BindRequest, + BindResponse: BindResponse, + CompareRequest: CompareRequest, + CompareResponse: CompareResponse, + DeleteRequest: DeleteRequest, + DeleteResponse: DeleteResponse, + ExtendedRequest: ExtendedRequest, + ExtendedResponse: ExtendedResponse, + ModifyRequest: ModifyRequest, + ModifyResponse: ModifyResponse, + ModifyDNRequest: ModifyDNRequest, + ModifyDNResponse: ModifyDNResponse, + SearchRequest: SearchRequest, + SearchEntry: SearchEntry, + SearchResponse: SearchResponse, + UnbindRequest: UnbindRequest, + UnbindResponse: UnbindResponse + +}; diff --git a/lib/messages/message.js b/lib/messages/message.js new file mode 100644 index 0000000..9d861d4 --- /dev/null +++ b/lib/messages/message.js @@ -0,0 +1,106 @@ +// Copyright 2011 Mark Cavage, Inc. All rights reserved. + +var assert = require('assert'); +var util = require('util'); + +var asn1 = require('asn1'); + +var Control = require('../control'); +var Protocol = require('../protocol'); + +var logStub = require('../log_stub'); + +///--- Globals + +var Ber = asn1.Ber; +var BerReader = asn1.BerReader; +var BerWriter = asn1.BerWriter; + + + +///--- API + +/** + * LDAPMessage structure. + * + * @param {Object} options stuff. + */ +function LDAPMessage(options) { + if (!options || typeof(options) !== 'object') + throw new TypeError('options (object) required'); + + this.messageID = options.messageID || 0; + this.protocolOp = options.protocolOp || undefined; + this.controls = options.controls ? options.controls.slice(0) : []; + + this.log4js = options.log4js || logStub; + + var self = this; + this.__defineGetter__('id', function() { return self.messageID; }); + this.__defineGetter__('dn', function() { return self._dn || ''; }); + this.__defineGetter__('type', function() { return 'LDAPMessage'; }); + this.__defineGetter__('json', function() { + var j = { + messageID: self.messageID, + protocolOp: self.type + }; + j = self._json(j); + j.controls = self.controls; + return j; + }); + this.__defineGetter__('log', function() { + if (!self._log) + self._log = self.log4js.getLogger(self.type); + return self._log; + }); +} +module.exports = LDAPMessage; + + +LDAPMessage.prototype.toString = function() { + return JSON.stringify(this.json); +}; + + +LDAPMessage.prototype.parse = function(data, length) { + if (!data || !Buffer.isBuffer(data)) + throw new TypeError('data (buffer) required'); + + if (this.log.isTraceEnabled()) + this.log.trace('parse: data=%s, len=%d', util.inspect(data), length); + + var ber = new BerReader(data); + + // Delegate off to the specific type to parse + this._parse(ber, length); + + // Look for controls + if (ber.peek === Protocol.LDAP_CONTROLS && ber.offset < length) { + ber.readSequence(); + var end = ber.offset + ber.length; + while (ber.offset < end) { + var c = new Control(); + if (c.parse(ber)) + this.controls.push(c); + } + } + + if (this.log.isTraceEnabled()) + this.log.trace('Parsing done: %j', this.json); + return true; +}; + + +LDAPMessage.prototype.toBer = function() { + var writer = new BerWriter(); + writer.startSequence(); + writer.writeInt(this.messageID); + writer.startSequence(this.protocolOp); + + if (this._toBer) + writer = this._toBer(writer); + + writer.endSequence(); + writer.endSequence(); + return writer.buffer; +}; diff --git a/lib/messages/moddn_request.js b/lib/messages/moddn_request.js new file mode 100644 index 0000000..26c98e7 --- /dev/null +++ b/lib/messages/moddn_request.js @@ -0,0 +1,95 @@ +// Copyright 2011 Mark Cavage, Inc. All rights reserved. + +var assert = require('assert'); +var util = require('util'); + +var asn1 = require('asn1'); + +var LDAPMessage = require('./message'); +var LDAPResult = require('./result'); + +var dn = require('../dn'); +var Attribute = require('../attribute'); +var Protocol = require('../protocol'); + + + +///--- Globals + +var Ber = asn1.Ber; + + +///--- API + +function ModifyDNRequest(options) { + if (options) { + if (typeof(options) !== 'object') + throw new TypeError('options must be an object'); + if (options.entry && !(options.entry instanceof dn.DN)) + throw new TypeError('options.entry must be a DN'); + if (options.newRdn && !(options.newRdn instanceof dn.DN)) + throw new TypeError('options.newRdn must be a DN'); + if (options.deleteOldRdn !== undefined && + typeof(options.deleteOldRdn) !== 'boolean') + throw new TypeError('options.deleteOldRdn must be a boolean'); + if (options.newSuperior && !(options.newSuperior instanceof dn.DN)) + throw new TypeError('options.newSuperior must be a DN'); + + } else { + options = {}; + } + + options.protocolOp = Protocol.LDAP_REQ_MODRDN; + LDAPMessage.call(this, options); + + this.entry = options.entry || null; + this.newRdn = options.newRdn || null; + this.deleteOldRdn = options.deleteOldRdn || false; + this.newSuperior = options.newSuperior || null; + + var self = this; + this.__defineGetter__('type', function() { return 'ModifyDNRequest'; }); + this.__defineGetter__('_dn', function() { + return self.entry ? self.entry.toString() : ''; + }); +} +util.inherits(ModifyDNRequest, LDAPMessage); +module.exports = ModifyDNRequest; + + +ModifyDNRequest.prototype._parse = function(ber) { + assert.ok(ber); + + this.entry = dn.parse(ber.readString()); + this.newRdn = dn.parse(ber.readString()); + this.deleteOldRdn = ber.readBoolean(); + if (ber.peek() === Ber.OctetString) + this.newSuperior = ber.readString(); + + return true; +}; + + +ModifyDNRequest.prototype._toBer = function(ber) { + assert.ok(ber); + + ber.writeString(this.entry.toString()); + ber.writeString(this.newRdn.toString()); + ber.writeBoolean(this.deleteOldRdn); + if (this.newSuperior) + ber.writeString(this.newSuperior.toString()); + + return ber; +}; + + +ModifyDNRequest.prototype._json = function(j) { + assert.ok(j); + + j.entry = this.entry.toString(); + j.newRdn = this.newRdn.toString(); + j.deleteOldRdn = this.deleteOldRdn; + j.newSuperior = this.newSuperior ? this.newSuperior.toString() : ''; + + return j; +}; diff --git a/lib/messages/moddn_response.js b/lib/messages/moddn_response.js new file mode 100644 index 0000000..a24a012 --- /dev/null +++ b/lib/messages/moddn_response.js @@ -0,0 +1,23 @@ +// Copyright 2011 Mark Cavage, Inc. All rights reserved. + +var util = require('util'); + +var LDAPResult = require('./result'); +var Protocol = require('../protocol'); + + +///--- API + +function ModifyDNResponse(options) { + if (options) { + if (typeof(options) !== 'object') + throw new TypeError('options must be an object'); + } else { + options = {}; + } + + options.protocolOp = Protocol.LDAP_REP_MODRDN; + LDAPResult.call(this, options); +} +util.inherits(ModifyDNResponse, LDAPResult); +module.exports = ModifyDNResponse; diff --git a/lib/messages/modify_request.js b/lib/messages/modify_request.js new file mode 100644 index 0000000..9df0aa7 --- /dev/null +++ b/lib/messages/modify_request.js @@ -0,0 +1,93 @@ +// Copyright 2011 Mark Cavage, Inc. All rights reserved. + +var assert = require('assert'); +var util = require('util'); + +var LDAPMessage = require('./message'); +var LDAPResult = require('./result'); + +var dn = require('../dn'); +var Change = require('../change'); +var Protocol = require('../protocol'); + + + +///--- API + +function ModifyRequest(options) { + if (options) { + if (typeof(options) !== 'object') + throw new TypeError('options must be an object'); + if (options.object && !(options.object instanceof dn.DN)) + throw new TypeError('options.object must be a DN'); + if (options.attributes) { + if (!Array.isArray(options.attributes)) + throw new TypeError('options.attributes must be [Attribute]'); + options.attributes.forEach(function(a) { + if (!(a instanceof Attribute)) + throw new TypeError('options.attributes must be [Attribute]'); + }); + } + } else { + options = {}; + } + + options.protocolOp = Protocol.LDAP_REQ_MODIFY; + LDAPMessage.call(this, options); + + this.object = options.object || null; + this.changes = options.changes ? options.changes.slice(0) : []; + + var self = this; + this.__defineGetter__('type', function() { return 'ModifyRequest'; }); + this.__defineGetter__('_dn', function() { + return self.object ? self.object.toString() : ''; + }); +} +util.inherits(ModifyRequest, LDAPMessage); +module.exports = ModifyRequest; + + +ModifyRequest.prototype._parse = function(ber) { + assert.ok(ber); + + this.object = dn.parse(ber.readString()); + + ber.readSequence(); + var end = ber.offset + ber.length; + while (ber.offset < end) { + var c = new Change(); + c.parse(ber); + this.changes.push(c); + } + + return true; +}; + + +ModifyRequest.prototype._toBer = function(ber) { + assert.ok(ber); + + ber.writeString(this.object.toString()); + ber.startSequence(); + this.changes.forEach(function(c) { + c.toBer(ber); + }); + ber.endSequence(); + + return ber; +}; + + +ModifyRequest.prototype._json = function(j) { + assert.ok(j); + + j.object = this.object; + j.changes = []; + + this.changes.forEach(function(c) { + j.changes.push(c.json); + }); + + return j; +}; diff --git a/lib/messages/modify_response.js b/lib/messages/modify_response.js new file mode 100644 index 0000000..c53502f --- /dev/null +++ b/lib/messages/modify_response.js @@ -0,0 +1,23 @@ +// Copyright 2011 Mark Cavage, Inc. All rights reserved. + +var util = require('util'); + +var LDAPResult = require('./result'); +var Protocol = require('../protocol'); + + +///--- API + +function ModifyResponse(options) { + if (options) { + if (typeof(options) !== 'object') + throw new TypeError('options must be an object'); + } else { + options = {}; + } + + options.protocolOp = Protocol.LDAP_REP_MODIFY; + LDAPResult.call(this, options); +} +util.inherits(ModifyResponse, LDAPResult); +module.exports = ModifyResponse; diff --git a/lib/messages/parser.js b/lib/messages/parser.js new file mode 100644 index 0000000..75ba272 --- /dev/null +++ b/lib/messages/parser.js @@ -0,0 +1,247 @@ +// Copyright 2011 Mark Cavage, Inc. All rights reserved. + +var assert = require('assert'); +var EventEmitter = require('events').EventEmitter; +var util = require('util'); + +var asn1 = require('asn1'); + +var AddRequest = require('./add_request'); +var AddResponse = require('./add_response'); +var BindRequest = require('./bind_request'); +var BindResponse = require('./bind_response'); +var CompareRequest = require('./compare_request'); +var CompareResponse = require('./compare_response'); +var DeleteRequest = require('./del_request'); +var DeleteResponse = require('./del_response'); +var ExtendedRequest = require('./ext_request'); +var ExtendedResponse = require('./ext_response'); +var ModifyRequest = require('./modify_request'); +var ModifyResponse = require('./modify_response'); +var ModifyDNRequest = require('./moddn_request'); +var ModifyDNResponse = require('./moddn_response'); +var SearchRequest = require('./search_request'); +var SearchEntry = require('./search_entry'); +var SearchResponse = require('./search_response'); +var UnbindRequest = require('./unbind_request'); +var UnbindResponse = require('./unbind_response'); +var Message = require('./message'); + +var Protocol = require('../protocol'); + +// Just make sure this adds to the prototype +require('buffertools'); + + + +///--- Globals + +var Ber = asn1.Ber; +var BerReader = asn1.BerReader; + + + +///--- API + +function Parser(options) { + if (!options || typeof(options) !== 'object') + throw new TypeError('options (object) required'); + if (!options.log4js || typeof(options.log4js) !== 'object') + throw new TypeError('options.log4js (object) required'); + + EventEmitter.call(this); + + this._reset(); + + var self = this; + this.log4js = options.log4js; + this.log = this.log4js.getLogger('Parser'); +} +util.inherits(Parser, EventEmitter); +module.exports = Parser; + + +Parser.prototype.write = function(data) { + if (!data || !Buffer.isBuffer(data)) + throw new TypeError('data (buffer) required'); + + var self = this; + + if (this._buffer) + data = this._buffer.concat(data); + + if (this.log.isTraceEnabled()) + this.log.trace('Processing buffer (concat\'d): ' + util.inspect(data)); + + // If there's more than one message in this buffer + var extra; + + try { + if (this._message === null) { + var ber = new BerReader(data); + if (!this._newMessage(ber)) + return false; + + data = data.slice(ber.offset); + } + + if (data.length > this._messageLength) { + extra = data.slice(ber.length); + data = data.slice(0, ber.length); + } + + if (!this._message.parse(data, ber.length)) + this.emit('protocolError', new Error('TODO')); + + var message = this._message; + this._reset(); + this.emit('message', message); + + } catch (e) { + if (e.name === 'InvalidAsn1Error') { + self.emit('protocolError', e, self._message); + } else { + self.emit('error', e); + } + return false; + } + + // Another message is already there + if (extra) { + if (this.log.isTraceEnabled()) + this.log.trace('parsing extra bytes: ' + util.inspect(extra)); + + return this.write(extra); + } + + return true; +}; + + +Parser.prototype._newMessage = function(ber) { + assert.ok(ber); + + if (this._messageLength === null) { + if (ber.readSequence() === null) { // not enough data for the length? + this._buffer = ber.buffer; + if (this.log.isTraceEnabled()) + this.log.trace('Not enough data for the message header'); + + return false; + } + this._messageLength = ber.length; + + } + + if (ber.remain < this._messageLength) { + if (this.log.isTraceEnabled()) + this.log.trace('Not enough data for the message'); + + this._buffer = ber.buffer; + return false; + } + + var messageID = ber.readInt(); + var type = ber.readSequence(); + + if (this.log.isTraceEnabled()) + this.log.trace('message id=%d, type=0x%s', messageID, type.toString(16)); + + var Message; + switch (type) { + + case Protocol.LDAP_REQ_ADD: + Message = AddRequest; + break; + + case Protocol.LDAP_REP_ADD: + Message = AddResponse; + break; + + case Protocol.LDAP_REQ_BIND: + Message = BindRequest; + break; + + case Protocol.LDAP_REP_BIND: + Message = BindResponse; + break; + + case Protocol.LDAP_REQ_COMPARE: + Message = CompareRequest; + break; + + case Protocol.LDAP_REP_COMPARE: + Message = CompareResponse; + break; + + case Protocol.LDAP_REQ_DELETE: + Message = DeleteRequest; + break; + + case Protocol.LDAP_REP_DELETE: + Message = DeleteResponse; + break; + + case Protocol.LDAP_REQ_EXTENSION: + Message = ExtendedRequest; + break; + + case Protocol.LDAP_REP_EXTENSION: + Message = ExtendedResponse; + break; + + case Protocol.LDAP_REQ_MODIFY: + Message = ModifyRequest; + break; + + case Protocol.LDAP_REP_MODIFY: + Message = ModifyResponse; + break; + + case Protocol.LDAP_REQ_MODRDN: + Message = ModifyDNRequest; + break; + + case Protocol.LDAP_REP_MODRDN: + Message = ModifyDNResponse; + break; + + case Protocol.LDAP_REQ_SEARCH: + Message = SearchRequest; + break; + + case Protocol.LDAP_REP_SEARCH_ENTRY: + Message = SearchEntry; + break; + + case Protocol.LDAP_REP_SEARCH: + Message = SearchResponse; + break; + + case Protocol.LDAP_REQ_UNBIND: + Message = UnbindRequest; + break; + + default: + var e = new Error('protocolOp 0x' + type.toString(16) + ' not supported'); + this.emit('protocolError', e, messageID); + this._reset(); + return false; + } + assert.ok(Message); + + var self = this; + this._message = new Message({ + messageID: messageID, + log4js: self.log4js + }); + + return true; +}; + + +Parser.prototype._reset = function() { + this._message = null; + this._messageLength = null; + this._buffer = null; +}; diff --git a/lib/messages/result.js b/lib/messages/result.js new file mode 100644 index 0000000..245bcdc --- /dev/null +++ b/lib/messages/result.js @@ -0,0 +1,117 @@ +// Copyright 2011 Mark Cavage, Inc. All rights reserved. + +var assert = require('assert'); +var util = require('util'); + +var asn1 = require('asn1'); + +var LDAPMessage = require('./message'); + +var Protocol = require('../protocol'); + + + +///--- Globals + +var Ber = asn1.Ber; +var BerWriter = asn1.BerWriter; + + +///--- API + +function LDAPResult(options) { + if (options) { + if (typeof(options) !== 'object') + throw new TypeError('options (object) required'); + if (options.status && typeof(options.status) !== 'number') + throw new TypeError('options.status must be a number'); + if (options.matchedDN && typeof(options.matchedDN) !== 'string') + throw new TypeError('options.matchedDN must be a string'); + if (options.errorMessage && typeof(options.errorMessage) !== 'string') + throw new TypeError('options.errorMessage must be a string'); + + if (options.referrals) { + if (!(options.referrals instanceof Array)) + throw new TypeError('options.referrrals must be an array[string]'); + options.referrals.forEach(function(r) { + if (typeof(r) !== 'string') + throw new TypeError('options.referrals must be an array[string]'); + }); + } + } else { + options = {}; + } + + LDAPMessage.call(this, options); + + this.status = options.status || 0; // LDAP SUCCESS + this.matchedDN = options.matchedDN || ''; + this.errorMessage = options.errorMessage || ''; + this.referrals = options.referrals || []; + + this.__defineGetter__('type', function() { return 'LDAPResult'; }); +} +util.inherits(LDAPResult, LDAPMessage); +module.exports = LDAPResult; + + +LDAPResult.prototype.end = function(status) { + assert.ok(this.connection); + + if (typeof(status) === 'number') + this.status = status; + + var ber = this.toBer(); + if (this.log.isDebugEnabled()) + this.log.debug('%s: sending: %j', this.connection.ldap.id, this.json); + + this.connection.write(ber); +}; + + +LDAPResult.prototype._parse = function(ber) { + assert.ok(ber); + + this.status = ber.readEnumeration(); + this.matchedDN = ber.readString(); + this.errorMessage = ber.readString(); + + var t = ber.peek(); + + if (t === Protocol.LDAP_REP_REFERRAL) { + var end = ber.offset + ber.length; + while (ber.offset < end) + this.referrals.push(ber.readString()); + } + + return true; +}; + + +LDAPResult.prototype._toBer = function(ber) { + assert.ok(ber); + + ber.writeEnumeration(this.status); + ber.writeString(this.matchedDN || ''); + ber.writeString(this.errorMessage || ''); + + if (this.referrals.length) { + ber.startSequence(Protocol.LDAP_REP_REFERRAL); + ber.writeStringArray(this.referrals); + ber.endSequence(); + } + + return ber; +}; + + +LDAPResult.prototype._json = function(j) { + assert.ok(j); + + j.status = this.status; + j.matchedDN = this.matchedDN; + j.errorMessage = this.errorMessage; + j.referrals = this.referrals; + + return j; +}; diff --git a/lib/messages/search_entry.js b/lib/messages/search_entry.js new file mode 100644 index 0000000..8847e31 --- /dev/null +++ b/lib/messages/search_entry.js @@ -0,0 +1,110 @@ +// Copyright 2011 Mark Cavage, Inc. All rights reserved. + +var assert = require('assert'); +var util = require('util'); + +var asn1 = require('asn1'); + +var LDAPMessage = require('./message'); +var Attribute = require('../attribute'); +var dn = require('../dn'); +var Protocol = require('../protocol'); + + + +///--- Globals + +var BerWriter = asn1.BerWriter; + + + +///--- API + +function SearchEntry(options) { + if (options) { + if (typeof(options) !== 'object') + throw new TypeError('options must be an object'); + if (options.objectName && !(options.objectName instanceof dn.DN)) + throw new TypeError('options.objectName must be a DN'); + if (options.attributes && !Array.isArray(options.attributes)) + throw new TypeError('options.attributes must be an array[Attribute]'); + if (options.attributes && options.attributes.length) { + options.attributes.forEach(function(a) { + if (!(a instanceof Attribute)) + throw new TypeError('options.attributes must be an array[Attribute]'); + }); + } + } else { + options = {}; + } + + options.protocolOp = Protocol.LDAP_REP_SEARCH_ENTRY; + LDAPMessage.call(this, options); + + this.objectName = options.objectName || null; + this.attributes = options.attributes ? options.attributes.slice(0) : []; + + var self = this; + this.__defineGetter__('type', function() { return 'SearchEntry'; }); + this.__defineGetter__('_dn', function() { + return self.objectName.toString(); + }); +} +util.inherits(SearchEntry, LDAPMessage); +module.exports = SearchEntry; + + +SearchEntry.prototype.addAttribute = function(attr) { + if (!attr || typeof(attr) !== 'object') + throw new TypeError('attr (attribute) required'); + + this.attributes.push(attr); +}; + + +SearchEntry.prototype._json = function(j) { + assert.ok(j); + + j.objectName = this.objectName.toString(); + j.attributes = []; + this.attributes.forEach(function(a) { + j.attributes.push(a.json); + }); + + return j; +}; + + +SearchEntry.prototype._parse = function(ber) { + assert.ok(ber); + + this.objectName = ber.readString(); + assert.ok(ber.readSequence()); + var end = ber.offset + ber.length; + + while (ber.offset < end) { + var a = new Attribute(); + a.parse(ber); + this.attributes.push(a); + } + + return true; +}; + + +SearchEntry.prototype._toBer = function(ber) { + assert.ok(ber); + + ber.writeString(this.objectName.toString()); + ber.startSequence(); + this.attributes.forEach(function(a) { + // This may or may not be an attribute + ber = Attribute.toBer(a, ber); + }); + ber.endSequence(); + + return ber; +}; + + + diff --git a/lib/messages/search_request.js b/lib/messages/search_request.js new file mode 100644 index 0000000..a392a58 --- /dev/null +++ b/lib/messages/search_request.js @@ -0,0 +1,154 @@ +// Copyright 2011 Mark Cavage, Inc. All rights reserved. + +var assert = require('assert'); +var util = require('util'); + +var asn1 = require('asn1'); + +var LDAPMessage = require('./message'); +var LDAPResult = require('./result'); + +var dn = require('../dn'); +var filters = require('../filters'); +var Protocol = require('../protocol'); + + + +///--- Globals + +var Ber = asn1.Ber; + + + +///--- API + +function SearchRequest(options) { + if (options) { + if (typeof(options) !== 'object') + throw new TypeError('options must be an object'); + } else { + options = {}; + } + + options.protocolOp = Protocol.LDAP_REQ_SEARCH; + LDAPMessage.call(this, options); + + var self = this; + this.__defineGetter__('type', function() { return 'SearchRequest'; }); + this.__defineGetter__('_dn', function() { + return self.baseObject; + }); + this.__defineGetter__('scope', function() { + switch (self._scope) { + case Protocol.SCOPE_BASE_OBJECT: return 'base'; + case Protocol.SCOPE_ONE_LEVEL: return 'one'; + case Protocol.SCOPE_SUBTREE: return 'sub'; + default: + throw new Error(self._scope + ' is an invalid search scope'); + } + }); + this.__defineSetter__('scope', function(s) { + if (typeof(s) === 'string') { + switch (s) { + case 'base': + self._scope = Protocol.SCOPE_BASE_OBJECT; + break; + case 'one': + self._scope = Protocol.SCOPE_ONE_LEVEL; + break; + case 'sub': + self._scope = Protocol.SCOPE_SUBTREE; + break; + default: + throw new Error(s + ' is an invalid search scope'); + } + } else { + self._scope = s; + } + }); + + this.baseObject = options.baseObject || new dn.DN([{}]); + this.scope = options.scope || 'base'; + this.derefAliases = options.derefAliases || Protocol.NEVER_DEREF_ALIASES; + this.sizeLimit = options.sizeLimit || 0; + this.timeLimit = options.timeLimit || 00; + this.typesOnly = options.typesOnly || false; + this.filter = options.filter || null; + this.attributes = options.attributes ? options.attributes.slice(0) : []; +} +util.inherits(SearchRequest, LDAPMessage); +module.exports = SearchRequest; + + +SearchRequest.prototype.newResult = function() { + var self = this; + + return new LDAPResult({ + messageID: self.messageID, + protocolOp: Protocol.LDAP_REP_SEARCH + }); +}; + + +SearchRequest.prototype._parse = function(ber) { + assert.ok(ber); + + this.baseObject = dn.parse(ber.readString()); + this.scope = ber.readEnumeration(); + this.derefAliases = ber.readEnumeration(); + this.sizeLimit = ber.readInt(); + this.timeLimit = ber.readInt(); + this.typesOnly = ber.readBoolean(); + + this.filter = filters.parse(ber); + + // look for attributes + if (ber.readSequence() === (Ber.Sequence | Ber.Constructor)) { + var end = ber.offset + ber.length; + while (ber.offset < end) + this.attributes.push(ber.readString().toLowerCase()); + } + + return true; +}; + + +SearchRequest.prototype._toBer = function(ber) { + assert.ok(ber); + + ber.writeString(this.baseObject.toString()); + ber.writeEnumeration(this._scope); + ber.writeEnumeration(this.derefAliases); + ber.writeInt(this.sizeLimit); + ber.writeInt(this.timeLimit); + ber.writeBoolean(this.typesOnly); + + var f = this.filter || new filters.PresenceFilter({attribute: 'objectclass'}); + ber = f.toBer(ber); + + if (this.attributes && this.attributes.length) { + ber.startSequence(Ber.Sequence | Ber.Constructor); + this.attributes.forEach(function(a) { + ber.writeString(a); + }); + ber.endSequence(); + } + + return ber; +}; + + +SearchRequest.prototype._json = function(j) { + assert.ok(j); + + j.baseObject = this.baseObject; + j.scope = this.scope; + j.derefAliases = this.derefAliases; + j.sizeLimit = this.sizeLimit; + j.timeLimit = this.timeLimit; + j.typesOnly = this.typesOnly; + j.filter = this.filter.toString(); + j.attributes = this.attributes; + + return j; +}; diff --git a/lib/messages/search_response.js b/lib/messages/search_response.js new file mode 100644 index 0000000..4f68a9a --- /dev/null +++ b/lib/messages/search_response.js @@ -0,0 +1,60 @@ +// Copyright 2011 Mark Cavage, Inc. All rights reserved. + +var assert = require('assert'); +var util = require('util'); + +var LDAPResult = require('./result'); +var SearchEntry = require('./search_entry'); + +var Protocol = require('../protocol'); + + + +///--- API + +function SearchResponse(options) { + if (!options) + options = {}; + if (typeof(options) !== 'object') + throw new TypeError('options must be an object'); + + options.protocolOp = Protocol.LDAP_REP_SEARCH; + LDAPResult.call(this, options); +} +util.inherits(SearchResponse, LDAPResult); +module.exports = SearchResponse; + + +/** + * Allows you to send a SearchEntry back to the client. + * + * @param {Object} entry an instance of SearchEntry. + */ +SearchResponse.prototype.send = function(entry) { + if (!entry || !(entry instanceof SearchEntry)) + throw new TypeError('entry (SearchEntry) required'); + if (entry.messageID !== this.messageID) + throw new Error('SearchEntry messageID mismatch'); + + assert.ok(this.connection); + + if (this.log.isDebugEnabled()) + this.log.debug('%s: sending: %j', this.connection.ldap.id, entry.json); + + this.connection.write(entry.toBer()); +}; + + +SearchResponse.prototype.createSearchEntry = function(options) { + if (options) { + if (typeof(options) !== 'object') + throw new TypeError('options must be an object'); + } else { + options = {}; + } + + options.messageID = this.messageID; + options.log4js = this.log4js; + + return new SearchEntry(options); +}; diff --git a/lib/messages/unbind_request.js b/lib/messages/unbind_request.js new file mode 100644 index 0000000..cc449bf --- /dev/null +++ b/lib/messages/unbind_request.js @@ -0,0 +1,83 @@ +// Copyright 2011 Mark Cavage, Inc. All rights reserved. + +var assert = require('assert'); +var util = require('util'); + +var asn1 = require('asn1'); + +var LDAPMessage = require('./message'); +var LDAPResult = require('./result'); + +var Protocol = require('../protocol'); + + + +///--- Globals + +var Ber = asn1.Ber; + + + +///--- API + +function UnbindRequest(options) { + if (options) { + if (typeof(options) !== 'object') + throw new TypeError('options must be an object'); + } else { + options = {}; + } + + options.protocolOp = Protocol.LDAP_REQ_UNBIND; + LDAPMessage.call(this, options); + + this.__defineGetter__('type', function() { return 'UnbindRequest'; }); + this.__defineGetter__('_dn', function() { return ''; }); +} +util.inherits(UnbindRequest, LDAPMessage); +module.exports = UnbindRequest; + + +UnbindRequest.prototype.newResult = function() { + var self = this; + + // This one is special, so just hack up the result object + function UnbindResponse(options) { + LDAPMessage.call(this, options); + this.__defineGetter__('type', function() { return 'UnbindResponse'; }); + } + util.inherits(UnbindResponse, LDAPMessage); + UnbindResponse.prototype.end = function(status) { + if (this.log.isTraceEnabled()) + log.trace('%s: unbinding!', this.connection.ldap.id); + this.connection.end(); + }; + UnbindResponse.prototype._json = function(j) { return j; }; + + return new UnbindResponse({ + messageID: 0, + protocolOp: 0, + status: 0 // Success + }); +}; + + +UnbindRequest.prototype._parse = function(ber) { + assert.ok(ber); + + return true; +}; + + +UnbindRequest.prototype._toBer = function(ber) { + assert.ok(ber); + + return ber; +}; + + +UnbindRequest.prototype._json = function(j) { + assert.ok(j); + + return j; +}; diff --git a/lib/messages/unbind_response.js b/lib/messages/unbind_response.js new file mode 100644 index 0000000..4a9f30f --- /dev/null +++ b/lib/messages/unbind_response.js @@ -0,0 +1,46 @@ +// Copyright 2011 Mark Cavage, Inc. All rights reserved. + +var assert = require('assert'); +var util = require('util'); + +var LDAPMessage = require('./result'); +var Protocol = require('../protocol'); + + +///--- API +// Ok, so there's really no such thing as an unbind 'response', but to make +// the framework not suck, I just made this up, and have it stubbed so it's +// not such a one-off. + +function UnbindResponse(options) { + if (!options) + options = {}; + if (typeof(options) !== 'object') + throw new TypeError('options must be an object'); + + options.protocolOp = 0; + LDAPMessage.call(this, options); + this.__defineGetter__('type', function() { return 'UnbindResponse'; }); +} +util.inherits(UnbindResponse, LDAPMessage); +module.exports = UnbindResponse; + + +/** + * Special override that just ends the connection, if present. + * + * @param {Number} status completely ignored. + */ +UnbindResponse.prototype.end = function(status) { + assert.ok(this.connection); + + if (this.log.isTraceEnabled()) + this.log.trace('%s: unbinding!', this.connection.ldap.id); + + this.connection.end(); +}; + + +UnbindResponse.prototype._json = function(j) { + return j; +}; diff --git a/lib/protocol.js b/lib/protocol.js new file mode 100644 index 0000000..398f7b3 --- /dev/null +++ b/lib/protocol.js @@ -0,0 +1,54 @@ +// Copyright 2011 Mark Cavage, Inc. All rights reserved. + + +module.exports = { + + // Misc + LDAP_VERSION_3: 0x03, + LBER_SET: 0x31, + LDAP_CONTROLS: 0xa0, + + // Search + SCOPE_BASE_OBJECT: 0, + SCOPE_ONE_LEVEL: 1, + SCOPE_SUBTREE: 2, + + NEVER_DEREF_ALIASES: 0, + DEREF_IN_SEARCHING: 1, + DEREF_BASE_OBJECT: 2, + DEREF_ALWAYS: 3, + + FILTER_AND: 0xa0, + FILTER_OR: 0xa1, + FILTER_NOT: 0xa2, + FILTER_EQUALITY: 0xa3, + FILTER_SUBSTRINGS: 0xa4, + FILTER_GE: 0xa5, + FILTER_LE: 0xa6, + FILTER_PRESENT: 0x87, + FILTER_APPROX: 0xa8, + FILTER_EXT: 0xa9, + + // Protocol Operations + LDAP_REQ_BIND: 0x60, + LDAP_REQ_UNBIND: 0x42, + LDAP_REQ_SEARCH: 0x63, + LDAP_REQ_MODIFY: 0x66, + LDAP_REQ_ADD: 0x68, + LDAP_REQ_DELETE: 0x4a, + LDAP_REQ_MODRDN: 0x6c, + LDAP_REQ_COMPARE: 0x6e, + LDAP_REQ_ABANDON: 0x50, + LDAP_REQ_EXTENSION: 0x77, + + LDAP_REP_BIND: 0x61, + LDAP_REP_SEARCH_ENTRY: 0x64, + LDAP_REP_SEARCH_REF: 0x73, + LDAP_REP_SEARCH: 0x65, + LDAP_REP_MODIFY: 0x67, + LDAP_REP_ADD: 0x69, + LDAP_REP_DELETE: 0x6b, + LDAP_REP_MODRDN: 0x6d, + LDAP_REP_COMPARE: 0x6f, + LDAP_REP_EXTENSION: 0x78 +}; diff --git a/lib/server.js b/lib/server.js new file mode 100644 index 0000000..efcfa54 --- /dev/null +++ b/lib/server.js @@ -0,0 +1,509 @@ +// Copyright 2011 Mark Cavage, Inc. All rights reserved. + +var assert = require('assert'); +var EventEmitter = require('events').EventEmitter; +var net = require('net'); +var tls = require('tls'); +var util = require('util'); + +var asn1 = require('asn1'); +var sprintf = require('sprintf').sprintf; + +var dn = require('./dn'); +var errors = require('./errors'); +var Protocol = require('./protocol'); +var logStub = require('./log_stub'); + +var Parser = require('./messages').Parser; +var AddResponse = require('./messages/add_response'); +var BindResponse = require('./messages/bind_response'); +var CompareResponse = require('./messages/compare_response'); +var DeleteResponse = require('./messages/del_response'); +var ExtendedResponse = require('./messages/ext_response'); +var ModifyResponse = require('./messages/modify_response'); +var ModifyDNResponse = require('./messages/moddn_response'); +var SearchResponse = require('./messages/search_response'); +var UnbindResponse = require('./messages/unbind_response'); + + + +///--- Globals + +var Ber = asn1.Ber; +var BerReader = asn1.BerReader; + + + +///--- Helpers + +function setupConnection(server, c, config) { + assert.ok(server); + assert.ok(c); + assert.ok(config); + + c.ldap = { + id: c.remoteAddress + ':' + c.remotePort, + config: config + }; + + c.addListener('timeout', function() { + server.log.trace('%s timed out', c.ldap.id); + c.destroy(); + }); + c.addListener('end', function() { + server.log.trace('%s shutdown', c.ldap.id); + }); + c.addListener('error', function(err) { + server.log.warn('%s unexpected connection error', c.ldap.id, err); + c.destroy(); + }); + c.addListener('close', function(had_err) { + server.log.trace('%s close; had_err=%j', c.ldap.id, had_err); + c.destroy(); + }); + return c; +} + + +function getResponse(req) { + assert.ok(req); + + var Response; + + switch (req.protocolOp) { + case Protocol.LDAP_REQ_BIND: + Response = BindResponse; + break; + case Protocol.LDAP_REQ_ABANDON: + return; // Noop + case Protocol.LDAP_REQ_ADD: + Response = AddResponse; + break; + case Protocol.LDAP_REQ_COMPARE: + Response = CompareResponse; + break; + case Protocol.LDAP_REQ_DELETE: + Response = DeleteResponse; + break; + case Protocol.LDAP_REQ_EXTENSION: + Response = ExtendedResponse; + break; + case Protocol.LDAP_REQ_MODIFY: + Response = ModifyResponse; + break; + case Protocol.LDAP_REQ_MODRDN: + Response = ModifyDNResponse; + break; + case Protocol.LDAP_REQ_SEARCH: + Response = SearchResponse; + break; + case Protocol.LDAP_REQ_UNBIND: + Response = UnbindResponse; + break; + default: + return null; + } + assert.ok(Response); + + var res = new Response({ + messageID: req.messageID, + log4js: req.log4js + }); + res.connection = req.connection; + res.logId = req.logId; + + return res; +} + + +function defaultHandler(req, res, next) { + assert.ok(req); + assert.ok(res); + assert.ok(next); + + res.matchedDN = req.dn.toString(); + res.errorMessage = 'Server method not implemented'; + res.end(errors.LDAP_OTHER); + return next(); +} + + +function noSuffixHandler(req, res, next) { + assert.ok(req); + assert.ok(res); + assert.ok(next); + + res.errorMessage = 'No tree found for: ' + req.dn.toString(); + res.end(errors.LDAP_NO_SUCH_OBJECT); + return next(); +} + + +function noExOpHandler(req, res, next) { + assert.ok(req); + assert.ok(res); + assert.ok(next); + + res.errorMessage = req.requestName + ' not supported'; + res.end(errors.LDAP_PROTOCOL_ERROR); + return next(); +} + + +function getHandlerChain(server, req) { + assert.ok(server); + assert.ok(req); + + var backend; + var handlers; + var matched = false; + for (var r in server.routes) { + if (server.routes.hasOwnProperty(r)) { + + if (req.protocolOp === Protocol.LDAP_REQ_EXTENSION) { + if (r === req.requestName) + matched = true; + } else if (req.protocolOp === Protocol.LDAP_REQ_UNBIND) { + matched = true; + } else { + if (req.dn) { + if (r === req.dn.toString()) { + matched = true; + } else if (server.routes[r]._dn && + server.routes[r]._dn.parentOf(req.dn)) { + matched = true; + } + } + } + if (!matched) + continue; + + switch (req.protocolOp) { + case Protocol.LDAP_REQ_BIND: + handlers = server.routes[r]._bind; + break; + + case Protocol.LDAP_REQ_ABANDON: + return; // Noop + + case Protocol.LDAP_REQ_ADD: + handlers = server.routes[r]._add; + break; + + case Protocol.LDAP_REQ_COMPARE: + handlers = server.routes[r]._compare; + break; + + case Protocol.LDAP_REQ_DELETE: + handlers = server.routes[r]._del; + break; + + case Protocol.LDAP_REQ_EXTENSION: + handlers = server.routes[r]._exop; + break; + + case Protocol.LDAP_REQ_MODIFY: + handlers = server.routes[r]._modify; + break; + + case Protocol.LDAP_REQ_MODRDN: + handlers = server.routes[r]._modifyDN; + break; + + case Protocol.LDAP_REQ_SEARCH: + handlers = server.routes[r]._search; + break; + + case Protocol.LDAP_REQ_UNBIND: + if (server.routes['unbind']) + handlers = server.routes['unbind']._unbind; + break; + + default: + server.log.warn('Unimplemented server method: %s', req.type); + return c.destroy(); + } + } + if (handlers) { + backend = server.routes[r]._backend; + break; + } + } + + if (!handlers) { + backend = server; + if (matched) { + server.log.warn('No handler registered for %s:%s, running default', + req.type, req.dn.toString()); + handlers = [defaultHandler]; + } else { + server.log.trace('%s does not map to a known suffix/oid', + req.dn.toString()); + handlers = [req.protocolOp !== Protocol.LDAP_REQ_EXTENSION ? + noSuffixHandler : noExOpHandler]; + } + } + + assert.ok(backend); + assert.ok(handlers); + assert.ok(handlers instanceof Array); + assert.ok(handlers.length); + + return { + backend: backend, + handlers: handlers + }; +} + + +function addHandlers(server) { + assert.ok(server); + assert.ok(server.log); + + var log = server.log; + + var ops = [ // We don't support abandon. + 'add', + 'bind', + 'compare', + 'del', + 'exop', + 'modify', + 'modifyDN', + 'search', + 'unbind' + ]; + + function processHandlerChain(chain) { + if (!chain) + return [defaultHandler]; + + if (chain instanceof Array) { + if (!chain.length) + return [defaultHandler]; + + chain.forEach(function(f) { + if (typeof(f) !== 'function') + throw new TypeError('[function(req, res, next)] required'); + }); + + return chain; + } else if (typeof(chain) === 'function') { + return [chain]; + } + + throw new TypeError('[function(req, res, next)] required'); + } + + server.routes = {}; + + ops.forEach(function(o) { + var op = '_' + o; + server[o] = function(name, handler) { + if (o === 'unbind') { + if (typeof(name === 'function')) { + handler = name; + name = 'unbind'; + } + } + if (!name || typeof(name) !== 'string') + throw new TypeError('name (string) required'); + if (!handler || typeof(handler) !== 'function') + throw new TypeError('[function(req, res, next)] required'); + + // Do this first so it will throw + var _dn = null; + if (o !== 'exop' && o !== 'unbind') { + _dn = dn.parse(name); + name = _dn.toString(); + } + + if (!server.routes[name]) + server.routes[name] = {}; + if (!server.routes[name]._backend) + server.routes[name]._backend = server; + + server.routes[name][op] = processHandlerChain(handler); + server.routes[name]._dn = _dn; + if (log.isTraceEnabled()) { + var _names = []; + server.routes[name][op].forEach(function(f) { + _names.push(f.name || 'Anonymous Function'); + }); + log.trace('%s(%s) -> %s', o, name, _names); + } + }; + }); + + server.mount = function(name, backend) { + if (!name || typeof(name) !== 'string') + throw new TypeError('name (string) required'); + if (!backend || typeof(backend) !== 'object') + throw new TypeError('backend (object) required'); + if (!backend.name) + throw new TypeError('backend is not a valid LDAP Backend'); + if (!backend.register || typeof(backend.register) !== 'function') + throw new TypeError('backend is not a valid LDAP Backend'); + + var _dn = null; + // Do this first so it will throw + _dn = dn.parse(name); + name = _dn.toString(); + + ops.forEach(function(o) { + if (o === 'exop' || o === 'unbind') + return; + + var op = '_' + o; + + if (!server.routes[name]) + server.routes[name] = {}; + if (!server.routes[name]._backend) + server.routes[name]._backend = backend; + + server.routes[name][op] = processHandlerChain(backend.register(o)); + if (log.isTraceEnabled()) { + var _names = []; + server.routes[name][op].forEach(function(f) { _names.push(f.name); }); + log.trace('%s(%s) -> %s', o, name, _names); + } + }); + + server.routes[name]._dn = _dn; + + log.info('%s mounted at %s', server.routes[name]._backend.toString(), dn); + + return server; + }; + + return server; +} + + + +///--- API + +module.exports = { + + createServer: function(options) { + if (options) { + if (typeof(options) !== 'object') + throw new TypeError('options (object) required'); + if (options.log4js && typeof(options.log4js) !== 'object') + throw new TypeError('options.log4s must be an object'); + + if (options.certificate || options.key) { + if (!(options.certificate && options.key) || + typeof(options.certificate) !== 'string' || + typeof(options.key) !== 'string') { + throw new TypeError('options.certificate and options.key (string) ' + + 'are both required for TLS'); + } + } + } else { + options = {}; + } + + var server; + + function newConnection(c) { + assert.ok(c); + + if (c.type === 'unix' && server.type === 'unix') { + c.remoteAddress = server.path; + c.remotePort = c.fd; + } + + assert.ok(c.remoteAddress); + assert.ok(c.remotePort); + + setupConnection(server, c, options); + if (server.log.isTraceEnabled()) + server.log.trace('new connection from %s', c.ldap.id); + + c.parser = new Parser({ + log4js: server.log4js + }); + c.parser.on('message', function(req) { + assert.ok(req); + + req.connection = c; + req.logId = c.remoteAddress + '::' + req.messageID; + + if (server.log.isDebugEnabled()) + server.log.debug('%s: message received: req=%j', c.ldap.id, req.json); + + var res = getResponse(req); + if (!res) { + server.log.warn('Unimplemented server method: %s', req.type); + c.destroy(); + return; + } + + var chain = getHandlerChain(server, req); + + var i = 0; + return function(err) { + if (err) { + res.status = err.code || errors.LDAP_OPERATIONS_ERROR; + res.matchedDN = err.dn ? err.dn.toString() : req.dn.toString(); + res.errorMessage = err.message || ''; + return res.end(); + } + + var next = arguments.callee; + if (chain.handlers[i]) + return chain.handlers[i++].call(chain.backend, req, res, next); + }(); + }); + + c.parser.on('protocolError', function(err, messageID) { + server.log.warn('%s sent invalid protocol message', c.ldap.id, err); + // TODO (mcavage) deal with this + // send an unsolicited notification + c.destroy(); + }); + c.parser.on('error', function(err) { + server.log.error('Exception happened parsing for %s: %s', + c.ldap.id, err.stack); + c.destroy(); + }); + c.on('data', function(data) { + assert.ok(data); + if (server.log.isTraceEnabled()) + server.log.trace('data on %s: %s', c.ldap.id, util.inspect(data)); + c.parser.write(data); + }); + + }; // end newConnection + + var secure = options.certificate && options.key; + + if (secure) { + server = tls.createServer(options, newConnection); + } else { + server = net.createServer(newConnection); + } + + server.log4js = options.log4js || logStub; + + server.ldap = { + config: options + }; + + server.__defineGetter__('log', function() { + if (!server._log) + server._log = server.log4js.getLogger('LDAPServer'); + + return server._log; + }); + + addHandlers(server); + + return server; + } +}; + + + + diff --git a/lib/url.js b/lib/url.js new file mode 100644 index 0000000..724cb75 --- /dev/null +++ b/lib/url.js @@ -0,0 +1,64 @@ +// Copyright 2011 Mark Cavage, Inc. All rights reserved. + +var querystring = require('querystring'); +var url = require('url'); +var util = require('util'); + +var dn = require('./dn'); + + + +module.exports = { + + parse: function(urlStr, parseDN) { + var u = url.parse(urlStr); + if (!u.protocol || !(u.protocol === 'ldap:' || u.protocol === 'ldaps:')) + throw new TypeError(urlStr + ' is an invalid LDAP url (protocol)'); + + if (!u.port) { + u.port = 389; + } else { + u.port = parseInt(u.port, 10); + } + + if (!u.hostname) + u.hostname = 'localhost'; + + u.secure = (u.protocol === 'ldaps:'); + + if (u.pathname) { + u.pathname = querystring.unescape(u.pathname.substr(1)); + u.DN = parseDN ? dn.parse(u.pathname) : u.pathname; + } + + if (u.search) { + u.attributes = []; + var tmp = u.search.substr(1).split('?'); + if (tmp && tmp.length) { + if (tmp[0]) { + tmp[0].split(',').forEach(function(a) { + u.attributes.push(querystring.unescape(a.trim())); + }); + } + } + if (tmp[1]) { + if (tmp[1] !== 'base' && tmp[1] !== 'one' && tmp[1] !== 'sub') + throw new TypeError(urlStr + ' is an invalid LDAP url (scope)'); + u.scope = tmp[1]; + } + if (tmp[2]) { + u.filter = querystring.unescape(tmp[2]); + } + if (tmp[3]) { + u.extensions = querystring.unescape(tmp[3]); + } + if (!u.scope) + u.scope = 'base'; + if (!u.filter) + u.filter = '(objectclass=*)'; + } + + return u; + } + +}; diff --git a/package.json b/package.json new file mode 100644 index 0000000..ad33122 --- /dev/null +++ b/package.json @@ -0,0 +1,27 @@ +{ + "author": "Mark Cavage ", + "name": "ldapjs", + "description": "LDAP client and server APIs", + "version": "0.0.1", + "repository": { + "type": "git", + "url": "git://github.com/mcavage/node-ldapjs.git" + }, + "main": "lib/index.js", + "engines": { + "node": ">=0.4.10" + }, + "dependencies": { + "asn1": "~0.1.5", + "buffertools": "~1.0.3", + "sprintf": "~0.1.1" + }, + "devDependencies": { + "tap": "~0.0.9", + "node-uuid": "~1.2.0" + }, + "scripts": { + "pretest": "which gjslint; if [[ \"$?\" = 0 ]] ; then gjslint --nojsdoc -r lib -r tst; else echo \"Missing gjslint. Skipping lint\"; fi", + "test": "./node_modules/.bin/tap ./tst" + } +} diff --git a/tst/attribute.test.js b/tst/attribute.test.js new file mode 100644 index 0000000..bac12fa --- /dev/null +++ b/tst/attribute.test.js @@ -0,0 +1,82 @@ +// Copyright 2011 Mark Cavage, Inc. All rights reserved. + +var test = require('tap').test; + +var asn1 = require('asn1'); + + +///--- Globals + +var BerReader = asn1.BerReader; +var BerWriter = asn1.BerWriter; +var Attribute; + + +///--- Tests + +test('load library', function(t) { + Attribute = require('../lib/index').Attribute; + t.ok(Attribute); + t.end(); +}); + + +test('new no args', function(t) { + t.ok(new Attribute()); + t.end(); +}); + + +test('new with args', function(t) { + var attr = new Attribute({ + type: 'cn', + vals: ['foo', 'bar'] + }); + t.ok(attr); + attr.addValue('baz'); + t.equal(attr.type, 'cn'); + t.equal(attr.vals.length, 3); + t.equal(attr.vals[0], 'foo'); + t.equal(attr.vals[1], 'bar'); + t.equal(attr.vals[2], 'baz'); + t.end(); +}); + + +test('toBer', function(t) { + var attr = new Attribute({ + type: 'cn', + vals: ['foo', 'bar'] + }); + t.ok(attr); + var ber = new BerWriter(); + attr.toBer(ber); + var reader = new BerReader(ber.buffer); + t.ok(reader.readSequence()); + t.equal(reader.readString(), 'cn'); + t.equal(reader.readSequence(), 0x31); // lber set + t.equal(reader.readString(), 'foo'); + t.equal(reader.readString(), 'bar'); + t.end(); +}); + + +test('parse', function(t) { + var ber = new BerWriter(); + ber.startSequence(); + ber.writeString('cn'); + ber.startSequence(0x31); + ber.writeStringArray(['foo', 'bar']); + ber.endSequence(); + ber.endSequence(); + + var attr = new Attribute(); + t.ok(attr); + t.ok(attr.parse(new BerReader(ber.buffer))); + + t.equal(attr.type, 'cn'); + t.equal(attr.vals.length, 2); + t.equal(attr.vals[0], 'foo'); + t.equal(attr.vals[1], 'bar'); + t.end(); +}); diff --git a/tst/change.test.js b/tst/change.test.js new file mode 100644 index 0000000..6b2d292 --- /dev/null +++ b/tst/change.test.js @@ -0,0 +1,100 @@ +// Copyright 2011 Mark Cavage, Inc. All rights reserved. + +var test = require('tap').test; + +var asn1 = require('asn1'); + + +///--- Globals + +var BerReader = asn1.BerReader; +var BerWriter = asn1.BerWriter; +var Attribute; +var Change; + + +///--- Tests + +test('load library', function(t) { + Attribute = require('../lib/index').Attribute; + Change = require('../lib/index').Change; + t.ok(Attribute); + t.ok(Change); + t.end(); +}); + + +test('new no args', function(t) { + t.ok(new Change()); + t.end(); +}); + + +test('new with args', function(t) { + var change = new Change({ + operation: 0x00, + modification: new Attribute({ + type: 'cn', + vals: ['foo', 'bar'] + }) + }); + t.ok(change); + + t.equal(change.operation, 'Add'); + t.equal(change.modification.type, 'cn'); + t.equal(change.modification.vals.length, 2); + t.equal(change.modification.vals[0], 'foo'); + t.equal(change.modification.vals[1], 'bar'); + + t.end(); +}); + + +test('toBer', function(t) { + var change = new Change({ + operation: 'Add', + modification: new Attribute({ + type: 'cn', + vals: ['foo', 'bar'] + }) + }); + t.ok(change); + + var ber = new BerWriter(); + change.toBer(ber); + var reader = new BerReader(ber.buffer); + t.ok(reader.readSequence()); + t.equal(reader.readEnumeration(), 0x00); + t.ok(reader.readSequence()); + t.equal(reader.readString(), 'cn'); + t.equal(reader.readSequence(), 0x31); // lber set + t.equal(reader.readString(), 'foo'); + t.equal(reader.readString(), 'bar'); + t.end(); +}); + + +test('parse', function(t) { + var ber = new BerWriter(); + ber.startSequence(); + ber.writeEnumeration(0x00); + ber.startSequence(); + ber.writeString('cn'); + ber.startSequence(0x31); + ber.writeStringArray(['foo', 'bar']); + ber.endSequence(); + ber.endSequence(); + ber.endSequence(); + + var change = new Change(); + t.ok(change); + t.ok(change.parse(new BerReader(ber.buffer))); + + t.equal(change.operation, 'Add'); + t.equal(change.modification.type, 'cn'); + t.equal(change.modification.vals.length, 2); + t.equal(change.modification.vals[0], 'foo'); + t.equal(change.modification.vals[1], 'bar'); + + t.end(); +}); diff --git a/tst/client.test.js b/tst/client.test.js new file mode 100644 index 0000000..3d8f04d --- /dev/null +++ b/tst/client.test.js @@ -0,0 +1,320 @@ +// Copyright 2011 Mark Cavage, Inc. All rights reserved. + +var test = require('tap').test; +var uuid = require('node-uuid'); + + +///--- Globals + +var BIND_DN = 'cn=root'; +var BIND_PW = 'secret'; +var SOCKET = '/tmp/.' + uuid(); + +var SUFFIX = 'dc=test'; + +var ldap; +var Attribute; +var Change; +var client; +var server; + + +///--- Tests + +test('setup', function(t) { + ldap = require('../lib/index'); + t.ok(ldap); + t.ok(ldap.createClient); + t.ok(ldap.createServer); + t.ok(ldap.Attribute); + t.ok(ldap.Change); + + Attribute = ldap.Attribute; + Change = ldap.Change; + + server = ldap.createServer(); + t.ok(server); + + server.bind(BIND_DN, function(req, res, next) { + if (req.credentials !== BIND_PW) + return next(new ldap.InvalidCredentialsError('Invalid password')); + + res.end(); + return next(); + }); + + server.add(SUFFIX, function(req, res, next) { + res.end(); + return next(); + }); + + server.compare(SUFFIX, function(req, res, next) { + if (req.value !== 'test') + return next(new ldap.CompareFalseError('value was test')); + + res.end(ldap.LDAP_COMPARE_TRUE); + return next(); + }); + + server.del(SUFFIX, function(req, res, next) { + res.end(); + return next(); + }); + + // LDAP whoami + server.exop('1.3.6.1.4.1.4203.1.11.3', function(req, res, next) { + res.value = 'u:xxyyz@EXAMPLE.NET'; + res.end(); + return next(); + }); + + server.modify(SUFFIX, function(req, res, next) { + res.end(); + return next(); + }); + + server.modifyDN(SUFFIX, function(req, res, next) { + res.end(); + return next(); + }); + + server.search(SUFFIX, function(req, res, next) { + var e = res.createSearchEntry({ + objectName: req.dn, + attributes: [ + new Attribute({ + type: 'cn', + vals: ['test'] + }) + ] + }); + res.send(e); + res.send(e); + res.end(); + return next(); + }); + + + server.unbind(function(req, res, next) { + res.end(); + return next(); + }); + + server.listen(SOCKET, function() { + client = ldap.createClient({ + socketPath: SOCKET + }); + t.ok(client); + // client.log4js.setLevel('Debug'); + t.end(); + }); + +}); + + +test('simple bind success', function(t) { + client.bind(BIND_DN, BIND_PW, function(err, res) { + t.ifError(err); + t.ok(res); + t.equal(res.status, 0); + t.end(); + }); +}); + + +test('simple bind failure', function(t) { + client.bind(BIND_DN, uuid(), function(err, res) { + t.ok(err); + t.notOk(res); + + t.ok(err instanceof ldap.InvalidCredentialsError); + t.ok(err instanceof Error); + t.ok(err.dn); + t.ok(err.message); + t.ok(err.stack); + + t.end(); + }); +}); + + +test('add success', function(t) { + var attrs = [ + new Attribute({ + type: 'cn', + vals: ['test'] + }) + ]; + client.add('cn=add, ' + SUFFIX, attrs, function(err, res) { + t.ifError(err); + t.ok(res); + t.equal(res.status, 0); + t.end(); + }); +}); + + +test('compare success', function(t) { + client.compare('cn=compare, ' + SUFFIX, 'cn', 'test', function(err, + matched, + res) { + t.ifError(err); + t.ok(matched); + t.ok(res); + t.end(); + }); +}); + + +test('compare false', function(t) { + client.compare('cn=compare, ' + SUFFIX, 'cn', 'foo', function(err, + matched, + res) { + t.ifError(err); + t.notOk(matched); + t.ok(res); + t.end(); + }); +}); + + +test('compare bad suffix', function(t) { + client.compare('cn=' + uuid(), 'cn', 'foo', function(err, + matched, + res) { + t.ok(err); + t.ok(err instanceof ldap.NoSuchObjectError); + t.notOk(matched); + t.notOk(res); + t.end(); + }); +}); + + +test('delete success', function(t) { + client.del('cn=delete, ' + SUFFIX, function(err, res) { + t.ifError(err); + t.ok(res); + t.end(); + }); +}); + + +test('exop success', function(t) { + client.exop('1.3.6.1.4.1.4203.1.11.3', function(err, value, res) { + t.ifError(err); + t.ok(value); + t.ok(res); + t.equal(value, 'u:xxyyz@EXAMPLE.NET'); + t.end(); + }); +}); + + +test('exop invalid', function(t) { + client.exop('1.2.3.4', function(err, res) { + t.ok(err); + t.ok(err instanceof ldap.ProtocolError); + t.notOk(res); + t.end(); + }); +}); + + +test('modify success', function(t) { + var change = new Change({ + type: 'Replace', + modification: new Attribute({ + type: 'cn', + vals: ['test'] + }) + }); + client.modify('cn=modify, ' + SUFFIX, change, function(err, res) { + t.ifError(err); + t.ok(res); + t.equal(res.status, 0); + t.end(); + }); +}); + + +test('modify array success', function(t) { + var changes = [ + new Change({ + operation: 'Replace', + modification: new Attribute({ + type: 'cn', + vals: ['test'] + }) + }), + new Change({ + operation: 'Delete', + modification: new Attribute({ + type: 'sn' + }) + }) + ]; + client.modify('cn=modify, ' + SUFFIX, changes, function(err, res) { + t.ifError(err); + t.ok(res); + t.equal(res.status, 0); + t.end(); + }); +}); + + +test('modify DN new RDN only', function(t) { + client.modifyDN('cn=old, ' + SUFFIX, 'cn=new', function(err, res) { + t.ifError(err); + t.ok(res); + t.equal(res.status, 0); + t.end(); + }); +}); + + +test('modify DN new superior', function(t) { + client.modifyDN('cn=old, ' + SUFFIX, 'cn=new, dc=foo', function(err, res) { + t.ifError(err); + t.ok(res); + t.equal(res.status, 0); + t.end(); + }); +}); + + +test('search basic', function(t) { + client.search('cn=test, ' + SUFFIX, '(objectclass=*)', function(err, res) { + t.ifError(err); + t.ok(res); + var gotEntry = 0; + res.on('searchEntry', function(entry) { + t.ok(entry); + t.ok(entry instanceof ldap.SearchEntry); + t.equal(entry.dn.toString(), 'cn=test, ' + SUFFIX); + t.ok(entry.attributes); + t.ok(entry.attributes.length); + gotEntry++; + }); + res.on('error', function(err) { + t.fail(err); + }); + res.on('end', function(res) { + t.ok(res); + t.ok(res instanceof ldap.SearchResponse); + t.equal(res.status, 0); + t.equal(gotEntry, 2); + t.end(); + }); + }); +}); + + +test('shutdown', function(t) { + client.unbind(function() { + server.on('close', function() { + t.end(); + }); + server.close(); + }); +}); diff --git a/tst/control.test.js b/tst/control.test.js new file mode 100644 index 0000000..5b82124 --- /dev/null +++ b/tst/control.test.js @@ -0,0 +1,59 @@ +// Copyright 2011 Mark Cavage, Inc. All rights reserved. + +var test = require('tap').test; + +var asn1 = require('asn1'); + + +///--- Globals + +var BerReader = asn1.BerReader; +var BerWriter = asn1.BerWriter; +var Control; + + +///--- Tests + +test('load library', function(t) { + Control = require('../lib/index').Control; + t.ok(Control); + t.end(); +}); + + +test('new no args', function(t) { + t.ok(new Control()); + t.end(); +}); + + +test('new with args', function(t) { + var c = new Control({ + type: '2.16.840.1.113730.3.4.2', + criticality: true + }); + t.ok(c); + t.equal(c.type, '2.16.840.1.113730.3.4.2'); + t.ok(c.criticality); + t.end(); +}); + + +test('parse', function(t) { + var ber = new BerWriter(); + ber.startSequence(); + ber.writeString('2.16.840.1.113730.3.4.2'); + ber.writeBoolean(true); + ber.writeString('foo'); + ber.endSequence(); + + var c = new Control(); + t.ok(c); + t.ok(c.parse(new BerReader(ber.buffer))); + + t.ok(c); + t.equal(c.type, '2.16.840.1.113730.3.4.2'); + t.ok(c.criticality); + t.equal(c.value, 'foo'); + t.end(); +}); diff --git a/tst/dn.test.js b/tst/dn.test.js new file mode 100644 index 0000000..e92bbf0 --- /dev/null +++ b/tst/dn.test.js @@ -0,0 +1,79 @@ +// Copyright 2011 Mark Cavage, Inc. All rights reserved. + +var test = require('tap').test; + + + +///--- Globals + +var dn; + + + +///--- Tests + +test('load library', function(t) { + dn = require('../lib/index').dn; + t.ok(dn); + t.end(); +}); + + +test('parse basic', function(t) { + var DN_STR = 'cn=mark, ou=people, o=joyent'; + var name = dn.parse(DN_STR); + t.ok(name); + t.ok(name.rdns); + t.ok(Array.isArray(name.rdns)); + t.equal(3, name.rdns.length); + name.rdns.forEach(function(rdn) { + t.equal('object', typeof(rdn)); + }); + t.equal(name.toString(), DN_STR); + t.end(); +}); + + +test('parse escaped', function(t) { + var DN_STR = 'cn=m\\,ark, ou=people, o=joyent'; + var name = dn.parse(DN_STR); + t.ok(name); + t.ok(name.rdns); + t.ok(Array.isArray(name.rdns)); + t.equal(3, name.rdns.length); + name.rdns.forEach(function(rdn) { + t.equal('object', typeof(rdn)); + }); + t.equal(name.toString(), DN_STR); + t.end(); +}); + + +test('parse compound', function(t) { + var DN_STR = 'cn=mark+sn=cavage, ou=people, o=joyent'; + var name = dn.parse(DN_STR); + t.ok(name); + t.ok(name.rdns); + t.ok(Array.isArray(name.rdns)); + t.equal(3, name.rdns.length); + name.rdns.forEach(function(rdn) { + t.equal('object', typeof(rdn)); + }); + t.equal(name.toString(), DN_STR); + t.end(); +}); + + +test('parse quoted', function(t) { + var DN_STR = 'cn="mark+sn=cavage", ou=people, o=joyent'; + var name = dn.parse(DN_STR); + t.ok(name); + t.ok(name.rdns); + t.ok(Array.isArray(name.rdns)); + t.equal(3, name.rdns.length); + name.rdns.forEach(function(rdn) { + t.equal('object', typeof(rdn)); + }); + t.equal(name.toString(), DN_STR); + t.end(); +}); diff --git a/tst/filters/and.test.js b/tst/filters/and.test.js new file mode 100644 index 0000000..ccd6d0c --- /dev/null +++ b/tst/filters/and.test.js @@ -0,0 +1,81 @@ +// Copyright 2011 Mark Cavage, Inc. All rights reserved. + +var test = require('tap').test; + +var asn1 = require('asn1'); + + +///--- Globals + +var EqualityFilter; +var AndFilter; + + + +///--- Tests + +test('load library', function(t) { + var filters = require('../../lib/index').filters; + t.ok(filters); + EqualityFilter = filters.EqualityFilter; + AndFilter = filters.AndFilter; + t.ok(EqualityFilter); + t.ok(AndFilter); + t.end(); +}); + + +test('Construct no args', function(t) { + t.ok(new AndFilter()); + t.end(); +}); + + +test('Construct args', function(t) { + var f = new AndFilter(); + f.addFilter(new EqualityFilter({ + attribute: 'foo', + value: 'bar' + })); + f.addFilter(new EqualityFilter({ + attribute: 'zig', + value: 'zag' + })); + t.ok(f); + t.equal(f.toString(), '(&(foo=bar)(zig=zag))'); + t.end(); +}); + + +test('match true', function(t) { + var f = new AndFilter(); + f.addFilter(new EqualityFilter({ + attribute: 'foo', + value: 'bar' + })); + f.addFilter(new EqualityFilter({ + attribute: 'zig', + value: 'zag' + })); + t.ok(f); + t.ok(f.matches({ foo: 'bar', zig: 'zag' })); + t.end(); +}); + + +test('match false', function(t) { + var f = new AndFilter(); + f.addFilter(new EqualityFilter({ + attribute: 'foo', + value: 'bar' + })); + f.addFilter(new EqualityFilter({ + attribute: 'zig', + value: 'zag' + })); + t.ok(f); + t.ok(!f.matches({ foo: 'bar', zig: 'zonk' })); + t.end(); +}); + + diff --git a/tst/filters/approx.test.js b/tst/filters/approx.test.js new file mode 100644 index 0000000..1f27df2 --- /dev/null +++ b/tst/filters/approx.test.js @@ -0,0 +1,98 @@ +// Copyright 2011 Mark Cavage, Inc. All rights reserved. + +var test = require('tap').test; + +var asn1 = require('asn1'); + + +///--- Globals + +var ApproximateFilter; +var BerReader = asn1.BerReader; +var BerWriter = asn1.BerWriter; + + + +///--- Tests + +test('load library', function(t) { + var filters = require('../../lib/index').filters; + t.ok(filters); + ApproximateFilter = filters.ApproximateFilter; + t.ok(ApproximateFilter); + t.end(); +}); + + +test('Construct no args', function(t) { + var f = new ApproximateFilter(); + t.ok(f); + t.ok(!f.attribute); + t.ok(!f.value); + t.end(); +}); + + +test('Construct args', function(t) { + var f = new ApproximateFilter({ + attribute: 'foo', + value: 'bar' + }); + t.ok(f); + t.equal(f.attribute, 'foo'); + t.equal(f.value, 'bar'); + t.equal(f.toString(), '(foo~=bar)'); + t.end(); +}); + + +test('match true', function(t) { + var f = new ApproximateFilter({ + attribute: 'foo', + value: 'bar' + }); + t.ok(f); + t.ok(f.matches({ foo: 'bar' })); + t.end(); +}); + + +test('match false', function(t) { + var f = new ApproximateFilter({ + attribute: 'foo', + value: 'bar' + }); + t.ok(f); + t.ok(!f.matches({ foo: 'baz' })); + t.end(); +}); + + +test('parse ok', function(t) { + var writer = new BerWriter(); + writer.writeString('foo'); + writer.writeString('bar'); + + var f = new ApproximateFilter(); + t.ok(f); + t.ok(f.parse(new BerReader(writer.buffer))); + t.ok(f.matches({ foo: 'bar' })); + t.end(); +}); + + +test('parse bad', function(t) { + var writer = new BerWriter(); + writer.writeString('foo'); + writer.writeInt(20); + + var f = new ApproximateFilter(); + t.ok(f); + try { + f.parse(new BerReader(writer.buffer)); + t.fail('Should have thrown InvalidAsn1Error'); + } catch (e) { + t.equal(e.name, 'InvalidAsn1Error'); + } + t.end(); +}); diff --git a/tst/filters/eq.test.js b/tst/filters/eq.test.js new file mode 100644 index 0000000..7d963ed --- /dev/null +++ b/tst/filters/eq.test.js @@ -0,0 +1,98 @@ +// Copyright 2011 Mark Cavage, Inc. All rights reserved. + +var test = require('tap').test; + +var asn1 = require('asn1'); + + +///--- Globals + +var EqualityFilter; +var BerReader = asn1.BerReader; +var BerWriter = asn1.BerWriter; + + + +///--- Tests + +test('load library', function(t) { + var filters = require('../../lib/index').filters; + t.ok(filters); + EqualityFilter = filters.EqualityFilter; + t.ok(EqualityFilter); + t.end(); +}); + + +test('Construct no args', function(t) { + var f = new EqualityFilter(); + t.ok(f); + t.ok(!f.attribute); + t.ok(!f.value); + t.end(); +}); + + +test('Construct args', function(t) { + var f = new EqualityFilter({ + attribute: 'foo', + value: 'bar' + }); + t.ok(f); + t.equal(f.attribute, 'foo'); + t.equal(f.value, 'bar'); + t.equal(f.toString(), '(foo=bar)'); + t.end(); +}); + + +test('match true', function(t) { + var f = new EqualityFilter({ + attribute: 'foo', + value: 'bar' + }); + t.ok(f); + t.ok(f.matches({ foo: 'bar' })); + t.end(); +}); + + +test('match false', function(t) { + var f = new EqualityFilter({ + attribute: 'foo', + value: 'bar' + }); + t.ok(f); + t.ok(!f.matches({ foo: 'baz' })); + t.end(); +}); + + +test('parse ok', function(t) { + var writer = new BerWriter(); + writer.writeString('foo'); + writer.writeString('bar'); + + var f = new EqualityFilter(); + t.ok(f); + t.ok(f.parse(new BerReader(writer.buffer))); + t.ok(f.matches({ foo: 'bar' })); + t.end(); +}); + + +test('parse bad', function(t) { + var writer = new BerWriter(); + writer.writeString('foo'); + writer.writeInt(20); + + var f = new EqualityFilter(); + t.ok(f); + try { + f.parse(new BerReader(writer.buffer)); + t.fail('Should have thrown InvalidAsn1Error'); + } catch (e) { + t.equal(e.name, 'InvalidAsn1Error'); + } + t.end(); +}); diff --git a/tst/filters/ge.test.js b/tst/filters/ge.test.js new file mode 100644 index 0000000..52b6cb2 --- /dev/null +++ b/tst/filters/ge.test.js @@ -0,0 +1,98 @@ +// Copyright 2011 Mark Cavage, Inc. All rights reserved. + +var test = require('tap').test; + +var asn1 = require('asn1'); + + +///--- Globals + +var GreaterThanEqualsFilter; +var BerReader = asn1.BerReader; +var BerWriter = asn1.BerWriter; + + + +///--- Tests + +test('load library', function(t) { + var filters = require('../../lib/index').filters; + t.ok(filters); + GreaterThanEqualsFilter = filters.GreaterThanEqualsFilter; + t.ok(GreaterThanEqualsFilter); + t.end(); +}); + + +test('Construct no args', function(t) { + var f = new GreaterThanEqualsFilter(); + t.ok(f); + t.ok(!f.attribute); + t.ok(!f.value); + t.end(); +}); + + +test('Construct args', function(t) { + var f = new GreaterThanEqualsFilter({ + attribute: 'foo', + value: 'bar' + }); + t.ok(f); + t.equal(f.attribute, 'foo'); + t.equal(f.value, 'bar'); + t.equal(f.toString(), '(foo>=bar)'); + t.end(); +}); + + +test('match true', function(t) { + var f = new GreaterThanEqualsFilter({ + attribute: 'foo', + value: 'bar' + }); + t.ok(f); + t.ok(f.matches({ foo: 'baz' })); + t.end(); +}); + + +test('match false', function(t) { + var f = new GreaterThanEqualsFilter({ + attribute: 'foo', + value: 'bar' + }); + t.ok(f); + t.ok(!f.matches({ foo: 'abc' })); + t.end(); +}); + + +test('parse ok', function(t) { + var writer = new BerWriter(); + writer.writeString('foo'); + writer.writeString('bar'); + + var f = new GreaterThanEqualsFilter(); + t.ok(f); + t.ok(f.parse(new BerReader(writer.buffer))); + t.ok(f.matches({ foo: 'bar' })); + t.end(); +}); + + +test('parse bad', function(t) { + var writer = new BerWriter(); + writer.writeString('foo'); + writer.writeInt(20); + + var f = new GreaterThanEqualsFilter(); + t.ok(f); + try { + f.parse(new BerReader(writer.buffer)); + t.fail('Should have thrown InvalidAsn1Error'); + } catch (e) { + t.equal(e.name, 'InvalidAsn1Error'); + } + t.end(); +}); diff --git a/tst/filters/le.test.js b/tst/filters/le.test.js new file mode 100644 index 0000000..21380b4 --- /dev/null +++ b/tst/filters/le.test.js @@ -0,0 +1,98 @@ +// Copyright 2011 Mark Cavage, Inc. All rights reserved. + +var test = require('tap').test; + +var asn1 = require('asn1'); + + +///--- Globals + +var LessThanEqualsFilter; +var BerReader = asn1.BerReader; +var BerWriter = asn1.BerWriter; + + + +///--- Tests + +test('load library', function(t) { + var filters = require('../../lib/index').filters; + t.ok(filters); + LessThanEqualsFilter = filters.LessThanEqualsFilter; + t.ok(LessThanEqualsFilter); + t.end(); +}); + + +test('Construct no args', function(t) { + var f = new LessThanEqualsFilter(); + t.ok(f); + t.ok(!f.attribute); + t.ok(!f.value); + t.end(); +}); + + +test('Construct args', function(t) { + var f = new LessThanEqualsFilter({ + attribute: 'foo', + value: 'bar' + }); + t.ok(f); + t.equal(f.attribute, 'foo'); + t.equal(f.value, 'bar'); + t.equal(f.toString(), '(foo<=bar)'); + t.end(); +}); + + +test('match true', function(t) { + var f = new LessThanEqualsFilter({ + attribute: 'foo', + value: 'bar' + }); + t.ok(f); + t.ok(f.matches({ foo: 'abc' })); + t.end(); +}); + + +test('match false', function(t) { + var f = new LessThanEqualsFilter({ + attribute: 'foo', + value: 'bar' + }); + t.ok(f); + t.ok(!f.matches({ foo: 'baz' })); + t.end(); +}); + + +test('parse ok', function(t) { + var writer = new BerWriter(); + writer.writeString('foo'); + writer.writeString('bar'); + + var f = new LessThanEqualsFilter(); + t.ok(f); + t.ok(f.parse(new BerReader(writer.buffer))); + t.ok(f.matches({ foo: 'bar' })); + t.end(); +}); + + +test('parse bad', function(t) { + var writer = new BerWriter(); + writer.writeString('foo'); + writer.writeInt(20); + + var f = new LessThanEqualsFilter(); + t.ok(f); + try { + f.parse(new BerReader(writer.buffer)); + t.fail('Should have thrown InvalidAsn1Error'); + } catch (e) { + t.equal(e.name, 'InvalidAsn1Error'); + } + t.end(); +}); diff --git a/tst/filters/not.test.js b/tst/filters/not.test.js new file mode 100644 index 0000000..48c17aa --- /dev/null +++ b/tst/filters/not.test.js @@ -0,0 +1,77 @@ +// Copyright 2011 Mark Cavage, Inc. All rights reserved. + +var test = require('tap').test; + +var asn1 = require('asn1'); + + +///--- Globals + +var EqualityFilter; +var NotFilter; + + + +///--- Tests + +test('load library', function(t) { + var filters = require('../../lib/index').filters; + t.ok(filters); + EqualityFilter = filters.EqualityFilter; + NotFilter = filters.NotFilter; + t.ok(EqualityFilter); + t.ok(NotFilter); + t.end(); +}); + + +test('Construct no args', function(t) { + try { + new NotFilter(); + t.fail('should have thrown'); + } catch (e) { + t.ok(e instanceof TypeError); + } + t.end(); +}); + + +test('Construct args', function(t) { + var f = new NotFilter({ + filter: new EqualityFilter({ + attribute: 'foo', + value: 'bar' + }) + }); + t.ok(f); + t.equal(f.toString(), '(!(foo=bar))'); + t.end(); +}); + + +test('match true', function(t) { + var f = new NotFilter({ + filter: new EqualityFilter({ + attribute: 'foo', + value: 'bar' + }) + }); + t.ok(f); + t.ok(f.matches({ foo: 'baz' })); + t.end(); +}); + + +test('match false', function(t) { + var f = new NotFilter({ + filter: new EqualityFilter({ + attribute: 'foo', + value: 'bar' + }) + }); + t.ok(f); + t.ok(!f.matches({ foo: 'bar' })); + t.end(); +}); + + diff --git a/tst/filters/or.test.js b/tst/filters/or.test.js new file mode 100644 index 0000000..ee368b2 --- /dev/null +++ b/tst/filters/or.test.js @@ -0,0 +1,81 @@ +// Copyright 2011 Mark Cavage, Inc. All rights reserved. + +var test = require('tap').test; + +var asn1 = require('asn1'); + + +///--- Globals + +var EqualityFilter; +var OrFilter; + + + +///--- Tests + +test('load library', function(t) { + var filters = require('../../lib/index').filters; + t.ok(filters); + EqualityFilter = filters.EqualityFilter; + OrFilter = filters.OrFilter; + t.ok(EqualityFilter); + t.ok(OrFilter); + t.end(); +}); + + +test('Construct no args', function(t) { + t.ok(new OrFilter()); + t.end(); +}); + + +test('Construct args', function(t) { + var f = new OrFilter(); + f.addFilter(new EqualityFilter({ + attribute: 'foo', + value: 'bar' + })); + f.addFilter(new EqualityFilter({ + attribute: 'zig', + value: 'zag' + })); + t.ok(f); + t.equal(f.toString(), '(|(foo=bar)(zig=zag))'); + t.end(); +}); + + +test('match true', function(t) { + var f = new OrFilter(); + f.addFilter(new EqualityFilter({ + attribute: 'foo', + value: 'bar' + })); + f.addFilter(new EqualityFilter({ + attribute: 'zig', + value: 'zag' + })); + t.ok(f); + t.ok(f.matches({ foo: 'bar', zig: 'zonk' })); + t.end(); +}); + + +test('match false', function(t) { + var f = new OrFilter(); + f.addFilter(new EqualityFilter({ + attribute: 'foo', + value: 'bar' + })); + f.addFilter(new EqualityFilter({ + attribute: 'zig', + value: 'zag' + })); + t.ok(f); + t.ok(!f.matches({ foo: 'baz', zig: 'zonk' })); + t.end(); +}); + + diff --git a/tst/filters/presence.test.js b/tst/filters/presence.test.js new file mode 100644 index 0000000..4cc1399 --- /dev/null +++ b/tst/filters/presence.test.js @@ -0,0 +1,91 @@ +// Copyright 2011 Mark Cavage, Inc. All rights reserved. + +var test = require('tap').test; + +var asn1 = require('asn1'); + + +///--- Globals + +var PresenceFilter; +var BerReader = asn1.BerReader; +var BerWriter = asn1.BerWriter; + + + +///--- Tests + +test('load library', function(t) { + var filters = require('../../lib/index').filters; + t.ok(filters); + PresenceFilter = filters.PresenceFilter; + t.ok(PresenceFilter); + t.end(); +}); + + +test('Construct no args', function(t) { + var f = new PresenceFilter(); + t.ok(f); + t.ok(!f.attribute); + t.end(); +}); + + +test('Construct args', function(t) { + var f = new PresenceFilter({ + attribute: 'foo' + }); + t.ok(f); + t.equal(f.attribute, 'foo'); + t.equal(f.toString(), '(foo=*)'); + t.end(); +}); + + +test('match true', function(t) { + var f = new PresenceFilter({ + attribute: 'foo' + }); + t.ok(f); + t.ok(f.matches({ foo: 'bar' })); + t.end(); +}); + + +test('match false', function(t) { + var f = new PresenceFilter({ + attribute: 'foo' + }); + t.ok(f); + t.ok(!f.matches({ bar: 'foo' })); + t.end(); +}); + + +test('parse ok', function(t) { + var writer = new BerWriter(); + writer.writeString('foo'); + + var f = new PresenceFilter(); + t.ok(f); + t.ok(f.parse(new BerReader(writer.buffer))); + t.ok(f.matches({ foo: 'bar' })); + t.end(); +}); + + +test('parse bad', function(t) { + var writer = new BerWriter(); + writer.writeInt(20); + + var f = new PresenceFilter(); + t.ok(f); + try { + f.parse(new BerReader(writer.buffer)); + t.fail('Should have thrown InvalidAsn1Error'); + } catch (e) { + t.equal(e.name, 'InvalidAsn1Error'); + } + t.end(); +}); diff --git a/tst/filters/substr.test.js b/tst/filters/substr.test.js new file mode 100644 index 0000000..cf10de4 --- /dev/null +++ b/tst/filters/substr.test.js @@ -0,0 +1,110 @@ +// Copyright 2011 Mark Cavage, Inc. All rights reserved. + +var test = require('tap').test; + +var asn1 = require('asn1'); + + +///--- Globals + +var SubstringFilter; +var BerReader = asn1.BerReader; +var BerWriter = asn1.BerWriter; + + + +///--- Tests + +test('load library', function(t) { + var filters = require('../../lib/index').filters; + t.ok(filters); + SubstringFilter = filters.SubstringFilter; + t.ok(SubstringFilter); + t.end(); +}); + + +test('Construct no args', function(t) { + var f = new SubstringFilter(); + t.ok(f); + t.ok(!f.attribute); + t.ok(!f.value); + t.end(); +}); + + +test('Construct args', function(t) { + var f = new SubstringFilter({ + attribute: 'foo', + initial: 'bar', + any: ['zig', 'zag'], + 'final': 'baz' + }); + t.ok(f); + t.equal(f.attribute, 'foo'); + t.equal(f.initial, 'bar'); + t.equal(f.any.length, 2); + t.equal(f.any[0], 'zig'); + t.equal(f.any[1], 'zag'); + t.equal(f['final'], 'baz'); + t.equal(f.toString(), '(foo=bar*zig*zag*baz)'); + t.end(); +}); + + +test('match true', function(t) { + var f = new SubstringFilter({ + attribute: 'foo', + initial: 'bar', + any: ['zig', 'zag'], + 'final': 'baz' + }); + t.ok(f); + t.ok(f.matches({ foo: 'barmoozigbarzagblahbaz' })); + t.end(); +}); + + +test('match false', function(t) { + var f = new SubstringFilter({ + attribute: 'foo', + initial: 'bar', + foo: ['zig', 'zag'], + 'final': 'baz' + }); + t.ok(f); + t.ok(!f.matches({ foo: 'bafmoozigbarzagblahbaz' })); + t.end(); +}); + + +test('parse ok', function(t) { + var writer = new BerWriter(); + writer.writeString('foo'); + writer.startSequence(); + writer.writeString('bar', 0x80); + writer.writeString('bad', 0x81); + writer.writeString('baz', 0x82); + writer.endSequence(); + var f = new SubstringFilter(); + t.ok(f); + t.ok(f.parse(new BerReader(writer.buffer))); + t.ok(f.matches({ foo: 'bargoobadgoobaz' })); + t.end(); +}); + + +test('parse bad', function(t) { + var writer = new BerWriter(); + writer.writeString('foo'); + writer.writeInt(20); + + var f = new SubstringFilter(); + t.ok(f); + try { + f.parse(new BerReader(writer.buffer)); + t.fail('Should have thrown InvalidAsn1Error'); + } catch (e) { + } + t.end(); +}); diff --git a/tst/messages/add_request.test.js b/tst/messages/add_request.test.js new file mode 100644 index 0000000..624b785 --- /dev/null +++ b/tst/messages/add_request.test.js @@ -0,0 +1,115 @@ +// Copyright 2011 Mark Cavage, Inc. All rights reserved. + +var test = require('tap').test; + +var asn1 = require('asn1'); + + +///--- Globals + +var BerReader = asn1.BerReader; +var BerWriter = asn1.BerWriter; +var AddRequest; +var Attribute; +var dn; + +///--- Tests + +test('load library', function(t) { + AddRequest = require('../../lib/index').AddRequest; + Attribute = require('../../lib/index').Attribute; + dn = require('../../lib/index').dn; + t.ok(AddRequest); + t.ok(Attribute); + t.ok(dn); + t.end(); +}); + + +test('new no args', function(t) { + t.ok(new AddRequest()); + t.end(); +}); + + +test('new with args', function(t) { + var req = new AddRequest({ + entry: dn.parse('cn=foo, o=test'), + attributes: [new Attribute({type: 'cn', vals: ['foo']}), + new Attribute({type: 'objectclass', vals: ['person']})] + }); + t.ok(req); + t.equal(req.dn.toString(), 'cn=foo, o=test'); + t.equal(req.attributes.length, 2); + t.equal(req.attributes[0].type, 'cn'); + t.equal(req.attributes[0].vals[0], 'foo'); + t.equal(req.attributes[1].type, 'objectclass'); + t.equal(req.attributes[1].vals[0], 'person'); + t.end(); +}); + + +test('parse', function(t) { + var ber = new BerWriter(); + ber.writeString('cn=foo,o=test'); + + ber.startSequence(); + + ber.startSequence(); + ber.writeString('cn'); + ber.startSequence(0x31); + ber.writeString('foo'); + ber.endSequence(); + ber.endSequence(); + + ber.startSequence(); + ber.writeString('objectclass'); + ber.startSequence(0x31); + ber.writeString('person'); + ber.endSequence(); + ber.endSequence(); + + ber.endSequence(); + + var req = new AddRequest(); + t.ok(req._parse(new BerReader(ber.buffer))); + t.equal(req.dn.toString(), 'cn=foo, o=test'); + t.equal(req.attributes.length, 2); + t.equal(req.attributes[0].type, 'cn'); + t.equal(req.attributes[0].vals[0], 'foo'); + t.equal(req.attributes[1].type, 'objectclass'); + t.equal(req.attributes[1].vals[0], 'person'); + t.end(); +}); + + +test('toBer', function(t) { + var req = new AddRequest({ + messageID: 123, + entry: dn.parse('cn=foo, o=test'), + attributes: [new Attribute({type: 'cn', vals: ['foo']}), + new Attribute({type: 'objectclass', vals: ['person']})] + }); + + t.ok(req); + + var ber = new BerReader(req.toBer()); + t.ok(ber); + t.equal(ber.readSequence(), 0x30); + t.equal(ber.readInt(), 123); + t.equal(ber.readSequence(), 0x68); + t.equal(ber.readString(), 'cn=foo, o=test'); + t.ok(ber.readSequence()); + + t.ok(ber.readSequence()); + t.equal(ber.readString(), 'cn'); + t.equal(ber.readSequence(), 0x31); + t.equal(ber.readString(), 'foo'); + + t.ok(ber.readSequence()); + t.equal(ber.readString(), 'objectclass'); + t.equal(ber.readSequence(), 0x31); + t.equal(ber.readString(), 'person'); + + t.end(); +}); diff --git a/tst/messages/add_response.test.js b/tst/messages/add_response.test.js new file mode 100644 index 0000000..fb262e4 --- /dev/null +++ b/tst/messages/add_response.test.js @@ -0,0 +1,76 @@ +// Copyright 2011 Mark Cavage, Inc. All rights reserved. + +var test = require('tap').test; + +var asn1 = require('asn1'); + + +///--- Globals + +var BerReader = asn1.BerReader; +var BerWriter = asn1.BerWriter; +var AddResponse; + + +///--- Tests + +test('load library', function(t) { + AddResponse = require('../../lib/index').AddResponse; + t.ok(AddResponse); + t.end(); +}); + + +test('new no args', function(t) { + t.ok(new AddResponse()); + t.end(); +}); + + +test('new with args', function(t) { + var res = new AddResponse({ + messageID: 123, + status: 0 + }); + t.ok(res); + t.equal(res.messageID, 123); + t.equal(res.status, 0); + t.end(); +}); + + +test('parse', function(t) { + var ber = new BerWriter(); + ber.writeEnumeration(0); + ber.writeString('cn=root'); + ber.writeString('foo'); + + var res = new AddResponse(); + t.ok(res._parse(new BerReader(ber.buffer))); + t.equal(res.status, 0); + t.equal(res.matchedDN, 'cn=root'); + t.equal(res.errorMessage, 'foo'); + t.end(); +}); + + +test('toBer', function(t) { + var res = new AddResponse({ + messageID: 123, + status: 3, + matchedDN: 'cn=root', + errorMessage: 'foo' + }); + t.ok(res); + + var ber = new BerReader(res.toBer()); + t.ok(ber); + t.equal(ber.readSequence(), 0x30); + t.equal(ber.readInt(), 123); + t.equal(ber.readSequence(), 0x69); + t.equal(ber.readEnumeration(), 3); + t.equal(ber.readString(), 'cn=root'); + t.equal(ber.readString(), 'foo'); + + t.end(); +}); diff --git a/tst/messages/bind_request.test.js b/tst/messages/bind_request.test.js new file mode 100644 index 0000000..2c590e9 --- /dev/null +++ b/tst/messages/bind_request.test.js @@ -0,0 +1,82 @@ +// Copyright 2011 Mark Cavage, Inc. All rights reserved. + +var test = require('tap').test; + +var asn1 = require('asn1'); + + +///--- Globals + +var BerReader = asn1.BerReader; +var BerWriter = asn1.BerWriter; +var BindRequest; +var dn; + +///--- Tests + +test('load library', function(t) { + BindRequest = require('../../lib/index').BindRequest; + dn = require('../../lib/index').dn; + t.ok(BindRequest); + t.ok(dn); + t.end(); +}); + + +test('new no args', function(t) { + t.ok(new BindRequest()); + t.end(); +}); + + +test('new with args', function(t) { + var req = new BindRequest({ + version: 3, + name: dn.parse('cn=root'), + credentials: 'secret' + }); + t.ok(req); + t.equal(req.version, 3); + t.equal(req.name.toString(), 'cn=root'); + t.equal(req.credentials, 'secret'); + t.end(); +}); + + +test('parse', function(t) { + var ber = new BerWriter(); + ber.writeInt(3); + ber.writeString('cn=root'); + ber.writeString('secret', 0x80); + + var req = new BindRequest(); + t.ok(req._parse(new BerReader(ber.buffer))); + t.equal(req.version, 3); + t.equal(req.dn, 'cn=root'); + t.ok(req.name.constructor); + t.equal(req.name.constructor.name, 'DN'); + t.equal(req.credentials, 'secret'); + t.end(); +}); + + +test('toBer', function(t) { + var req = new BindRequest({ + messageID: 123, + version: 3, + name: dn.parse('cn=root'), + credentials: 'secret' + }); + t.ok(req); + + var ber = new BerReader(req.toBer()); + t.ok(ber); + t.equal(ber.readSequence(), 0x30); + t.equal(ber.readInt(), 123); + t.equal(ber.readSequence(), 0x60); + t.equal(ber.readInt(), 0x03); + t.equal(ber.readString(), 'cn=root'); + t.equal(ber.readString(0x80), 'secret'); + + t.end(); +}); diff --git a/tst/messages/bind_response.test.js b/tst/messages/bind_response.test.js new file mode 100644 index 0000000..269270b --- /dev/null +++ b/tst/messages/bind_response.test.js @@ -0,0 +1,76 @@ +// Copyright 2011 Mark Cavage, Inc. All rights reserved. + +var test = require('tap').test; + +var asn1 = require('asn1'); + + +///--- Globals + +var BerReader = asn1.BerReader; +var BerWriter = asn1.BerWriter; +var BindResponse; + + +///--- Tests + +test('load library', function(t) { + BindResponse = require('../../lib/index').BindResponse; + t.ok(BindResponse); + t.end(); +}); + + +test('new no args', function(t) { + t.ok(new BindResponse()); + t.end(); +}); + + +test('new with args', function(t) { + var res = new BindResponse({ + messageID: 123, + status: 0 + }); + t.ok(res); + t.equal(res.messageID, 123); + t.equal(res.status, 0); + t.end(); +}); + + +test('parse', function(t) { + var ber = new BerWriter(); + ber.writeEnumeration(0); + ber.writeString('cn=root'); + ber.writeString('foo'); + + var res = new BindResponse(); + t.ok(res._parse(new BerReader(ber.buffer))); + t.equal(res.status, 0); + t.equal(res.matchedDN, 'cn=root'); + t.equal(res.errorMessage, 'foo'); + t.end(); +}); + + +test('toBer', function(t) { + var res = new BindResponse({ + messageID: 123, + status: 3, + matchedDN: 'cn=root', + errorMessage: 'foo' + }); + t.ok(res); + + var ber = new BerReader(res.toBer()); + t.ok(ber); + t.equal(ber.readSequence(), 0x30); + t.equal(ber.readInt(), 123); + t.equal(ber.readSequence(), 0x61); + t.equal(ber.readEnumeration(), 3); + t.equal(ber.readString(), 'cn=root'); + t.equal(ber.readString(), 'foo'); + + t.end(); +}); diff --git a/tst/messages/compare_request.test.js b/tst/messages/compare_request.test.js new file mode 100644 index 0000000..68a1166 --- /dev/null +++ b/tst/messages/compare_request.test.js @@ -0,0 +1,87 @@ +// Copyright 2011 Mark Cavage, Inc. All rights reserved. + +var test = require('tap').test; + +var asn1 = require('asn1'); + + +///--- Globals + +var BerReader = asn1.BerReader; +var BerWriter = asn1.BerWriter; +var CompareRequest; +var dn; + +///--- Tests + +test('load library', function(t) { + CompareRequest = require('../../lib/index').CompareRequest; + dn = require('../../lib/index').dn; + t.ok(CompareRequest); + t.ok(dn); + t.end(); +}); + + +test('new no args', function(t) { + t.ok(new CompareRequest()); + t.end(); +}); + + +test('new with args', function(t) { + var req = new CompareRequest({ + entry: dn.parse('cn=foo, o=test'), + attribute: 'sn', + value: 'testy' + }); + t.ok(req); + t.equal(req.dn, 'cn=foo, o=test'); + t.equal(req.attribute, 'sn'); + t.equal(req.value, 'testy'); + t.end(); +}); + + +test('parse', function(t) { + var ber = new BerWriter(); + ber.writeString('cn=foo,o=test'); + + ber.startSequence(); + ber.writeString('sn'); + ber.writeString('testy'); + ber.endSequence(); + + + var req = new CompareRequest(); + t.ok(req._parse(new BerReader(ber.buffer))); + t.equal(req.dn, 'cn=foo, o=test'); + t.equal(req.attribute, 'sn'); + t.equal(req.value, 'testy'); + t.end(); +}); + + +test('toBer', function(t) { + var req = new CompareRequest({ + messageID: 123, + entry: dn.parse('cn=foo, o=test'), + attribute: 'sn', + value: 'testy' + }); + + t.ok(req); + + var ber = new BerReader(req.toBer()); + t.ok(ber); + t.equal(ber.readSequence(), 0x30); + t.equal(ber.readInt(), 123); + t.equal(ber.readSequence(), 0x6e); + t.equal(ber.readString(), 'cn=foo, o=test'); + t.ok(ber.readSequence()); + + t.equal(ber.readString(), 'sn'); + t.equal(ber.readString(), 'testy'); + + t.end(); +}); diff --git a/tst/messages/compare_response.test.js b/tst/messages/compare_response.test.js new file mode 100644 index 0000000..837d1c9 --- /dev/null +++ b/tst/messages/compare_response.test.js @@ -0,0 +1,76 @@ +// Copyright 2011 Mark Cavage, Inc. All rights reserved. + +var test = require('tap').test; + +var asn1 = require('asn1'); + + +///--- Globals + +var BerReader = asn1.BerReader; +var BerWriter = asn1.BerWriter; +var CompareResponse; + + +///--- Tests + +test('load library', function(t) { + CompareResponse = require('../../lib/index').CompareResponse; + t.ok(CompareResponse); + t.end(); +}); + + +test('new no args', function(t) { + t.ok(new CompareResponse()); + t.end(); +}); + + +test('new with args', function(t) { + var res = new CompareResponse({ + messageID: 123, + status: 0 + }); + t.ok(res); + t.equal(res.messageID, 123); + t.equal(res.status, 0); + t.end(); +}); + + +test('parse', function(t) { + var ber = new BerWriter(); + ber.writeEnumeration(0); + ber.writeString('cn=root'); + ber.writeString('foo'); + + var res = new CompareResponse(); + t.ok(res._parse(new BerReader(ber.buffer))); + t.equal(res.status, 0); + t.equal(res.matchedDN, 'cn=root'); + t.equal(res.errorMessage, 'foo'); + t.end(); +}); + + +test('toBer', function(t) { + var res = new CompareResponse({ + messageID: 123, + status: 3, + matchedDN: 'cn=root', + errorMessage: 'foo' + }); + t.ok(res); + + var ber = new BerReader(res.toBer()); + t.ok(ber); + t.equal(ber.readSequence(), 0x30); + t.equal(ber.readInt(), 123); + t.equal(ber.readSequence(), 0x6f); + t.equal(ber.readEnumeration(), 3); + t.equal(ber.readString(), 'cn=root'); + t.equal(ber.readString(), 'foo'); + + t.end(); +}); diff --git a/tst/messages/del_request.test.js b/tst/messages/del_request.test.js new file mode 100644 index 0000000..2cd2f8e --- /dev/null +++ b/tst/messages/del_request.test.js @@ -0,0 +1,69 @@ +// Copyright 2011 Mark Cavage, Inc. All rights reserved. + +var test = require('tap').test; + +var asn1 = require('asn1'); + + +///--- Globals + +var BerReader = asn1.BerReader; +var BerWriter = asn1.BerWriter; +var DeleteRequest; +var dn; + + +///--- Tests + +test('load library', function(t) { + DeleteRequest = require('../../lib/index').DeleteRequest; + dn = require('../../lib/index').dn; + t.ok(DeleteRequest); + t.end(); +}); + + +test('new no args', function(t) { + t.ok(new DeleteRequest()); + t.end(); +}); + + +test('new with args', function(t) { + var req = new DeleteRequest({ + entry: dn.parse('cn=test') + }); + t.ok(req); + t.equal(req.dn, 'cn=test'); + t.end(); +}); + + +test('parse', function(t) { + var ber = new BerWriter(); + ber.writeString('cn=test', 0x4a); + + var req = new DeleteRequest(); + var reader = new BerReader(ber.buffer); + reader.readSequence(0x4a); + t.ok(req.parse(reader.buffer, reader.length)); + t.equal(req.dn, 'cn=test'); + t.end(); +}); + + +test('toBer', function(t) { + var req = new DeleteRequest({ + messageID: 123, + entry: dn.parse('cn=test') + }); + t.ok(req); + + var ber = new BerReader(req.toBer()); + t.ok(ber); + t.equal(ber.readSequence(), 0x30); + t.equal(ber.readInt(), 123); + t.equal(ber.readString(0x4a), 'cn=test'); + + t.end(); +}); diff --git a/tst/messages/del_response.test.js b/tst/messages/del_response.test.js new file mode 100644 index 0000000..61b3916 --- /dev/null +++ b/tst/messages/del_response.test.js @@ -0,0 +1,76 @@ +// Copyright 2011 Mark Cavage, Inc. All rights reserved. + +var test = require('tap').test; + +var asn1 = require('asn1'); + + +///--- Globals + +var BerReader = asn1.BerReader; +var BerWriter = asn1.BerWriter; +var DeleteResponse; + + +///--- Tests + +test('load library', function(t) { + DeleteResponse = require('../../lib/index').DeleteResponse; + t.ok(DeleteResponse); + t.end(); +}); + + +test('new no args', function(t) { + t.ok(new DeleteResponse()); + t.end(); +}); + + +test('new with args', function(t) { + var res = new DeleteResponse({ + messageID: 123, + status: 0 + }); + t.ok(res); + t.equal(res.messageID, 123); + t.equal(res.status, 0); + t.end(); +}); + + +test('parse', function(t) { + var ber = new BerWriter(); + ber.writeEnumeration(0); + ber.writeString('cn=root'); + ber.writeString('foo'); + + var res = new DeleteResponse(); + t.ok(res._parse(new BerReader(ber.buffer))); + t.equal(res.status, 0); + t.equal(res.matchedDN, 'cn=root'); + t.equal(res.errorMessage, 'foo'); + t.end(); +}); + + +test('toBer', function(t) { + var res = new DeleteResponse({ + messageID: 123, + status: 3, + matchedDN: 'cn=root', + errorMessage: 'foo' + }); + t.ok(res); + + var ber = new BerReader(res.toBer()); + t.ok(ber); + t.equal(ber.readSequence(), 0x30); + t.equal(ber.readInt(), 123); + t.equal(ber.readSequence(), 0x6b); + t.equal(ber.readEnumeration(), 3); + t.equal(ber.readString(), 'cn=root'); + t.equal(ber.readString(), 'foo'); + + t.end(); +}); diff --git a/tst/messages/ext_request.test.js b/tst/messages/ext_request.test.js new file mode 100644 index 0000000..e670269 --- /dev/null +++ b/tst/messages/ext_request.test.js @@ -0,0 +1,76 @@ +// Copyright 2011 Mark Cavage, Inc. All rights reserved. + +var test = require('tap').test; + +var asn1 = require('asn1'); + + +///--- Globals + +var BerReader = asn1.BerReader; +var BerWriter = asn1.BerWriter; +var ExtendedRequest; +var dn; + +///--- Tests + +test('load library', function(t) { + ExtendedRequest = require('../../lib/index').ExtendedRequest; + dn = require('../../lib/index').dn; + t.ok(ExtendedRequest); + t.ok(dn); + t.end(); +}); + + +test('new no args', function(t) { + t.ok(new ExtendedRequest()); + t.end(); +}); + + +test('new with args', function(t) { + var req = new ExtendedRequest({ + requestName: '1.2.3.4', + requestValue: 'test' + }); + t.ok(req); + t.equal(req.requestName, '1.2.3.4'); + t.equal(req.requestValue, 'test'); + t.end(); +}); + + +test('parse', function(t) { + var ber = new BerWriter(); + ber.writeString('1.2.3.4', 0x80); + ber.writeString('test', 0x81); + + + var req = new ExtendedRequest(); + t.ok(req._parse(new BerReader(ber.buffer))); + t.equal(req.requestName, '1.2.3.4'); + t.equal(req.requestValue, 'test'); + t.end(); +}); + + +test('toBer', function(t) { + var req = new ExtendedRequest({ + messageID: 123, + requestName: '1.2.3.4', + requestValue: 'test' + }); + + t.ok(req); + + var ber = new BerReader(req.toBer()); + t.ok(ber); + t.equal(ber.readSequence(), 0x30); + t.equal(ber.readInt(), 123); + t.equal(ber.readSequence(), 0x77); + t.equal(ber.readString(0x80), '1.2.3.4'); + t.equal(ber.readString(0x81), 'test'); + + t.end(); +}); diff --git a/tst/messages/ext_response.test.js b/tst/messages/ext_response.test.js new file mode 100644 index 0000000..e56ba01 --- /dev/null +++ b/tst/messages/ext_response.test.js @@ -0,0 +1,88 @@ +// Copyright 2011 Mark Cavage, Inc. All rights reserved. + +var test = require('tap').test; + +var asn1 = require('asn1'); + + +///--- Globals + +var BerReader = asn1.BerReader; +var BerWriter = asn1.BerWriter; +var ExtendedResponse; + + +///--- Tests + +test('load library', function(t) { + ExtendedResponse = require('../../lib/index').ExtendedResponse; + t.ok(ExtendedResponse); + t.end(); +}); + + +test('new no args', function(t) { + t.ok(new ExtendedResponse()); + t.end(); +}); + + +test('new with args', function(t) { + var res = new ExtendedResponse({ + messageID: 123, + status: 0, + responseName: '1.2.3.4', + responseValue: 'test' + }); + t.ok(res); + t.equal(res.messageID, 123); + t.equal(res.status, 0); + t.equal(res.responseName, '1.2.3.4'); + t.equal(res.responseValue, 'test'); + t.end(); +}); + + +test('parse', function(t) { + var ber = new BerWriter(); + ber.writeEnumeration(0); + ber.writeString('cn=root'); + ber.writeString('foo'); + ber.writeString('1.2.3.4', 0x8a); + ber.writeString('test', 0x8b); + + var res = new ExtendedResponse(); + t.ok(res._parse(new BerReader(ber.buffer))); + t.equal(res.status, 0); + t.equal(res.matchedDN, 'cn=root'); + t.equal(res.errorMessage, 'foo'); + t.equal(res.responseName, '1.2.3.4'); + t.equal(res.responseValue, 'test'); + t.end(); +}); + + +test('toBer', function(t) { + var res = new ExtendedResponse({ + messageID: 123, + status: 3, + matchedDN: 'cn=root', + errorMessage: 'foo', + responseName: '1.2.3.4', + responseValue: 'test' + }); + t.ok(res); + + var ber = new BerReader(res.toBer()); + t.ok(ber); + t.equal(ber.readSequence(), 0x30); + t.equal(ber.readInt(), 123); + t.equal(ber.readSequence(), 0x78); + t.equal(ber.readEnumeration(), 3); + t.equal(ber.readString(), 'cn=root'); + t.equal(ber.readString(), 'foo'); + t.equal(ber.readString(0x8a), '1.2.3.4'); + t.equal(ber.readString(0x8b), 'test'); + + t.end(); +}); diff --git a/tst/messages/moddn_request.test.js b/tst/messages/moddn_request.test.js new file mode 100644 index 0000000..a9afdd3 --- /dev/null +++ b/tst/messages/moddn_request.test.js @@ -0,0 +1,82 @@ +// Copyright 2011 Mark Cavage, Inc. All rights reserved. + +var test = require('tap').test; + +var asn1 = require('asn1'); + + +///--- Globals + +var BerReader = asn1.BerReader; +var BerWriter = asn1.BerWriter; +var ModifyDNRequest; +var dn; + +///--- Tests + +test('load library', function(t) { + ModifyDNRequest = require('../../lib/index').ModifyDNRequest; + dn = require('../../lib/index').dn; + t.ok(ModifyDNRequest); + t.ok(dn); + t.end(); +}); + + +test('new no args', function(t) { + t.ok(new ModifyDNRequest()); + t.end(); +}); + + +test('new with args', function(t) { + var req = new ModifyDNRequest({ + entry: dn.parse('cn=foo, o=test'), + newRdn: dn.parse('cn=foo2'), + deleteOldRdn: true + }); + t.ok(req); + t.equal(req.dn, 'cn=foo, o=test'); + t.equal(req.newRdn.toString(), 'cn=foo2'); + t.equal(req.deleteOldRdn, true); + t.end(); +}); + + +test('parse', function(t) { + var ber = new BerWriter(); + ber.writeString('cn=foo,o=test'); + ber.writeString('cn=foo2'); + ber.writeBoolean(true); + + var req = new ModifyDNRequest(); + t.ok(req._parse(new BerReader(ber.buffer))); + t.equal(req.dn, 'cn=foo, o=test'); + t.equal(req.newRdn.toString(), 'cn=foo2'); + t.equal(req.deleteOldRdn, true); + + t.end(); +}); + + +test('toBer', function(t) { + var req = new ModifyDNRequest({ + messageID: 123, + entry: dn.parse('cn=foo, o=test'), + newRdn: dn.parse('cn=foo2'), + deleteOldRdn: true + }); + + t.ok(req); + + var ber = new BerReader(req.toBer()); + t.ok(ber); + t.equal(ber.readSequence(), 0x30); + t.equal(ber.readInt(), 123); + t.equal(ber.readSequence(), 0x6c); + t.equal(ber.readString(), 'cn=foo, o=test'); + t.equal(ber.readString(), 'cn=foo2'); + t.equal(ber.readBoolean(), true); + + t.end(); +}); diff --git a/tst/messages/moddn_response.test.js b/tst/messages/moddn_response.test.js new file mode 100644 index 0000000..f5c3425 --- /dev/null +++ b/tst/messages/moddn_response.test.js @@ -0,0 +1,76 @@ +// Copyright 2011 Mark Cavage, Inc. All rights reserved. + +var test = require('tap').test; + +var asn1 = require('asn1'); + + +///--- Globals + +var BerReader = asn1.BerReader; +var BerWriter = asn1.BerWriter; +var ModifyDNResponse; + + +///--- Tests + +test('load library', function(t) { + ModifyDNResponse = require('../../lib/index').ModifyDNResponse; + t.ok(ModifyDNResponse); + t.end(); +}); + + +test('new no args', function(t) { + t.ok(new ModifyDNResponse()); + t.end(); +}); + + +test('new with args', function(t) { + var res = new ModifyDNResponse({ + messageID: 123, + status: 0 + }); + t.ok(res); + t.equal(res.messageID, 123); + t.equal(res.status, 0); + t.end(); +}); + + +test('parse', function(t) { + var ber = new BerWriter(); + ber.writeEnumeration(0); + ber.writeString('cn=root'); + ber.writeString('foo'); + + var res = new ModifyDNResponse(); + t.ok(res._parse(new BerReader(ber.buffer))); + t.equal(res.status, 0); + t.equal(res.matchedDN, 'cn=root'); + t.equal(res.errorMessage, 'foo'); + t.end(); +}); + + +test('toBer', function(t) { + var res = new ModifyDNResponse({ + messageID: 123, + status: 3, + matchedDN: 'cn=root', + errorMessage: 'foo' + }); + t.ok(res); + + var ber = new BerReader(res.toBer()); + t.ok(ber); + t.equal(ber.readSequence(), 0x30); + t.equal(ber.readInt(), 123); + t.equal(ber.readSequence(), 0x6d); + t.equal(ber.readEnumeration(), 3); + t.equal(ber.readString(), 'cn=root'); + t.equal(ber.readString(), 'foo'); + + t.end(); +}); diff --git a/tst/messages/modify_request.test.js b/tst/messages/modify_request.test.js new file mode 100644 index 0000000..1058132 --- /dev/null +++ b/tst/messages/modify_request.test.js @@ -0,0 +1,114 @@ +// Copyright 2011 Mark Cavage, Inc. All rights reserved. + +var test = require('tap').test; + +var asn1 = require('asn1'); + + +///--- Globals + +var BerReader = asn1.BerReader; +var BerWriter = asn1.BerWriter; +var ModifyRequest; +var Attribute; +var Change; +var dn; + +///--- Tests + +test('load library', function(t) { + ModifyRequest = require('../../lib/index').ModifyRequest; + Attribute = require('../../lib/index').Attribute; + Change = require('../../lib/index').Change; + dn = require('../../lib/index').dn; + t.ok(ModifyRequest); + t.ok(Attribute); + t.ok(Change); + t.ok(dn); + t.end(); +}); + + +test('new no args', function(t) { + t.ok(new ModifyRequest()); + t.end(); +}); + + +test('new with args', function(t) { + var req = new ModifyRequest({ + object: dn.parse('cn=foo, o=test'), + changes: [new Change({ + operation: 'Replace', + modification: new Attribute({type: 'objectclass', vals: ['person']}) + })] + }); + t.ok(req); + t.equal(req.dn, 'cn=foo, o=test'); + t.equal(req.changes.length, 1); + t.equal(req.changes[0].operation, 'Replace'); + t.equal(req.changes[0].modification.type, 'objectclass'); + t.equal(req.changes[0].modification.vals[0], 'person'); + t.end(); +}); + + +test('parse', function(t) { + var ber = new BerWriter(); + ber.writeString('cn=foo,o=test'); + ber.startSequence(); + + ber.startSequence(); + ber.writeEnumeration(0x02); + + ber.startSequence(); + ber.writeString('objectclass'); + ber.startSequence(0x31); + ber.writeString('person'); + ber.endSequence(); + ber.endSequence(); + + ber.endSequence(); + + ber.endSequence(); + + var req = new ModifyRequest(); + t.ok(req._parse(new BerReader(ber.buffer))); + t.equal(req.dn, 'cn=foo, o=test'); + t.equal(req.changes.length, 1); + t.equal(req.changes[0].operation, 'Replace'); + t.equal(req.changes[0].modification.type, 'objectclass'); + t.equal(req.changes[0].modification.vals[0], 'person'); + t.end(); +}); + + +test('toBer', function(t) { + var req = new ModifyRequest({ + messageID: 123, + object: dn.parse('cn=foo, o=test'), + changes: [new Change({ + operation: 'Replace', + modification: new Attribute({type: 'objectclass', vals: ['person']}) + })] + }); + + t.ok(req); + + var ber = new BerReader(req.toBer()); + t.ok(ber); + t.equal(ber.readSequence(), 0x30); + t.equal(ber.readInt(), 123); + t.equal(ber.readSequence(), 0x66); + t.equal(ber.readString(), 'cn=foo, o=test'); + t.ok(ber.readSequence()); + t.ok(ber.readSequence()); + t.equal(ber.readEnumeration(), 0x02); + + t.ok(ber.readSequence()); + t.equal(ber.readString(), 'objectclass'); + t.equal(ber.readSequence(), 0x31); + t.equal(ber.readString(), 'person'); + + t.end(); +}); diff --git a/tst/messages/modify_response.test.js b/tst/messages/modify_response.test.js new file mode 100644 index 0000000..1044303 --- /dev/null +++ b/tst/messages/modify_response.test.js @@ -0,0 +1,76 @@ +// Copyright 2011 Mark Cavage, Inc. All rights reserved. + +var test = require('tap').test; + +var asn1 = require('asn1'); + + +///--- Globals + +var BerReader = asn1.BerReader; +var BerWriter = asn1.BerWriter; +var ModifyResponse; + + +///--- Tests + +test('load library', function(t) { + ModifyResponse = require('../../lib/index').ModifyResponse; + t.ok(ModifyResponse); + t.end(); +}); + + +test('new no args', function(t) { + t.ok(new ModifyResponse()); + t.end(); +}); + + +test('new with args', function(t) { + var res = new ModifyResponse({ + messageID: 123, + status: 0 + }); + t.ok(res); + t.equal(res.messageID, 123); + t.equal(res.status, 0); + t.end(); +}); + + +test('parse', function(t) { + var ber = new BerWriter(); + ber.writeEnumeration(0); + ber.writeString('cn=root'); + ber.writeString('foo'); + + var res = new ModifyResponse(); + t.ok(res._parse(new BerReader(ber.buffer))); + t.equal(res.status, 0); + t.equal(res.matchedDN, 'cn=root'); + t.equal(res.errorMessage, 'foo'); + t.end(); +}); + + +test('toBer', function(t) { + var res = new ModifyResponse({ + messageID: 123, + status: 3, + matchedDN: 'cn=root', + errorMessage: 'foo' + }); + t.ok(res); + + var ber = new BerReader(res.toBer()); + t.ok(ber); + t.equal(ber.readSequence(), 0x30); + t.equal(ber.readInt(), 123); + t.equal(ber.readSequence(), 0x67); + t.equal(ber.readEnumeration(), 3); + t.equal(ber.readString(), 'cn=root'); + t.equal(ber.readString(), 'foo'); + + t.end(); +}); diff --git a/tst/messages/search_entry.test.js b/tst/messages/search_entry.test.js new file mode 100644 index 0000000..d9c1f5c --- /dev/null +++ b/tst/messages/search_entry.test.js @@ -0,0 +1,116 @@ +// Copyright 2011 Mark Cavage, Inc. All rights reserved. + +var test = require('tap').test; + +var asn1 = require('asn1'); + + +///--- Globals + +var BerReader = asn1.BerReader; +var BerWriter = asn1.BerWriter; +var SearchEntry; +var Attribute; +var dn; + +///--- Tests + +test('load library', function(t) { + SearchEntry = require('../../lib/index').SearchEntry; + Attribute = require('../../lib/index').Attribute; + dn = require('../../lib/index').dn; + t.ok(SearchEntry); + t.ok(dn); + t.ok(Attribute); + t.end(); +}); + + +test('new no args', function(t) { + t.ok(new SearchEntry()); + t.end(); +}); + + +test('new with args', function(t) { + var res = new SearchEntry({ + messageID: 123, + objectName: dn.parse('cn=foo, o=test'), + attributes: [new Attribute({type: 'cn', vals: ['foo']}), + new Attribute({type: 'objectclass', vals: ['person']})] + }); + t.ok(res); + t.equal(res.messageID, 123); + t.equal(res.dn, 'cn=foo, o=test'); + t.equal(res.attributes.length, 2); + t.equal(res.attributes[0].type, 'cn'); + t.equal(res.attributes[0].vals[0], 'foo'); + t.equal(res.attributes[1].type, 'objectclass'); + t.equal(res.attributes[1].vals[0], 'person'); + t.end(); +}); + + +test('parse', function(t) { + var ber = new BerWriter(); + ber.writeString('cn=foo, o=test'); + + ber.startSequence(); + + ber.startSequence(); + ber.writeString('cn'); + ber.startSequence(0x31); + ber.writeString('foo'); + ber.endSequence(); + ber.endSequence(); + + ber.startSequence(); + ber.writeString('objectclass'); + ber.startSequence(0x31); + ber.writeString('person'); + ber.endSequence(); + ber.endSequence(); + + ber.endSequence(); + + var res = new SearchEntry(); + t.ok(res._parse(new BerReader(ber.buffer))); + t.equal(res.dn, 'cn=foo, o=test'); + t.equal(res.attributes.length, 2); + t.equal(res.attributes[0].type, 'cn'); + t.equal(res.attributes[0].vals[0], 'foo'); + t.equal(res.attributes[1].type, 'objectclass'); + t.equal(res.attributes[1].vals[0], 'person'); + t.end(); +}); + + +test('toBer', function(t) { + var res = new SearchEntry({ + messageID: 123, + objectName: dn.parse('cn=foo, o=test'), + attributes: [new Attribute({type: 'cn', vals: ['foo']}), + new Attribute({type: 'objectclass', vals: ['person']})] + }); + t.ok(res); + + var ber = new BerReader(res.toBer()); + t.ok(ber); + t.equal(ber.readSequence(), 0x30); + t.equal(ber.readInt(), 123); + t.equal(ber.readSequence(), 0x64); + t.equal(ber.readString(), 'cn=foo, o=test'); + t.ok(ber.readSequence()); + + t.ok(ber.readSequence()); + t.equal(ber.readString(), 'cn'); + t.equal(ber.readSequence(), 0x31); + t.equal(ber.readString(), 'foo'); + + t.ok(ber.readSequence()); + t.equal(ber.readString(), 'objectclass'); + t.equal(ber.readSequence(), 0x31); + t.equal(ber.readString(), 'person'); + + t.end(); +}); diff --git a/tst/messages/search_request.test.js b/tst/messages/search_request.test.js new file mode 100644 index 0000000..d1fc8b5 --- /dev/null +++ b/tst/messages/search_request.test.js @@ -0,0 +1,120 @@ +// Copyright 2011 Mark Cavage, Inc. All rights reserved. + +var test = require('tap').test; + +var asn1 = require('asn1'); + + +///--- Globals + +var BerReader = asn1.BerReader; +var BerWriter = asn1.BerWriter; +var SearchRequest; +var EqualityFilter; +var dn; + +///--- Tests + +test('load library', function(t) { + SearchRequest = require('../../lib/index').SearchRequest; + EqualityFilter = require('../../lib/index').EqualityFilter; + dn = require('../../lib/index').dn; + t.ok(SearchRequest); + t.ok(EqualityFilter); + t.ok(dn); + t.end(); +}); + + +test('new no args', function(t) { + t.ok(new SearchRequest()); + t.end(); +}); + + +test('new with args', function(t) { + var req = new SearchRequest({ + baseObject: dn.parse('cn=foo, o=test'), + filter: new EqualityFilter({ + attribute: 'email', + value: 'foo@bar.com' + }), + attributes: ['cn', 'sn'] + }); + t.ok(req); + t.equal(req.dn.toString(), 'cn=foo, o=test'); + t.equal(req.filter.toString(), '(email=foo@bar.com)'); + t.equal(req.attributes.length, 2); + t.equal(req.attributes[0], 'cn'); + t.equal(req.attributes[1], 'sn'); + t.end(); +}); + + +test('parse', function(t) { + var f = new EqualityFilter({ + attribute: 'email', + value: 'foo@bar.com' + }); + + var ber = new BerWriter(); + ber.writeString('cn=foo,o=test'); + ber.writeEnumeration(0); + ber.writeEnumeration(0); + ber.writeInt(1); + ber.writeInt(2); + ber.writeBoolean(false); + ber = f.toBer(ber); + + var req = new SearchRequest(); + t.ok(req._parse(new BerReader(ber.buffer))); + t.equal(req.dn.toString(), 'cn=foo, o=test'); + t.equal(req.scope, 'base'); + t.equal(req.derefAliases, 0); + t.equal(req.sizeLimit, 1); + t.equal(req.timeLimit, 2); + t.equal(req.typesOnly, false); + t.equal(req.filter.toString(), '(email=foo@bar.com)'); + t.equal(req.attributes.length, 0); + t.end(); +}); + + +test('toBer', function(t) { + var req = new SearchRequest({ + messageID: 123, + baseObject: dn.parse('cn=foo, o=test'), + scope: 1, + derefAliases: 2, + sizeLimit: 10, + timeLimit: 20, + typesOnly: true, + filter: new EqualityFilter({ + attribute: 'email', + value: 'foo@bar.com' + }), + attributes: ['cn', 'sn'] + }); + + t.ok(req); + + var ber = new BerReader(req.toBer()); + t.ok(ber); + t.equal(ber.readSequence(), 0x30); + t.equal(ber.readInt(), 123); + t.equal(ber.readSequence(), 0x63); + t.equal(ber.readString(), 'cn=foo, o=test'); + t.equal(ber.readEnumeration(), 1); + t.equal(ber.readEnumeration(), 2); + t.equal(ber.readInt(), 10); + t.equal(ber.readInt(), 20); + t.ok(ber.readBoolean()); + t.equal(ber.readSequence(), 0xa3); + t.equal(ber.readString(), 'email'); + t.equal(ber.readString(), 'foo@bar.com'); + t.ok(ber.readSequence()); + t.equal(ber.readString(), 'cn'); + t.equal(ber.readString(), 'sn'); + + t.end(); +}); diff --git a/tst/messages/search_response.test.js b/tst/messages/search_response.test.js new file mode 100644 index 0000000..c6f17d0 --- /dev/null +++ b/tst/messages/search_response.test.js @@ -0,0 +1,76 @@ +// Copyright 2011 Mark Cavage, Inc. All rights reserved. + +var test = require('tap').test; + +var asn1 = require('asn1'); + + +///--- Globals + +var BerReader = asn1.BerReader; +var BerWriter = asn1.BerWriter; +var SearchResponse; + + +///--- Tests + +test('load library', function(t) { + SearchResponse = require('../../lib/index').SearchResponse; + t.ok(SearchResponse); + t.end(); +}); + + +test('new no args', function(t) { + t.ok(new SearchResponse()); + t.end(); +}); + + +test('new with args', function(t) { + var res = new SearchResponse({ + messageID: 123, + status: 0 + }); + t.ok(res); + t.equal(res.messageID, 123); + t.equal(res.status, 0); + t.end(); +}); + + +test('parse', function(t) { + var ber = new BerWriter(); + ber.writeEnumeration(0); + ber.writeString('cn=root'); + ber.writeString('foo'); + + var res = new SearchResponse(); + t.ok(res._parse(new BerReader(ber.buffer))); + t.equal(res.status, 0); + t.equal(res.matchedDN, 'cn=root'); + t.equal(res.errorMessage, 'foo'); + t.end(); +}); + + +test('toBer', function(t) { + var res = new SearchResponse({ + messageID: 123, + status: 3, + matchedDN: 'cn=root', + errorMessage: 'foo' + }); + t.ok(res); + + var ber = new BerReader(res.toBer()); + t.ok(ber); + t.equal(ber.readSequence(), 0x30); + t.equal(ber.readInt(), 123); + t.equal(ber.readSequence(), 0x65); + t.equal(ber.readEnumeration(), 3); + t.equal(ber.readString(), 'cn=root'); + t.equal(ber.readString(), 'foo'); + + t.end(); +}); diff --git a/tst/messages/unbind_request.test.js b/tst/messages/unbind_request.test.js new file mode 100644 index 0000000..759d0ab --- /dev/null +++ b/tst/messages/unbind_request.test.js @@ -0,0 +1,57 @@ +// Copyright 2011 Mark Cavage, Inc. All rights reserved. + +var test = require('tap').test; + +var asn1 = require('asn1'); + + +///--- Globals + +var BerReader = asn1.BerReader; +var BerWriter = asn1.BerWriter; +var UnbindRequest; + + +///--- Tests + +test('load library', function(t) { + UnbindRequest = require('../../lib/index').UnbindRequest; + t.ok(UnbindRequest); + t.end(); +}); + + +test('new no args', function(t) { + t.ok(new UnbindRequest()); + t.end(); +}); + + +test('new with args', function(t) { + var req = new UnbindRequest({}); + t.ok(req); + t.end(); +}); + + +test('parse', function(t) { + var ber = new BerWriter(); + + var req = new UnbindRequest(); + t.ok(req._parse(new BerReader(ber.buffer))); + t.end(); +}); + + +test('toBer', function(t) { + var req = new UnbindRequest({ + messageID: 123 + }); + t.ok(req); + + var ber = new BerReader(req.toBer()); + t.ok(ber); + t.equal(ber.readSequence(), 0x30); + t.equal(ber.readInt(), 123); + t.end(); +}); diff --git a/tst/url.test.js b/tst/url.test.js new file mode 100644 index 0000000..9ae8038 --- /dev/null +++ b/tst/url.test.js @@ -0,0 +1,73 @@ +// Copyright 2011 Mark Cavage, Inc. All rights reserved. + +var test = require('tap').test; + + + +///--- Globals + +var url; + + + +///--- Tests + +test('load library', function(t) { + url = require('../lib/index').url; + t.ok(url); + + t.end(); +}); + + +test('parse empty', function(t) { + var u = url.parse('ldap:///'); + t.equal(u.hostname, 'localhost'); + t.equal(u.port, 389); + t.ok(!u.DN); + t.ok(!u.attributes); + t.equal(u.secure, false); + t.end(); +}); + + +test('parse hostname', function(t) { + var u = url.parse('ldap://example.com/'); + t.equal(u.hostname, 'example.com'); + t.equal(u.port, 389); + t.ok(!u.DN); + t.ok(!u.attributes); + t.equal(u.secure, false); + t.end(); +}); + + +test('parse host and port', function(t) { + var u = url.parse('ldap://example.com:1389/'); + t.equal(u.hostname, 'example.com'); + t.equal(u.port, 1389); + t.ok(!u.DN); + t.ok(!u.attributes); + t.equal(u.secure, false); + t.end(); +}); + + +test('parse full', function(t) { + + var u = url.parse('ldaps://ldap.example.com:1389/dc=example%20,dc=com' + + '?cn,sn?sub?(cn=Babs%20Jensen)'); + + t.equal(u.secure, true); + t.equal(u.hostname, 'ldap.example.com'); + t.equal(u.port, 1389); + t.equal(u.DN, 'dc=example ,dc=com'); + t.ok(u.attributes); + t.equal(u.attributes.length, 2); + t.equal(u.attributes[0], 'cn'); + t.equal(u.attributes[1], 'sn'); + t.equal(u.scope, 'sub'); + t.equal(u.filter, '(cn=Babs Jensen)'); + + t.end(); +});