forked from nodejs/node
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add SecurePair for handling of a ssl/tls stream.
- Loading branch information
Showing
2 changed files
with
333 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,330 @@ | ||
var util = require('util'); | ||
var events = require('events'); | ||
var stream = require('stream'); | ||
var assert = process.assert; | ||
|
||
var debugLevel = parseInt(process.env.NODE_DEBUG, 16); | ||
|
||
function debug () { | ||
if (debugLevel & 0x2) { | ||
util.error.apply(this, arguments); | ||
} | ||
} | ||
|
||
/* Lazy Loaded crypto object */ | ||
var SecureStream = null; | ||
|
||
/** | ||
* Provides a pair of streams to do encrypted communication. | ||
*/ | ||
|
||
function SecurePair(credentials, is_server) | ||
{ | ||
if (!(this instanceof SecurePair)) { | ||
return new SecurePair(credentials, is_server); | ||
} | ||
|
||
var self = this; | ||
|
||
try { | ||
SecureStream = process.binding('crypto').SecureStream; | ||
} | ||
catch (e) { | ||
throw new Error('node.js not compiled with openssl crypto support.'); | ||
} | ||
|
||
events.EventEmitter.call(this); | ||
|
||
this._secureEstablished = false; | ||
this._is_server = is_server ? true : false; | ||
this._enc_write_state = true; | ||
this._clear_write_state = true; | ||
this._done = false; | ||
|
||
var crypto = require("crypto"); | ||
|
||
if (!credentials) { | ||
this.credentials = crypto.createCredentials(); | ||
} | ||
else { | ||
this.credentials = credentials; | ||
} | ||
|
||
if (!this._is_server) { | ||
/* For clients, we will always have either a given ca list or be using default one */ | ||
this.credentials.shouldVerify = true; | ||
} | ||
|
||
this._secureEstablished = false; | ||
this._encIn_pending = []; | ||
this._clearIn_pending = []; | ||
|
||
this._ssl = new SecureStream(this.credentials.context, | ||
this._is_server ? true : false, | ||
this.credentials.shouldVerify); | ||
|
||
|
||
/* Acts as a r/w stream to the cleartext side of the stream. */ | ||
this.cleartext = new stream.Stream(); | ||
|
||
/* Acts as a r/w stream to the encrypted side of the stream. */ | ||
this.encrypted = new stream.Stream(); | ||
|
||
this.cleartext.write = function(data) { | ||
debug('clearIn data'); | ||
self._clearIn_pending.push(data); | ||
self._cycle(); | ||
return self._cleartext_write_state; | ||
}; | ||
|
||
this.cleartext.pause = function() { | ||
self._cleartext_write_state = false; | ||
}; | ||
|
||
this.cleartext.resume = function() { | ||
self._cleartext_write_state = true; | ||
}; | ||
|
||
this.cleartext.end = function(err) { | ||
debug('cleartext end'); | ||
if (!self._done) { | ||
self._ssl.shutdown(); | ||
self._cycle(); | ||
} | ||
self._destroy(err); | ||
}; | ||
|
||
this.encrypted.write = function(data) { | ||
debug('encIn data'); | ||
self._encIn_pending.push(data); | ||
self._cycle(); | ||
return self._encrypted_write_state; | ||
}; | ||
|
||
this.encrypted.pause = function() { | ||
self._encrypted_write_state = false; | ||
}; | ||
|
||
this.encrypted.resume = function() { | ||
self._encrypted_write_state = true; | ||
}; | ||
|
||
this.encrypted.end = function(err) { | ||
debug('encrypted end'); | ||
if (!self._done) { | ||
self._ssl.shutdown(); | ||
self._cycle(); | ||
} | ||
self._destroy(err); | ||
}; | ||
|
||
this.cleartext.on('end', function(err) { | ||
debug('clearIn end'); | ||
if (!self._done) { | ||
self._ssl.shutdown(); | ||
self._cycle(); | ||
} | ||
self._destroy(err); | ||
}); | ||
|
||
this.cleartext.on('close', function() { | ||
debug('source close'); | ||
self.emit('close'); | ||
self._destroy(); | ||
}); | ||
|
||
this.encrypted.on('end', function() { | ||
if (!self._done) { | ||
self._error(new Error('Encrypted stream ended before secure pair was done')); | ||
} | ||
}); | ||
|
||
this.encrypted.on('close', function() { | ||
if (!self._done) { | ||
self._error(new Error('Encrypted stream closed before secure pair was done')); | ||
} | ||
}); | ||
|
||
this.cleartext.on('drain', function() { | ||
debug('source drain'); | ||
self._cycle(); | ||
self.encrypted.resume(); | ||
}); | ||
|
||
this.encrypted.on('drain', function() { | ||
debug('target drain'); | ||
self._cycle(); | ||
self.cleartext.resume(); | ||
}); | ||
|
||
process.nextTick(function() { | ||
self._ssl.start(); | ||
self._cycle(); | ||
}); | ||
} | ||
|
||
util.inherits(SecurePair, events.EventEmitter); | ||
|
||
exports.createSecurePair = function(credentials, is_server) | ||
{ | ||
var pair = new SecurePair(credentials, is_server); | ||
return pair; | ||
}; | ||
|
||
/** | ||
* Attempt to cycle OpenSSLs buffers in various directions. | ||
* | ||
* An SSL Connection can be viewed as four separate piplines, | ||
* interacting with one has no connection to the behavoir of | ||
* any of the other 3 -- This might not sound reasonable, | ||
* but consider things like mid-stream renegotiation of | ||
* the ciphers. | ||
* | ||
* The four pipelines, using terminology of the client (server is just reversed): | ||
* 1) Encrypted Output stream (Writing encrypted data to peer) | ||
* 2) Encrypted Input stream (Reading encrypted data from peer) | ||
* 3) Cleartext Output stream (Decrypted content from the peer) | ||
* 4) Cleartext Input stream (Cleartext content to send to the peer) | ||
* | ||
* This function attempts to pull any available data out of the Cleartext | ||
* input stream (#4), and the Encrypted input stream (#2). Then it pushes | ||
* any data available from the cleartext output stream (#3), and finally | ||
* from the Encrypted output stream (#1) | ||
* | ||
* It is called whenever we do something with OpenSSL -- post reciving content, | ||
* trying to flush, trying to change ciphers, or shutting down the connection. | ||
* | ||
* Because it is also called everywhere, we also check if the connection | ||
* has completed negotiation and emit 'secure' from here if it has. | ||
*/ | ||
SecurePair.prototype._cycle = function() { | ||
if (this._done) { | ||
return; | ||
} | ||
|
||
var self = this; | ||
var rv; | ||
var tmp; | ||
var bytesRead; | ||
var bytesWritten; | ||
var chunkBytes; | ||
var chunk = null; | ||
var pool = null; | ||
|
||
while (this._encIn_pending.length > 0) { | ||
tmp = this._encIn_pending.shift(); | ||
|
||
try { | ||
rv = this._ssl.encIn(tmp, 0, tmp.length); | ||
} catch (e) { | ||
return this._error(e); | ||
} | ||
|
||
if (rv === 0) { | ||
this._encIn_pending.unshift(tmp); | ||
break; | ||
} | ||
|
||
assert(rv === tmp.length); | ||
} | ||
|
||
while (this._clearIn_pending.length > 0) { | ||
tmp = this._clearIn_pending.shift(); | ||
try { | ||
rv = this._ssl.clearIn(tmp, 0, tmp.length); | ||
} catch (e) { | ||
return this._error(e); | ||
} | ||
|
||
if (rv === 0) { | ||
this._clearIn_pending.unshift(tmp); | ||
break; | ||
} | ||
|
||
assert(rv === tmp.length); | ||
} | ||
|
||
function mover(reader, writer, checker) { | ||
var bytesRead; | ||
var pool; | ||
var chunkBytes; | ||
do { | ||
bytesRead = 0; | ||
chunkBytes = 0; | ||
pool = new Buffer(4096); | ||
pool.used = 0; | ||
|
||
do { | ||
try { | ||
chunkBytes = reader(pool, | ||
pool.used + bytesRead, | ||
pool.length - pool.used - bytesRead) | ||
} catch (e) { | ||
return self._error(e); | ||
} | ||
if (chunkBytes >= 0) { | ||
bytesRead += chunkBytes; | ||
} | ||
} while ((chunkBytes > 0) && (pool.used + bytesRead < pool.length)); | ||
|
||
if (bytesRead > 0) { | ||
chunk = pool.slice(0, bytesRead); | ||
writer(chunk); | ||
} | ||
} while (checker(bytesRead)); | ||
} | ||
|
||
mover( | ||
function(pool, offset, length) { | ||
return self._ssl.clearOut(pool, offset, length); | ||
}, | ||
function(chunk) { | ||
self.cleartext.emit('data', chunk); | ||
}, | ||
function(bytesRead) { | ||
return bytesRead > 0 && self._cleartext_write_state === true; | ||
}); | ||
|
||
mover( | ||
function(pool, offset, length) { | ||
return self._ssl.encOut(pool, offset, length); | ||
}, | ||
function(chunk) { | ||
self.encrypted.emit('data', chunk); | ||
}, | ||
function(bytesRead) { | ||
return bytesRead > 0 && self._encrypted_write_state === true; | ||
}); | ||
|
||
if (!this._secureEstablished && this._ssl.isInitFinished()) { | ||
this._secureEstablished = true; | ||
debug('secure established'); | ||
this.emit('secure'); | ||
this._cycle(); | ||
} | ||
}; | ||
|
||
SecurePair.prototype._destroy = function(err) | ||
{ | ||
if (!this._done) { | ||
this._done = true; | ||
this._ssl.close(); | ||
delete this._ssl; | ||
this.emit('end', err); | ||
} | ||
}; | ||
|
||
SecurePair.prototype._error = function (err) | ||
{ | ||
this.emit('error', err); | ||
}; | ||
|
||
SecurePair.prototype.getPeerCertificate = function (err) | ||
{ | ||
return this._ssl.getPeerCertificate(); | ||
}; | ||
|
||
SecurePair.prototype.getCipher = function (err) | ||
{ | ||
return this._ssl.getCurrentCipher(); | ||
}; |