From 75aba691f1755a8eb587ab6f70095c3225af5bc2 Mon Sep 17 00:00:00 2001 From: Mark Cavage Date: Mon, 7 Nov 2011 14:13:53 -0800 Subject: [PATCH] GH-27 Support for abandon operations client side, and noop it server side --- lib/client.js | 123 +++++++++++++++++++++---------- lib/messages/abandon_request.js | 103 ++++++++++++++++++++++++++ lib/messages/abandon_response.js | 32 ++++++++ lib/messages/index.js | 4 + lib/messages/parser.js | 5 ++ lib/server.js | 23 +++++- package.json | 14 ++-- tst/client.test.js | 8 ++ 8 files changed, 263 insertions(+), 49 deletions(-) create mode 100644 lib/messages/abandon_request.js create mode 100644 lib/messages/abandon_response.js diff --git a/lib/client.js b/lib/client.js index 8945099..9b4273c 100644 --- a/lib/client.js +++ b/lib/client.js @@ -21,6 +21,7 @@ var url = require('./url'); ///--- Globals +var AbandonRequest = messages.AbandonRequest; var AddRequest = messages.AddRequest; var BindRequest = messages.BindRequest; var CompareRequest = messages.CompareRequest; @@ -189,6 +190,38 @@ Client.prototype.bind = function(name, credentials, controls, callback, conn) { }; +/** + * Sends an abandon request to the LDAP server. + * + * The callback will be invoked as soon as the data is flushed out to the + * network, as there is never a response from abandon. + * + * @param {Number} messageID the messageID to abandon. + * @param {Control} controls (optional) either a Control or [Control]. + * @param {Function} callback of the form f(err). + * @throws {TypeError} on invalid input. + */ +Client.prototype.abandon = function(messageID, controls, callback) { + if (typeof(messageID) !== 'number') + throw new TypeError('messageID (number) 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 AbandonRequest({ + abandonID: messageID, + controls: controls + }); + + return this._send(req, 'abandon', callback); +}; + + /** * Adds an entry to the LDAP server. * @@ -583,49 +616,51 @@ Client.prototype._send = function(message, expect, callback, connection) { // 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); + if (expect !== 'abandon') { + 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; + var err = null; - if (res instanceof LDAPResult) { - delete conn.ldap.messages[message.messageID]; + if (res instanceof LDAPResult) { + delete conn.ldap.messages[message.messageID]; - if (expect.indexOf(res.status) === -1) { - err = errors.getError(res); + 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 if (res instanceof SearchReference) { + assert.ok(callback instanceof EventEmitter); + callback.emit('searchReference', res); + + } else if (res instanceof Error) { + return callback(res); + + } else { + delete conn.ldap.messages[message.messageID]; + + err = new errors.ProtocolError(res.type); if (typeof(callback) === 'function') return callback(err); - return callback.emit('error', err); + 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 if (res instanceof SearchReference) { - assert.ok(callback instanceof EventEmitter); - callback.emit('searchReference', res); - - } else if (res instanceof Error) { - return callback(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()) @@ -633,9 +668,19 @@ Client.prototype._send = function(message, expect, callback, connection) { // 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.end(); - } : null)); + var _writeCb = null; + if (expect === 'abandon') { + _writeCb = function() { + return callback(); + }; + } else if (expect === 'unbind') { + _writeCb = function() { + conn.end(); + }; + } else { + // noop + } + return conn.write(message.toBer(), _writeCb); }; diff --git a/lib/messages/abandon_request.js b/lib/messages/abandon_request.js new file mode 100644 index 0000000..b044b2a --- /dev/null +++ b/lib/messages/abandon_request.js @@ -0,0 +1,103 @@ +// 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 AbandonRequest(options) { + if (options) { + if (typeof(options) !== 'object') + throw new TypeError('options must be an object'); + if (options.abandonID && typeof(options.abandonID) !== 'number') + throw new TypeError('abandonID must be a number'); + } else { + options = {}; + } + + options.protocolOp = Protocol.LDAP_REQ_ABANDON; + LDAPMessage.call(this, options); + + this.abandonID = options.abandonID || 0; + + var self = this; + this.__defineGetter__('type', function() { return 'AbandonRequest'; }); +} +util.inherits(AbandonRequest, LDAPMessage); +module.exports = AbandonRequest; + + +AbandonRequest.prototype._parse = function(ber, length) { + assert.ok(ber); + assert.ok(length); + + // What a PITA - have to replicate ASN.1 integer logic to work around the + // way abandon is encoded and the way ldapjs framework handles "normal" + // messages + + var buf = ber.buffer; + var offset = 0; + var value = 0; + + var fb = buf[offset++]; + value = fb & 0x7F; + for (var i = 1; i < length; i++) { + value <<= 8; + value |= (buf[offset++] & 0xff); + } + if ((fb & 0x80) == 0x80) + value = -value; + + ber._offset += length; + + this.abandonID = value; + + return true; +}; + + +AbandonRequest.prototype._toBer = function(ber) { + assert.ok(ber); + + var i = this.abandonID; + var sz = 4; + + while ((((i & 0xff800000) == 0) || ((i & 0xff800000) == 0xff800000)) && + (sz > 1)) { + sz--; + i <<= 8; + } + assert.ok(sz <= 4); + + while (sz-- > 0) { + ber.writeByte((i & 0xff000000) >> 24); + i <<= 8; + } + + return ber; +}; + + +AbandonRequest.prototype._json = function(j) { + assert.ok(j); + + j.abandonID = this.abandonID; + + return j; +}; + diff --git a/lib/messages/abandon_response.js b/lib/messages/abandon_response.js new file mode 100644 index 0000000..c390bf5 --- /dev/null +++ b/lib/messages/abandon_response.js @@ -0,0 +1,32 @@ +// 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 +// Stub this out + +function AbandonResponse(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 'AbandonResponse'; }); +} +util.inherits(AbandonResponse, LDAPMessage); +module.exports = AbandonResponse; + + +AbandonResponse.prototype.end = function(status) {}; + + +AbandonResponse.prototype._json = function(j) { + return j; +}; diff --git a/lib/messages/index.js b/lib/messages/index.js index 4449d31..899ee6f 100644 --- a/lib/messages/index.js +++ b/lib/messages/index.js @@ -4,6 +4,8 @@ var LDAPMessage = require('./message'); var LDAPResult = require('./result'); var Parser = require('./parser'); +var AbandonRequest = require('./abandon_request'); +var AbandonResponse = require('./abandon_response'); var AddRequest = require('./add_request'); var AddResponse = require('./add_response'); var BindRequest = require('./bind_request'); @@ -35,6 +37,8 @@ module.exports = { LDAPResult: LDAPResult, Parser: Parser, + AbandonRequest: AbandonRequest, + AbandonResponse: AbandonResponse, AddRequest: AddRequest, AddResponse: AddResponse, BindRequest: BindRequest, diff --git a/lib/messages/parser.js b/lib/messages/parser.js index 172400f..39b2854 100644 --- a/lib/messages/parser.js +++ b/lib/messages/parser.js @@ -6,6 +6,7 @@ var util = require('util'); var asn1 = require('asn1'); +var AbandonRequest = require('./abandon_request'); var AddRequest = require('./add_request'); var AddResponse = require('./add_response'); var BindRequest = require('./bind_request'); @@ -148,6 +149,10 @@ Parser.prototype._newMessage = function(ber) { var Message; switch (type) { + case Protocol.LDAP_REQ_ABANDON: + Message = AbandonRequest; + break; + case Protocol.LDAP_REQ_ADD: Message = AddRequest; break; diff --git a/lib/server.js b/lib/server.js index 3367c53..1bf59ab 100644 --- a/lib/server.js +++ b/lib/server.js @@ -16,6 +16,7 @@ var Protocol = require('./protocol'); var logStub = require('./log_stub'); var Parser = require('./messages').Parser; +var AbandonResponse = require('./messages/abandon_response'); var AddResponse = require('./messages/add_response'); var BindResponse = require('./messages/bind_response'); var CompareResponse = require('./messages/compare_response'); @@ -79,7 +80,8 @@ function getResponse(req) { Response = BindResponse; break; case Protocol.LDAP_REQ_ABANDON: - return; // Noop + Response = AbandonResponse; + break; case Protocol.LDAP_REQ_ADD: Response = AddResponse; break; @@ -133,6 +135,16 @@ function defaultHandler(req, res, next) { } +function defaultAbandonHandler(req, res, next) { + assert.ok(req); + assert.ok(res); + assert.ok(next); + + res.end(); + return next(); +} + + function defaultUnbindHandler(req, res, next) { assert.ok(req); assert.ok(res); @@ -333,7 +345,6 @@ function Server(options) { res.requestDN = req.dn; var chain = self._getHandlerChain(req); - //.concat(this._postChain || []); var i = 0; return function(err) { @@ -650,6 +661,7 @@ Server.prototype.after = function() { }); }; + // All these just reexpose the requisite net.Server APIs Server.prototype.listen = function(port, host, callback) { if (!port) @@ -736,7 +748,7 @@ Server.prototype._getHandlerChain = function(req) { for (var r in routes) { if (routes.hasOwnProperty(r)) { var route = routes[r]; - // Special cases are exops and unbinds, handle those first. + // Special cases are abandons, exops and unbinds, handle those first. if (req.protocolOp === Protocol.LDAP_REQ_EXTENSION) { if (r !== req.requestName) continue; @@ -758,6 +770,11 @@ Server.prototype._getHandlerChain = function(req) { backend: routes['unbind'] ? routes['unbind'].backend : self, handlers: getUnbindChain() }; + } else if (req.protocolOp === Protocol.LDAP_REQ_ABANDON) { + return { + backend: self, + handlers: [defaultAbandonHandler] + }; } if (!route[op]) diff --git a/package.json b/package.json index 0a466b8..634c10b 100644 --- a/package.json +++ b/package.json @@ -17,15 +17,15 @@ "node": ">=0.4" }, "dependencies": { - "asn1": "~0.1.7", - "buffertools": "~1.0.3", - "dtrace-provider": "~0.0.3", - "nopt": "~1.0.10", - "sprintf": "~0.1.1" + "asn1": "0.1.8", + "buffertools": "1.0.5", + "dtrace-provider": "0.0.3", + "nopt": "1.0.10", + "sprintf": "0.1.1" }, "devDependencies": { - "tap": "~0.0.9", - "node-uuid": "~1.2.0" + "tap": "0.0.12", + "node-uuid": "1.2.0" }, "scripts": { "test": "./node_modules/.bin/tap ./tst" diff --git a/tst/client.test.js b/tst/client.test.js index 77380a3..c1de299 100644 --- a/tst/client.test.js +++ b/tst/client.test.js @@ -541,6 +541,14 @@ test('GH-24 attribute selection of *', function(t) { }); +test('abandon (GH-27)', function(t) { + client.abandon(401876543, function(err) { + t.ifError(err); + t.end(); + }); +}); + + test('shutdown', function(t) { client.unbind(function() { server.on('close', function() {