Implement client-side StartTLS support

This commit is contained in:
Patrick Mooney 2015-04-25 22:36:36 -05:00
parent ec062d370c
commit 821569c2c4
3 changed files with 138 additions and 40 deletions

View File

@ -355,7 +355,7 @@ function Client(options) {
}); });
} }
this.socket = null; this._socket = null;
this.connected = false; this.connected = false;
this.connect(); this.connect();
} }
@ -792,6 +792,7 @@ Client.prototype.search = function search(base,
var self = this; var self = this;
var baseDN = ensureDN(base, this.strictDN); var baseDN = ensureDN(base, this.strictDN);
function sendRequest(ctrls, emitter, cb) { function sendRequest(ctrls, emitter, cb) {
var req = new SearchRequest({ var req = new SearchRequest({
baseObject: baseDN, baseObject: baseDN,
@ -834,7 +835,7 @@ Client.prototype.search = function search(base,
pager.on('search', sendRequest); pager.on('search', sendRequest);
pager.begin(); pager.begin();
} else { } else {
sendRequest(controls, new EventEmitter, callback); sendRequest(controls, new EventEmitter(), callback);
} }
}; };
@ -859,7 +860,7 @@ Client.prototype.unbind = function unbind(callback) {
// user-initiated unbind or something else. // user-initiated unbind or something else.
this.unbound = true; this.unbound = true;
if (!this.socket) if (!this._socket)
return callback(); return callback();
var req = new UnbindRequest(); var req = new UnbindRequest();
@ -867,6 +868,98 @@ Client.prototype.unbind = function unbind(callback) {
}; };
/**
* Attempt to secure connection with StartTLS.
*/
Client.prototype.starttls = function starttls(options,
controls,
callback,
_bypass) {
assert.optionalObject(options);
options = options || {};
callback = once(callback);
var self = this;
if (this._starttls) {
return callback(new Error('STARTTLS already in progress or active'));
}
function onSend(err, emitter) {
if (err) {
callback(err);
return;
}
/*
* Now that the request has been sent, block all outgoing messages
* until an error is received or we successfully complete the setup.
*/
// TODO: block traffic
self._starttls = {
started: true
};
emitter.on('error', function (err) {
self._starttls = null;
callback(err);
});
emitter.on('end', function (res) {
var sock = self._socket;
/*
* Unplumb socket data during SSL negotiation.
* This will prevent the LDAP parser from stumbling over the TLS
* handshake and raising a ruckus.
*/
sock.removeAllListeners('data');
options.socket = sock;
var secure = tls.connect(options);
secure.once('secureConnect', function () {
/*
* Wire up 'data' and 'error' handlers like the normal socket.
* Handling 'end' events isn't necessary since the underlying socket
* will handle those.
*/
secure.removeAllListeners('error');
secure.on('data', function onData(data) {
if (self.log.trace())
self.log.trace('data event: %s', util.inspect(data));
self._tracker.parser.write(data);
});
secure.on('error', function (err) {
if (self.log.trace())
self.log.trace({err: err}, 'error event: %s', new Error().stack);
self.emit('error', err);
sock.destroy();
});
callback(null);
});
secure.once('error', function (err) {
// If the SSL negotiation failed, to back to plain mode.
self._starttls = null;
secure.removeAllListeners();
callback(err);
});
self._starttls.success = true;
self._socket = secure;
});
}
var req = new ExtendedRequest({
requestName: '1.3.6.1.4.1.1466.20037',
requestValue: null,
controls: controls
});
return this._send(req,
[errors.LDAP_SUCCESS],
new EventEmitter(),
onSend,
_bypass);
};
/** /**
* Disconnect from the LDAP server and do not allow reconnection. * Disconnect from the LDAP server and do not allow reconnection.
* *
@ -889,8 +982,8 @@ Client.prototype.destroy = function destroy(err) {
}); });
if (this.connected) { if (this.connected) {
this.unbind(); this.unbind();
} else if (this.socket) { } else if (this._socket) {
this.socket.destroy(); this._socket.destroy();
} }
this.emit('destroy', err); this.emit('destroy', err);
}; };
@ -906,6 +999,7 @@ Client.prototype.connect = function connect() {
var self = this; var self = this;
var log = this.log; var log = this.log;
var socket; var socket;
var tracker;
// Establish basic socket connection // Establish basic socket connection
function connectSocket(cb) { function connectSocket(cb) {
@ -930,8 +1024,8 @@ Client.prototype.connect = function connect() {
.removeAllListeners('connect') .removeAllListeners('connect')
.removeAllListeners('secureConnect'); .removeAllListeners('secureConnect');
socket.ldap.id = nextClientId() + '__' + socket.ldap.id; tracker.id = nextClientId() + '__' + tracker.id;
self.log = self.log.child({ldap_id: socket.ldap.id}, true); self.log = self.log.child({ldap_id: tracker.id}, true);
// Move on to client setup // Move on to client setup
setupClient(cb); setupClient(cb);
@ -953,7 +1047,7 @@ Client.prototype.connect = function connect() {
self.connectTimer = setTimeout(function onConnectTimeout() { self.connectTimer = setTimeout(function onConnectTimeout() {
if (!socket || !socket.readable || !socket.writeable) { if (!socket || !socket.readable || !socket.writeable) {
socket.destroy(); socket.destroy();
self.socket = null; self._socket = null;
onResult(new ConnectionError('connection timeout')); onResult(new ConnectionError('connection timeout'));
} }
}, self.connectTimeout); }, self.connectTimeout);
@ -962,7 +1056,7 @@ Client.prototype.connect = function connect() {
// Initialize socket events and LDAP parser. // Initialize socket events and LDAP parser.
function initSocket() { function initSocket() {
socket.ldap = new MessageTracker({ tracker = new MessageTracker({
id: self.url ? self.url.href : self.socketPath, id: self.url ? self.url.href : self.socketPath,
parser: new Parser({log: log}) parser: new Parser({log: log})
}); });
@ -979,13 +1073,13 @@ Client.prototype.connect = function connect() {
if (log.trace()) if (log.trace())
log.trace('data event: %s', util.inspect(data)); log.trace('data event: %s', util.inspect(data));
socket.ldap.parser.write(data); tracker.parser.write(data);
}); });
// The "router" // The "router"
socket.ldap.parser.on('message', function onMessage(message) { tracker.parser.on('message', function onMessage(message) {
message.connection = socket; message.connection = self._socket;
var callback = socket.ldap.fetch(message.messageID); var callback = tracker.fetch(message.messageID);
if (!callback) { if (!callback) {
log.error({message: message.json}, 'unsolicited message'); log.error({message: message.json}, 'unsolicited message');
@ -995,9 +1089,9 @@ Client.prototype.connect = function connect() {
return callback(message); return callback(message);
}); });
socket.ldap.parser.on('error', function onParseError(err) { tracker.parser.on('error', function onParseError(err) {
self.emit('error', new VError(err, 'Parser error for %s', self.emit('error', new VError(err, 'Parser error for %s',
socket.ldap.id)); tracker.id));
self.connected = false; self.connected = false;
socket.end(); socket.end();
}); });
@ -1018,7 +1112,8 @@ Client.prototype.connect = function connect() {
socket.once('end', bail); socket.once('end', bail);
socket.once('timeout', bail); socket.once('timeout', bail);
self.socket = socket; self._socket = socket;
self._tracker = tracker;
// Run any requested setup (such as automatically performing a bind) on // Run any requested setup (such as automatically performing a bind) on
// socket before signalling successful connection. // socket before signalling successful connection.
@ -1152,14 +1247,15 @@ Client.prototype._flushQueue = function _flushQueue() {
* Clean up socket/parser resources after socket close. * Clean up socket/parser resources after socket close.
*/ */
Client.prototype._onClose = function _onClose(had_err) { Client.prototype._onClose = function _onClose(had_err) {
var socket = this.socket; var socket = this._socket;
var tracker = this._tracker;
socket.removeAllListeners('connect') socket.removeAllListeners('connect')
.removeAllListeners('data') .removeAllListeners('data')
.removeAllListeners('drain') .removeAllListeners('drain')
.removeAllListeners('end') .removeAllListeners('end')
.removeAllListeners('error') .removeAllListeners('error')
.removeAllListeners('timeout'); .removeAllListeners('timeout');
this.socket = null; this._socket = null;
this.connected = false; this.connected = false;
((socket.socket) ? socket.socket : socket).removeAllListeners('close'); ((socket.socket) ? socket.socket : socket).removeAllListeners('close');
@ -1170,12 +1266,12 @@ Client.prototype._onClose = function _onClose(had_err) {
this.emit('close', had_err); this.emit('close', had_err);
// On close we have to walk the outstanding messages and go invoke their // On close we have to walk the outstanding messages and go invoke their
// callback with an error. // callback with an error.
socket.ldap.pending.forEach(function (msgid) { tracker.pending.forEach(function (msgid) {
var cb = socket.ldap.fetch(msgid); var cb = tracker.fetch(msgid);
socket.ldap.remove(msgid); tracker.remove(msgid);
if (socket.unbindMessageID !== parseInt(msgid, 10)) { if (socket.unbindMessageID !== parseInt(msgid, 10)) {
return cb(new ConnectionError(socket.ldap.id + ' closed')); return cb(new ConnectionError(tracker.id + ' closed'));
} else { } else {
// Unbinds will be communicated as a success since we're closed // Unbinds will be communicated as a success since we're closed
var unbind = new UnbindResponse({messageID: msgid}); var unbind = new UnbindResponse({messageID: msgid});
@ -1184,8 +1280,9 @@ Client.prototype._onClose = function _onClose(had_err) {
} }
}); });
delete socket.ldap.parser; // Trash any parser or starttls state
delete socket.ldap; this._tracker = null;
delete this._starttls;
// Automatically fire reconnect logic if the socket was closed for any reason // Automatically fire reconnect logic if the socket was closed for any reason
// other than a user-initiated unbind. // other than a user-initiated unbind.
@ -1212,8 +1309,8 @@ Client.prototype._updateIdle = function _updateIdle(override) {
var self = this; var self = this;
function isIdle(disable) { function isIdle(disable) {
return ((disable !== true) && return ((disable !== true) &&
(self.socket && self.connected) && (self._socket && self.connected) &&
(self.socket.ldap.pending.length === 0)); (self._tracker.pending.length === 0));
} }
if (isIdle(override)) { if (isIdle(override)) {
if (!this._idleTimer) { if (!this._idleTimer) {
@ -1242,14 +1339,14 @@ Client.prototype._send = function _send(message,
_bypass) { _bypass) {
assert.ok(message); assert.ok(message);
assert.ok(expect); assert.ok(expect);
assert.ok(typeof (emitter) !== undefined); assert.optionalObject(emitter);
assert.ok(callback); assert.ok(callback);
// Allow connect setup traffic to bypass checks // Allow connect setup traffic to bypass checks
if (_bypass && this.socket && this.socket.writable) { if (_bypass && this._socket && this._socket.writable) {
return this._sendSocket(message, expect, emitter, callback); return this._sendSocket(message, expect, emitter, callback);
} }
if (!this.socket || !this.connected) { if (!this._socket || !this.connected) {
if (!this.queue.enqueue(message, expect, emitter, callback)) { if (!this.queue.enqueue(message, expect, emitter, callback)) {
callback(new ConnectionError('connection unavailable')); callback(new ConnectionError('connection unavailable'));
} }
@ -1268,7 +1365,8 @@ Client.prototype._sendSocket = function _sendSocket(message,
expect, expect,
emitter, emitter,
callback) { callback) {
var conn = this.socket; var conn = this._socket;
var tracker = this._tracker;
var log = this.log; var log = this.log;
var self = this; var self = this;
var timer = false; var timer = false;
@ -1313,7 +1411,7 @@ Client.prototype._sendSocket = function _sendSocket(message,
event = event[0].toLowerCase() + event.slice(1); event = event[0].toLowerCase() + event.slice(1);
return _done(event, msg); return _done(event, msg);
} else { } else {
conn.ldap.remove(message.messageID); tracker.remove(message.messageID);
// Potentially mark client as idle // Potentially mark client as idle
self._updateIdle(); self._updateIdle();
@ -1332,7 +1430,7 @@ Client.prototype._sendSocket = function _sendSocket(message,
function onRequestTimeout() { function onRequestTimeout() {
self.emit('timeout', message); self.emit('timeout', message);
var cb = conn.ldap.fetch(message.messageID); var cb = tracker.fetch(message.messageID);
if (cb) { if (cb) {
//FIXME: the timed-out request should be abandoned //FIXME: the timed-out request should be abandoned
cb(new errors.TimeoutError('request timeout (client interrupt)')); cb(new errors.TimeoutError('request timeout (client interrupt)'));
@ -1342,9 +1440,9 @@ Client.prototype._sendSocket = function _sendSocket(message,
function writeCallback() { function writeCallback() {
if (expect === 'abandon') { if (expect === 'abandon') {
// Mark the messageID specified as abandoned // Mark the messageID specified as abandoned
conn.ldap.abandon(message.abandonID); tracker.abandon(message.abandonID);
// No need to track the abandon request itself // No need to track the abandon request itself
conn.ldap.remove(message.id); tracker.remove(message.id);
return callback(null); return callback(null);
} else if (expect === 'unbind') { } else if (expect === 'unbind') {
conn.unbindMessageID = message.id; conn.unbindMessageID = message.id;
@ -1363,7 +1461,7 @@ Client.prototype._sendSocket = function _sendSocket(message,
} // end writeCallback() } // end writeCallback()
// Start actually doing something... // Start actually doing something...
conn.ldap.track(message, messageCallback); tracker.track(message, messageCallback);
// Mark client as active // Mark client as active
this._updateIdle(true); this._updateIdle(true);

View File

@ -73,9 +73,9 @@ ExtendedRequest.prototype._toBer = function (ber) {
assert.ok(ber); assert.ok(ber);
ber.writeString(this.requestName, 0x80); ber.writeString(this.requestName, 0x80);
if (Buffer.isBuffer(this.requestValue)) if (Buffer.isBuffer(this.requestValue)) {
ber.writeBuffer(this.requestValue, 0x81); ber.writeBuffer(this.requestValue, 0x81);
else { } else if (typeof (this.requestValue) === 'string') {
ber.writeString(this.requestValue, 0x81); ber.writeString(this.requestValue, 0x81);
} }

View File

@ -987,7 +987,7 @@ test('setup reconnect', function (t) {
// can't test had_err because the socket error is being faked // can't test had_err because the socket error is being faked
cb(); cb();
}); });
rClient.socket.emit('error', new Error(msg)); rClient._socket.emit('error', new Error(msg));
}, },
doSearch doSearch
] ]
@ -1081,7 +1081,7 @@ test('reconnect on server close', function (t) {
}); });
}); });
clt.once('connect', function () { clt.once('connect', function () {
t.ok(clt.socket); t.ok(clt._socket);
clt.once('connect', function () { clt.once('connect', function () {
t.ok(true, 'successful reconnect'); t.ok(true, 'successful reconnect');
clt.destroy(); clt.destroy();
@ -1089,7 +1089,7 @@ test('reconnect on server close', function (t) {
}); });
// Simulate server-side close // Simulate server-side close
clt.socket.destroy(); clt._socket.destroy();
}); });
}); });