Skip to content
This repository was archived by the owner on May 14, 2024. It is now read-only.

Commit 821569c

Browse files
committed
Implement client-side StartTLS support
1 parent ec062d3 commit 821569c

File tree

3 files changed

+138
-40
lines changed

3 files changed

+138
-40
lines changed

lib/client/client.js

Lines changed: 133 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -355,7 +355,7 @@ function Client(options) {
355355
});
356356
}
357357

358-
this.socket = null;
358+
this._socket = null;
359359
this.connected = false;
360360
this.connect();
361361
}
@@ -792,6 +792,7 @@ Client.prototype.search = function search(base,
792792

793793
var self = this;
794794
var baseDN = ensureDN(base, this.strictDN);
795+
795796
function sendRequest(ctrls, emitter, cb) {
796797
var req = new SearchRequest({
797798
baseObject: baseDN,
@@ -834,7 +835,7 @@ Client.prototype.search = function search(base,
834835
pager.on('search', sendRequest);
835836
pager.begin();
836837
} else {
837-
sendRequest(controls, new EventEmitter, callback);
838+
sendRequest(controls, new EventEmitter(), callback);
838839
}
839840
};
840841

@@ -859,14 +860,106 @@ Client.prototype.unbind = function unbind(callback) {
859860
// user-initiated unbind or something else.
860861
this.unbound = true;
861862

862-
if (!this.socket)
863+
if (!this._socket)
863864
return callback();
864865

865866
var req = new UnbindRequest();
866867
return this._send(req, 'unbind', null, callback);
867868
};
868869

869870

871+
/**
872+
* Attempt to secure connection with StartTLS.
873+
*/
874+
Client.prototype.starttls = function starttls(options,
875+
controls,
876+
callback,
877+
_bypass) {
878+
assert.optionalObject(options);
879+
options = options || {};
880+
callback = once(callback);
881+
var self = this;
882+
883+
if (this._starttls) {
884+
return callback(new Error('STARTTLS already in progress or active'));
885+
}
886+
887+
function onSend(err, emitter) {
888+
if (err) {
889+
callback(err);
890+
return;
891+
}
892+
/*
893+
* Now that the request has been sent, block all outgoing messages
894+
* until an error is received or we successfully complete the setup.
895+
*/
896+
// TODO: block traffic
897+
self._starttls = {
898+
started: true
899+
};
900+
901+
emitter.on('error', function (err) {
902+
self._starttls = null;
903+
callback(err);
904+
});
905+
emitter.on('end', function (res) {
906+
var sock = self._socket;
907+
/*
908+
* Unplumb socket data during SSL negotiation.
909+
* This will prevent the LDAP parser from stumbling over the TLS
910+
* handshake and raising a ruckus.
911+
*/
912+
sock.removeAllListeners('data');
913+
914+
options.socket = sock;
915+
var secure = tls.connect(options);
916+
secure.once('secureConnect', function () {
917+
/*
918+
* Wire up 'data' and 'error' handlers like the normal socket.
919+
* Handling 'end' events isn't necessary since the underlying socket
920+
* will handle those.
921+
*/
922+
secure.removeAllListeners('error');
923+
secure.on('data', function onData(data) {
924+
if (self.log.trace())
925+
self.log.trace('data event: %s', util.inspect(data));
926+
927+
self._tracker.parser.write(data);
928+
});
929+
secure.on('error', function (err) {
930+
if (self.log.trace())
931+
self.log.trace({err: err}, 'error event: %s', new Error().stack);
932+
933+
self.emit('error', err);
934+
sock.destroy();
935+
});
936+
callback(null);
937+
});
938+
secure.once('error', function (err) {
939+
// If the SSL negotiation failed, to back to plain mode.
940+
self._starttls = null;
941+
secure.removeAllListeners();
942+
callback(err);
943+
});
944+
self._starttls.success = true;
945+
self._socket = secure;
946+
});
947+
}
948+
949+
var req = new ExtendedRequest({
950+
requestName: '1.3.6.1.4.1.1466.20037',
951+
requestValue: null,
952+
controls: controls
953+
});
954+
955+
return this._send(req,
956+
[errors.LDAP_SUCCESS],
957+
new EventEmitter(),
958+
onSend,
959+
_bypass);
960+
};
961+
962+
870963
/**
871964
* Disconnect from the LDAP server and do not allow reconnection.
872965
*
@@ -889,8 +982,8 @@ Client.prototype.destroy = function destroy(err) {
889982
});
890983
if (this.connected) {
891984
this.unbind();
892-
} else if (this.socket) {
893-
this.socket.destroy();
985+
} else if (this._socket) {
986+
this._socket.destroy();
894987
}
895988
this.emit('destroy', err);
896989
};
@@ -906,6 +999,7 @@ Client.prototype.connect = function connect() {
906999
var self = this;
9071000
var log = this.log;
9081001
var socket;
1002+
var tracker;
9091003

9101004
// Establish basic socket connection
9111005
function connectSocket(cb) {
@@ -930,8 +1024,8 @@ Client.prototype.connect = function connect() {
9301024
.removeAllListeners('connect')
9311025
.removeAllListeners('secureConnect');
9321026

933-
socket.ldap.id = nextClientId() + '__' + socket.ldap.id;
934-
self.log = self.log.child({ldap_id: socket.ldap.id}, true);
1027+
tracker.id = nextClientId() + '__' + tracker.id;
1028+
self.log = self.log.child({ldap_id: tracker.id}, true);
9351029

9361030
// Move on to client setup
9371031
setupClient(cb);
@@ -953,7 +1047,7 @@ Client.prototype.connect = function connect() {
9531047
self.connectTimer = setTimeout(function onConnectTimeout() {
9541048
if (!socket || !socket.readable || !socket.writeable) {
9551049
socket.destroy();
956-
self.socket = null;
1050+
self._socket = null;
9571051
onResult(new ConnectionError('connection timeout'));
9581052
}
9591053
}, self.connectTimeout);
@@ -962,7 +1056,7 @@ Client.prototype.connect = function connect() {
9621056

9631057
// Initialize socket events and LDAP parser.
9641058
function initSocket() {
965-
socket.ldap = new MessageTracker({
1059+
tracker = new MessageTracker({
9661060
id: self.url ? self.url.href : self.socketPath,
9671061
parser: new Parser({log: log})
9681062
});
@@ -979,13 +1073,13 @@ Client.prototype.connect = function connect() {
9791073
if (log.trace())
9801074
log.trace('data event: %s', util.inspect(data));
9811075

982-
socket.ldap.parser.write(data);
1076+
tracker.parser.write(data);
9831077
});
9841078

9851079
// The "router"
986-
socket.ldap.parser.on('message', function onMessage(message) {
987-
message.connection = socket;
988-
var callback = socket.ldap.fetch(message.messageID);
1080+
tracker.parser.on('message', function onMessage(message) {
1081+
message.connection = self._socket;
1082+
var callback = tracker.fetch(message.messageID);
9891083

9901084
if (!callback) {
9911085
log.error({message: message.json}, 'unsolicited message');
@@ -995,9 +1089,9 @@ Client.prototype.connect = function connect() {
9951089
return callback(message);
9961090
});
9971091

998-
socket.ldap.parser.on('error', function onParseError(err) {
1092+
tracker.parser.on('error', function onParseError(err) {
9991093
self.emit('error', new VError(err, 'Parser error for %s',
1000-
socket.ldap.id));
1094+
tracker.id));
10011095
self.connected = false;
10021096
socket.end();
10031097
});
@@ -1018,7 +1112,8 @@ Client.prototype.connect = function connect() {
10181112
socket.once('end', bail);
10191113
socket.once('timeout', bail);
10201114

1021-
self.socket = socket;
1115+
self._socket = socket;
1116+
self._tracker = tracker;
10221117

10231118
// Run any requested setup (such as automatically performing a bind) on
10241119
// socket before signalling successful connection.
@@ -1152,14 +1247,15 @@ Client.prototype._flushQueue = function _flushQueue() {
11521247
* Clean up socket/parser resources after socket close.
11531248
*/
11541249
Client.prototype._onClose = function _onClose(had_err) {
1155-
var socket = this.socket;
1250+
var socket = this._socket;
1251+
var tracker = this._tracker;
11561252
socket.removeAllListeners('connect')
11571253
.removeAllListeners('data')
11581254
.removeAllListeners('drain')
11591255
.removeAllListeners('end')
11601256
.removeAllListeners('error')
11611257
.removeAllListeners('timeout');
1162-
this.socket = null;
1258+
this._socket = null;
11631259
this.connected = false;
11641260

11651261
((socket.socket) ? socket.socket : socket).removeAllListeners('close');
@@ -1170,12 +1266,12 @@ Client.prototype._onClose = function _onClose(had_err) {
11701266
this.emit('close', had_err);
11711267
// On close we have to walk the outstanding messages and go invoke their
11721268
// callback with an error.
1173-
socket.ldap.pending.forEach(function (msgid) {
1174-
var cb = socket.ldap.fetch(msgid);
1175-
socket.ldap.remove(msgid);
1269+
tracker.pending.forEach(function (msgid) {
1270+
var cb = tracker.fetch(msgid);
1271+
tracker.remove(msgid);
11761272

11771273
if (socket.unbindMessageID !== parseInt(msgid, 10)) {
1178-
return cb(new ConnectionError(socket.ldap.id + ' closed'));
1274+
return cb(new ConnectionError(tracker.id + ' closed'));
11791275
} else {
11801276
// Unbinds will be communicated as a success since we're closed
11811277
var unbind = new UnbindResponse({messageID: msgid});
@@ -1184,8 +1280,9 @@ Client.prototype._onClose = function _onClose(had_err) {
11841280
}
11851281
});
11861282

1187-
delete socket.ldap.parser;
1188-
delete socket.ldap;
1283+
// Trash any parser or starttls state
1284+
this._tracker = null;
1285+
delete this._starttls;
11891286

11901287
// Automatically fire reconnect logic if the socket was closed for any reason
11911288
// other than a user-initiated unbind.
@@ -1212,8 +1309,8 @@ Client.prototype._updateIdle = function _updateIdle(override) {
12121309
var self = this;
12131310
function isIdle(disable) {
12141311
return ((disable !== true) &&
1215-
(self.socket && self.connected) &&
1216-
(self.socket.ldap.pending.length === 0));
1312+
(self._socket && self.connected) &&
1313+
(self._tracker.pending.length === 0));
12171314
}
12181315
if (isIdle(override)) {
12191316
if (!this._idleTimer) {
@@ -1242,14 +1339,14 @@ Client.prototype._send = function _send(message,
12421339
_bypass) {
12431340
assert.ok(message);
12441341
assert.ok(expect);
1245-
assert.ok(typeof (emitter) !== undefined);
1342+
assert.optionalObject(emitter);
12461343
assert.ok(callback);
12471344

12481345
// Allow connect setup traffic to bypass checks
1249-
if (_bypass && this.socket && this.socket.writable) {
1346+
if (_bypass && this._socket && this._socket.writable) {
12501347
return this._sendSocket(message, expect, emitter, callback);
12511348
}
1252-
if (!this.socket || !this.connected) {
1349+
if (!this._socket || !this.connected) {
12531350
if (!this.queue.enqueue(message, expect, emitter, callback)) {
12541351
callback(new ConnectionError('connection unavailable'));
12551352
}
@@ -1268,7 +1365,8 @@ Client.prototype._sendSocket = function _sendSocket(message,
12681365
expect,
12691366
emitter,
12701367
callback) {
1271-
var conn = this.socket;
1368+
var conn = this._socket;
1369+
var tracker = this._tracker;
12721370
var log = this.log;
12731371
var self = this;
12741372
var timer = false;
@@ -1313,7 +1411,7 @@ Client.prototype._sendSocket = function _sendSocket(message,
13131411
event = event[0].toLowerCase() + event.slice(1);
13141412
return _done(event, msg);
13151413
} else {
1316-
conn.ldap.remove(message.messageID);
1414+
tracker.remove(message.messageID);
13171415
// Potentially mark client as idle
13181416
self._updateIdle();
13191417

@@ -1332,7 +1430,7 @@ Client.prototype._sendSocket = function _sendSocket(message,
13321430

13331431
function onRequestTimeout() {
13341432
self.emit('timeout', message);
1335-
var cb = conn.ldap.fetch(message.messageID);
1433+
var cb = tracker.fetch(message.messageID);
13361434
if (cb) {
13371435
//FIXME: the timed-out request should be abandoned
13381436
cb(new errors.TimeoutError('request timeout (client interrupt)'));
@@ -1342,9 +1440,9 @@ Client.prototype._sendSocket = function _sendSocket(message,
13421440
function writeCallback() {
13431441
if (expect === 'abandon') {
13441442
// Mark the messageID specified as abandoned
1345-
conn.ldap.abandon(message.abandonID);
1443+
tracker.abandon(message.abandonID);
13461444
// No need to track the abandon request itself
1347-
conn.ldap.remove(message.id);
1445+
tracker.remove(message.id);
13481446
return callback(null);
13491447
} else if (expect === 'unbind') {
13501448
conn.unbindMessageID = message.id;
@@ -1363,7 +1461,7 @@ Client.prototype._sendSocket = function _sendSocket(message,
13631461
} // end writeCallback()
13641462

13651463
// Start actually doing something...
1366-
conn.ldap.track(message, messageCallback);
1464+
tracker.track(message, messageCallback);
13671465
// Mark client as active
13681466
this._updateIdle(true);
13691467

lib/messages/ext_request.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -73,9 +73,9 @@ ExtendedRequest.prototype._toBer = function (ber) {
7373
assert.ok(ber);
7474

7575
ber.writeString(this.requestName, 0x80);
76-
if (Buffer.isBuffer(this.requestValue))
76+
if (Buffer.isBuffer(this.requestValue)) {
7777
ber.writeBuffer(this.requestValue, 0x81);
78-
else {
78+
} else if (typeof (this.requestValue) === 'string') {
7979
ber.writeString(this.requestValue, 0x81);
8080
}
8181

test/client.test.js

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -987,7 +987,7 @@ test('setup reconnect', function (t) {
987987
// can't test had_err because the socket error is being faked
988988
cb();
989989
});
990-
rClient.socket.emit('error', new Error(msg));
990+
rClient._socket.emit('error', new Error(msg));
991991
},
992992
doSearch
993993
]
@@ -1081,15 +1081,15 @@ test('reconnect on server close', function (t) {
10811081
});
10821082
});
10831083
clt.once('connect', function () {
1084-
t.ok(clt.socket);
1084+
t.ok(clt._socket);
10851085
clt.once('connect', function () {
10861086
t.ok(true, 'successful reconnect');
10871087
clt.destroy();
10881088
t.end();
10891089
});
10901090

10911091
// Simulate server-side close
1092-
clt.socket.destroy();
1092+
clt._socket.destroy();
10931093
});
10941094
});
10951095

0 commit comments

Comments
 (0)