GH-27 Support for abandon operations client side, and noop it server side

This commit is contained in:
Mark Cavage 2011-11-07 14:13:53 -08:00
parent 060bb2c55b
commit 75aba691f1
8 changed files with 263 additions and 49 deletions

View File

@ -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);
};

View File

@ -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;
};

View File

@ -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;
};

View File

@ -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,

View File

@ -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;

View File

@ -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])

View File

@ -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"

View File

@ -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() {