Created
January 8, 2015 02:59
-
-
Save ntrrgc/c5cbff08d0033e00f503 to your computer and use it in GitHub Desktop.
Patch node's TLS module to skip certificate name check
This file contains hidden or 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 is a modified version of node's built-in tls.js patched not to check | |
// the server hostname against its certificate. | |
// | |
// node 0.10 does not support this in a clean way. | |
// This will be allowed in node 0.12 setting a checkServerIdentity function | |
// in the HTTPS agent config. | |
function checkServerIdentity(host, cert) { | |
return true; | |
} | |
exports.checkServerIdentity = checkServerIdentity; | |
// BEGIN ALMOST VERBATIM NODE'S tls.js | |
// checkServerIdentity function has been removed | |
// Copyright Joyent, Inc. and other Node contributors. | |
// | |
// Permission is hereby granted, free of charge, to any person obtaining a | |
// copy of this software and associated documentation files (the | |
// "Software"), to deal in the Software without restriction, including | |
// without limitation the rights to use, copy, modify, merge, publish, | |
// distribute, sublicense, and/or sell copies of the Software, and to permit | |
// persons to whom the Software is furnished to do so, subject to the | |
// following conditions: | |
// | |
// The above copyright notice and this permission notice shall be included | |
// in all copies or substantial portions of the Software. | |
// | |
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS | |
// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF | |
// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN | |
// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, | |
// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR | |
// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE | |
// USE OR OTHER DEALINGS IN THE SOFTWARE. | |
var crypto = require('crypto'); | |
var util = require('util'); | |
var net = require('net'); | |
var url = require('url'); | |
var events = require('events'); | |
var stream = require('stream'); | |
var assert = require('assert').ok; | |
var Buffer = require('buffer').Buffer; | |
var constants = require('constants'); | |
var Timer = process.binding('timer_wrap').Timer; | |
var DEFAULT_CIPHERS = 'ECDHE-RSA-AES128-SHA256:AES128-GCM-SHA256:' + // TLS 1.2 | |
'RC4:HIGH:!MD5:!aNULL:!EDH'; // TLS 1.0 | |
// Allow {CLIENT_RENEG_LIMIT} client-initiated session renegotiations | |
// every {CLIENT_RENEG_WINDOW} seconds. An error event is emitted if more | |
// renegotations are seen. The settings are applied to all remote client | |
// connections. | |
exports.CLIENT_RENEG_LIMIT = 3; | |
exports.CLIENT_RENEG_WINDOW = 600; | |
exports.SLAB_BUFFER_SIZE = 10 * 1024 * 1024; | |
exports.getCiphers = function() { | |
var names = process.binding('crypto').getSSLCiphers(); | |
// Drop all-caps names in favor of their lowercase aliases, | |
var ctx = {}; | |
names.forEach(function(name) { | |
if (/^[0-9A-Z\-]+$/.test(name)) name = name.toLowerCase(); | |
ctx[name] = true; | |
}); | |
return Object.getOwnPropertyNames(ctx).sort(); | |
}; | |
var debug; | |
if (process.env.NODE_DEBUG && /tls/.test(process.env.NODE_DEBUG)) { | |
debug = function(a) { console.error('TLS:', a); }; | |
} else { | |
debug = function() { }; | |
} | |
var Connection = null; | |
try { | |
Connection = process.binding('crypto').Connection; | |
} catch (e) { | |
throw new Error('node.js not compiled with openssl crypto support.'); | |
} | |
// Convert protocols array into valid OpenSSL protocols list | |
// ("\x06spdy/2\x08http/1.1\x08http/1.0") | |
function convertNPNProtocols(NPNProtocols, out) { | |
// If NPNProtocols is Array - translate it into buffer | |
if (Array.isArray(NPNProtocols)) { | |
var buff = new Buffer(NPNProtocols.reduce(function(p, c) { | |
return p + 1 + Buffer.byteLength(c); | |
}, 0)); | |
NPNProtocols.reduce(function(offset, c) { | |
var clen = Buffer.byteLength(c); | |
buff[offset] = clen; | |
buff.write(c, offset + 1); | |
return offset + 1 + clen; | |
}, 0); | |
NPNProtocols = buff; | |
} | |
// If it's already a Buffer - store it | |
if (Buffer.isBuffer(NPNProtocols)) { | |
out.NPNProtocols = NPNProtocols; | |
} | |
} | |
function SlabBuffer() { | |
this.create(); | |
} | |
SlabBuffer.prototype.create = function create() { | |
this.isFull = false; | |
this.pool = new Buffer(exports.SLAB_BUFFER_SIZE); | |
this.offset = 0; | |
this.remaining = this.pool.length; | |
}; | |
SlabBuffer.prototype.use = function use(context, fn, size) { | |
if (this.remaining === 0) { | |
this.isFull = true; | |
return 0; | |
} | |
var actualSize = this.remaining; | |
if (size !== null) actualSize = Math.min(size, actualSize); | |
var bytes = fn.call(context, this.pool, this.offset, actualSize); | |
if (bytes > 0) { | |
this.offset += bytes; | |
this.remaining -= bytes; | |
} | |
assert(this.remaining >= 0); | |
return bytes; | |
}; | |
var slabBuffer = null; | |
// Base class of both CleartextStream and EncryptedStream | |
function CryptoStream(pair, options) { | |
stream.Duplex.call(this, options); | |
this.pair = pair; | |
this._pending = null; | |
this._pendingEncoding = ''; | |
this._pendingCallback = null; | |
this._doneFlag = false; | |
this._retryAfterPartial = false; | |
this._halfRead = false; | |
this._sslOutCb = null; | |
this._resumingSession = false; | |
this._reading = true; | |
this._destroyed = false; | |
this._ended = false; | |
this._finished = false; | |
this._opposite = null; | |
if (slabBuffer === null) slabBuffer = new SlabBuffer(); | |
this._buffer = slabBuffer; | |
this.once('finish', onCryptoStreamFinish); | |
// net.Socket calls .onend too | |
this.once('end', onCryptoStreamEnd); | |
} | |
util.inherits(CryptoStream, stream.Duplex); | |
function onCryptoStreamFinish() { | |
this._finished = true; | |
if (this === this.pair.cleartext) { | |
debug('cleartext.onfinish'); | |
if (this.pair.ssl) { | |
// Generate close notify | |
// NOTE: first call checks if client has sent us shutdown, | |
// second call enqueues shutdown into the BIO. | |
if (this.pair.ssl.shutdown() !== 1) { | |
if (this.pair.ssl && this.pair.ssl.error) | |
return this.pair.error(); | |
this.pair.ssl.shutdown(); | |
} | |
if (this.pair.ssl && this.pair.ssl.error) | |
return this.pair.error(); | |
} | |
} else { | |
debug('encrypted.onfinish'); | |
} | |
// Try to read just to get sure that we won't miss EOF | |
if (this._opposite.readable) this._opposite.read(0); | |
if (this._opposite._ended) { | |
this._done(); | |
// No half-close, sorry | |
if (this === this.pair.cleartext) this._opposite._done(); | |
} | |
} | |
function onCryptoStreamEnd() { | |
this._ended = true; | |
if (this === this.pair.cleartext) { | |
debug('cleartext.onend'); | |
} else { | |
debug('encrypted.onend'); | |
} | |
if (this.onend) this.onend(); | |
} | |
// NOTE: Called once `this._opposite` is set. | |
CryptoStream.prototype.init = function init() { | |
var self = this; | |
this._opposite.on('sslOutEnd', function() { | |
if (self._sslOutCb) { | |
var cb = self._sslOutCb; | |
self._sslOutCb = null; | |
cb(null); | |
} | |
}); | |
}; | |
CryptoStream.prototype._write = function write(data, encoding, cb) { | |
assert(this._pending === null); | |
// Black-hole data | |
if (!this.pair.ssl) return cb(null); | |
// When resuming session don't accept any new data. | |
// And do not put too much data into openssl, before writing it from encrypted | |
// side. | |
// | |
// TODO(indutny): Remove magic number, use watermark based limits | |
if (!this._resumingSession && | |
this._opposite._internallyPendingBytes() < 128 * 1024) { | |
// Write current buffer now | |
var written; | |
if (this === this.pair.cleartext) { | |
debug('cleartext.write called with ' + data.length + ' bytes'); | |
written = this.pair.ssl.clearIn(data, 0, data.length); | |
} else { | |
debug('encrypted.write called with ' + data.length + ' bytes'); | |
written = this.pair.ssl.encIn(data, 0, data.length); | |
} | |
// Handle and report errors | |
if (this.pair.ssl && this.pair.ssl.error) { | |
return cb(this.pair.error(true)); | |
} | |
// Force SSL_read call to cycle some states/data inside OpenSSL | |
this.pair.cleartext.read(0); | |
// Cycle encrypted data | |
if (this.pair.encrypted._internallyPendingBytes()) | |
this.pair.encrypted.read(0); | |
// Get NPN and Server name when ready | |
this.pair.maybeInitFinished(); | |
// Whole buffer was written | |
if (written === data.length) { | |
if (this === this.pair.cleartext) { | |
debug('cleartext.write succeed with ' + written + ' bytes'); | |
} else { | |
debug('encrypted.write succeed with ' + written + ' bytes'); | |
} | |
// Invoke callback only when all data read from opposite stream | |
if (this._opposite._halfRead) { | |
assert(this._sslOutCb === null); | |
this._sslOutCb = cb; | |
} else { | |
cb(null); | |
} | |
return; | |
} else if (written !== 0 && written !== -1) { | |
assert(!this._retryAfterPartial); | |
this._retryAfterPartial = true; | |
this._write(data.slice(written), encoding, cb); | |
this._retryAfterPartial = false; | |
return; | |
} | |
} else { | |
debug('cleartext.write queue is full'); | |
// Force SSL_read call to cycle some states/data inside OpenSSL | |
this.pair.cleartext.read(0); | |
} | |
// No write has happened | |
this._pending = data; | |
this._pendingEncoding = encoding; | |
this._pendingCallback = cb; | |
if (this === this.pair.cleartext) { | |
debug('cleartext.write queued with ' + data.length + ' bytes'); | |
} else { | |
debug('encrypted.write queued with ' + data.length + ' bytes'); | |
} | |
}; | |
CryptoStream.prototype._writePending = function writePending() { | |
var data = this._pending, | |
encoding = this._pendingEncoding, | |
cb = this._pendingCallback; | |
this._pending = null; | |
this._pendingEncoding = ''; | |
this._pendingCallback = null; | |
this._write(data, encoding, cb); | |
}; | |
CryptoStream.prototype._read = function read(size) { | |
// XXX: EOF?! | |
if (!this.pair.ssl) return this.push(null); | |
// Wait for session to be resumed | |
// Mark that we're done reading, but don't provide data or EOF | |
if (this._resumingSession || !this._reading) return this.push(''); | |
var out; | |
if (this === this.pair.cleartext) { | |
debug('cleartext.read called with ' + size + ' bytes'); | |
out = this.pair.ssl.clearOut; | |
} else { | |
debug('encrypted.read called with ' + size + ' bytes'); | |
out = this.pair.ssl.encOut; | |
} | |
var bytesRead = 0, | |
start = this._buffer.offset, | |
last = start; | |
do { | |
assert(last === this._buffer.offset); | |
var read = this._buffer.use(this.pair.ssl, out, size - bytesRead); | |
if (read > 0) { | |
bytesRead += read; | |
} | |
last = this._buffer.offset; | |
// Handle and report errors | |
if (this.pair.ssl && this.pair.ssl.error) { | |
this.pair.error(); | |
break; | |
} | |
} while (read > 0 && | |
!this._buffer.isFull && | |
bytesRead < size && | |
this.pair.ssl !== null); | |
// Get NPN and Server name when ready | |
this.pair.maybeInitFinished(); | |
// Create new buffer if previous was filled up | |
var pool = this._buffer.pool; | |
if (this._buffer.isFull) this._buffer.create(); | |
assert(bytesRead >= 0); | |
if (this === this.pair.cleartext) { | |
debug('cleartext.read succeed with ' + bytesRead + ' bytes'); | |
} else { | |
debug('encrypted.read succeed with ' + bytesRead + ' bytes'); | |
} | |
// Try writing pending data | |
if (this._pending !== null) this._writePending(); | |
if (this._opposite._pending !== null) this._opposite._writePending(); | |
if (bytesRead === 0) { | |
// EOF when cleartext has finished and we have nothing to read | |
if (this._opposite._finished && this._internallyPendingBytes() === 0 || | |
this.pair.ssl && this.pair.ssl.receivedShutdown) { | |
// Perform graceful shutdown | |
this._done(); | |
// No half-open, sorry! | |
if (this === this.pair.cleartext) { | |
this._opposite._done(); | |
// EOF | |
this.push(null); | |
} else if (!this.pair.ssl || !this.pair.ssl.receivedShutdown) { | |
// EOF | |
this.push(null); | |
} | |
} else { | |
// Bail out | |
this.push(''); | |
} | |
} else { | |
// Give them requested data | |
if (this.ondata) { | |
this.ondata(pool, start, start + bytesRead); | |
// Force state.reading to set to false | |
this.push(''); | |
// Try reading more, we most likely have some data | |
this.read(0); | |
} else { | |
this.push(pool.slice(start, start + bytesRead)); | |
} | |
} | |
// Let users know that we've some internal data to read | |
var halfRead = this._internallyPendingBytes() !== 0; | |
// Smart check to avoid invoking 'sslOutEnd' in the most of the cases | |
if (this._halfRead !== halfRead) { | |
this._halfRead = halfRead; | |
// Notify listeners about internal data end | |
if (!halfRead) { | |
if (this === this.pair.cleartext) { | |
debug('cleartext.sslOutEnd'); | |
} else { | |
debug('encrypted.sslOutEnd'); | |
} | |
this.emit('sslOutEnd'); | |
} | |
} | |
}; | |
CryptoStream.prototype.setTimeout = function(timeout, callback) { | |
if (this.socket) this.socket.setTimeout(timeout, callback); | |
}; | |
CryptoStream.prototype.setNoDelay = function(noDelay) { | |
if (this.socket) this.socket.setNoDelay(noDelay); | |
}; | |
CryptoStream.prototype.setKeepAlive = function(enable, initialDelay) { | |
if (this.socket) this.socket.setKeepAlive(enable, initialDelay); | |
}; | |
CryptoStream.prototype.__defineGetter__('bytesWritten', function() { | |
return this.socket ? this.socket.bytesWritten : 0; | |
}); | |
// Example: | |
// C=US\nST=CA\nL=SF\nO=Joyent\nOU=Node.js\nCN=ca1\[email protected] | |
function parseCertString(s) { | |
var out = {}; | |
var parts = s.split('\n'); | |
for (var i = 0, len = parts.length; i < len; i++) { | |
var sepIndex = parts[i].indexOf('='); | |
if (sepIndex > 0) { | |
var key = parts[i].slice(0, sepIndex); | |
var value = parts[i].slice(sepIndex + 1); | |
if (key in out) { | |
if (!Array.isArray(out[key])) { | |
out[key] = [out[key]]; | |
} | |
out[key].push(value); | |
} else { | |
out[key] = value; | |
} | |
} | |
} | |
return out; | |
} | |
CryptoStream.prototype.getPeerCertificate = function() { | |
if (this.pair.ssl) { | |
var c = this.pair.ssl.getPeerCertificate(); | |
if (c) { | |
if (c.issuer) c.issuer = parseCertString(c.issuer); | |
if (c.subject) c.subject = parseCertString(c.subject); | |
return c; | |
} | |
} | |
return null; | |
}; | |
CryptoStream.prototype.getSession = function() { | |
if (this.pair.ssl) { | |
return this.pair.ssl.getSession(); | |
} | |
return null; | |
}; | |
CryptoStream.prototype.isSessionReused = function() { | |
if (this.pair.ssl) { | |
return this.pair.ssl.isSessionReused(); | |
} | |
return null; | |
}; | |
CryptoStream.prototype.getCipher = function(err) { | |
if (this.pair.ssl) { | |
return this.pair.ssl.getCurrentCipher(); | |
} else { | |
return null; | |
} | |
}; | |
CryptoStream.prototype.end = function(chunk, encoding) { | |
if (this === this.pair.cleartext) { | |
debug('cleartext.end'); | |
} else { | |
debug('encrypted.end'); | |
} | |
// Write pending data first | |
if (this._pending !== null) this._writePending(); | |
this.writable = false; | |
stream.Duplex.prototype.end.call(this, chunk, encoding); | |
}; | |
CryptoStream.prototype.destroySoon = function(err) { | |
if (this === this.pair.cleartext) { | |
debug('cleartext.destroySoon'); | |
} else { | |
debug('encrypted.destroySoon'); | |
} | |
if (this.writable) | |
this.end(); | |
if (this._writableState.finished && this._opposite._ended) { | |
this.destroy(); | |
} else { | |
// Wait for both `finish` and `end` events to ensure that all data that | |
// was written on this side was read from the other side. | |
var self = this; | |
var waiting = 1; | |
this._opposite.once('end', finish); | |
if (!this._finished) { | |
this.once('finish', finish); | |
++waiting; | |
} | |
} | |
function finish() { | |
if (--waiting === 0) self.destroy(); | |
} | |
}; | |
CryptoStream.prototype.destroy = function(err) { | |
if (this._destroyed) return; | |
this._destroyed = true; | |
this.readable = this.writable = false; | |
// Destroy both ends | |
if (this === this.pair.cleartext) { | |
debug('cleartext.destroy'); | |
} else { | |
debug('encrypted.destroy'); | |
} | |
this._opposite.destroy(); | |
var self = this; | |
process.nextTick(function() { | |
// Force EOF | |
self.push(null); | |
// Emit 'close' event | |
self.emit('close', err ? true : false); | |
}); | |
}; | |
CryptoStream.prototype._done = function() { | |
this._doneFlag = true; | |
if (this === this.pair.encrypted && !this.pair._secureEstablished) | |
return this.pair.error(); | |
if (this.pair.cleartext._doneFlag && | |
this.pair.encrypted._doneFlag && | |
!this.pair._doneFlag) { | |
// If both streams are done: | |
this.pair.destroy(); | |
} | |
}; | |
// readyState is deprecated. Don't use it. | |
Object.defineProperty(CryptoStream.prototype, 'readyState', { | |
get: function() { | |
if (this._connecting) { | |
return 'opening'; | |
} else if (this.readable && this.writable) { | |
return 'open'; | |
} else if (this.readable && !this.writable) { | |
return 'readOnly'; | |
} else if (!this.readable && this.writable) { | |
return 'writeOnly'; | |
} else { | |
return 'closed'; | |
} | |
} | |
}); | |
function CleartextStream(pair, options) { | |
CryptoStream.call(this, pair, options); | |
// This is a fake kludge to support how the http impl sits | |
// on top of net Sockets | |
var self = this; | |
this._handle = { | |
readStop: function() { | |
self._reading = false; | |
}, | |
readStart: function() { | |
if (self._reading && self._readableState.length > 0) return; | |
self._reading = true; | |
self.read(0); | |
if (self._opposite.readable) self._opposite.read(0); | |
} | |
}; | |
} | |
util.inherits(CleartextStream, CryptoStream); | |
CleartextStream.prototype._internallyPendingBytes = function() { | |
if (this.pair.ssl) { | |
return this.pair.ssl.clearPending(); | |
} else { | |
return 0; | |
} | |
}; | |
CleartextStream.prototype.address = function() { | |
return this.socket && this.socket.address(); | |
}; | |
CleartextStream.prototype.__defineGetter__('remoteAddress', function() { | |
return this.socket && this.socket.remoteAddress; | |
}); | |
CleartextStream.prototype.__defineGetter__('remotePort', function() { | |
return this.socket && this.socket.remotePort; | |
}); | |
function EncryptedStream(pair, options) { | |
CryptoStream.call(this, pair, options); | |
} | |
util.inherits(EncryptedStream, CryptoStream); | |
EncryptedStream.prototype._internallyPendingBytes = function() { | |
if (this.pair.ssl) { | |
return this.pair.ssl.encPending(); | |
} else { | |
return 0; | |
} | |
}; | |
function onhandshakestart() { | |
debug('onhandshakestart'); | |
var self = this; | |
var ssl = self.ssl; | |
var now = Timer.now(); | |
assert(now >= ssl.lastHandshakeTime); | |
if ((now - ssl.lastHandshakeTime) >= exports.CLIENT_RENEG_WINDOW * 1000) { | |
ssl.handshakes = 0; | |
} | |
var first = (ssl.lastHandshakeTime === 0); | |
ssl.lastHandshakeTime = now; | |
if (first) return; | |
if (++ssl.handshakes > exports.CLIENT_RENEG_LIMIT) { | |
// Defer the error event to the next tick. We're being called from OpenSSL's | |
// state machine and OpenSSL is not re-entrant. We cannot allow the user's | |
// callback to destroy the connection right now, it would crash and burn. | |
setImmediate(function() { | |
var err = new Error('TLS session renegotiation attack detected.'); | |
if (self.cleartext) self.cleartext.emit('error', err); | |
}); | |
} | |
} | |
function onhandshakedone() { | |
// for future use | |
debug('onhandshakedone'); | |
} | |
function onclienthello(hello) { | |
var self = this, | |
once = false; | |
this._resumingSession = true; | |
function callback(err, session) { | |
if (once) return; | |
once = true; | |
if (err) return self.socket.destroy(err); | |
self.ssl.loadSession(session); | |
// Cycle data | |
self._resumingSession = false; | |
self.cleartext.read(0); | |
self.encrypted.read(0); | |
} | |
if (hello.sessionId.length <= 0 || | |
!this.server || | |
!this.server.emit('resumeSession', hello.sessionId, callback)) { | |
callback(null, null); | |
} | |
} | |
function onnewsession(key, session) { | |
if (!this.server) return; | |
this.server.emit('newSession', key, session); | |
} | |
/** | |
* Provides a pair of streams to do encrypted communication. | |
*/ | |
function SecurePair(credentials, isServer, requestCert, rejectUnauthorized, | |
options) { | |
if (!(this instanceof SecurePair)) { | |
return new SecurePair(credentials, | |
isServer, | |
requestCert, | |
rejectUnauthorized, | |
options); | |
} | |
var self = this; | |
options || (options = {}); | |
events.EventEmitter.call(this); | |
this.server = options.server; | |
this._secureEstablished = false; | |
this._isServer = isServer ? true : false; | |
this._encWriteState = true; | |
this._clearWriteState = true; | |
this._doneFlag = false; | |
this._destroying = false; | |
if (!credentials) { | |
this.credentials = crypto.createCredentials(); | |
} else { | |
this.credentials = credentials; | |
} | |
if (!this._isServer) { | |
// For clients, we will always have either a given ca list or be using | |
// default one | |
requestCert = true; | |
} | |
this._rejectUnauthorized = rejectUnauthorized ? true : false; | |
this._requestCert = requestCert ? true : false; | |
this.ssl = new Connection(this.credentials.context, | |
this._isServer ? true : false, | |
this._isServer ? this._requestCert : | |
options.servername, | |
this._rejectUnauthorized); | |
if (this._isServer) { | |
this.ssl.onhandshakestart = onhandshakestart.bind(this); | |
this.ssl.onhandshakedone = onhandshakedone.bind(this); | |
this.ssl.onclienthello = onclienthello.bind(this); | |
this.ssl.onnewsession = onnewsession.bind(this); | |
this.ssl.lastHandshakeTime = 0; | |
this.ssl.handshakes = 0; | |
} | |
if (process.features.tls_sni) { | |
if (this._isServer && options.SNICallback) { | |
this.ssl.setSNICallback(options.SNICallback); | |
} | |
this.servername = null; | |
} | |
if (process.features.tls_npn && options.NPNProtocols) { | |
this.ssl.setNPNProtocols(options.NPNProtocols); | |
this.npnProtocol = null; | |
} | |
/* Acts as a r/w stream to the cleartext side of the stream. */ | |
this.cleartext = new CleartextStream(this, options.cleartext); | |
/* Acts as a r/w stream to the encrypted side of the stream. */ | |
this.encrypted = new EncryptedStream(this, options.encrypted); | |
/* Let streams know about each other */ | |
this.cleartext._opposite = this.encrypted; | |
this.encrypted._opposite = this.cleartext; | |
this.cleartext.init(); | |
this.encrypted.init(); | |
process.nextTick(function() { | |
/* The Connection may be destroyed by an abort call */ | |
if (self.ssl) { | |
self.ssl.start(); | |
/* In case of cipher suite failures - SSL_accept/SSL_connect may fail */ | |
if (self.ssl && self.ssl.error) | |
self.error(); | |
} | |
}); | |
} | |
util.inherits(SecurePair, events.EventEmitter); | |
exports.createSecurePair = function(credentials, | |
isServer, | |
requestCert, | |
rejectUnauthorized) { | |
var pair = new SecurePair(credentials, | |
isServer, | |
requestCert, | |
rejectUnauthorized); | |
return pair; | |
}; | |
SecurePair.prototype.maybeInitFinished = function() { | |
if (this.ssl && !this._secureEstablished && this.ssl.isInitFinished()) { | |
if (process.features.tls_npn) { | |
this.npnProtocol = this.ssl.getNegotiatedProtocol(); | |
} | |
if (process.features.tls_sni) { | |
this.servername = this.ssl.getServername(); | |
} | |
this._secureEstablished = true; | |
debug('secure established'); | |
this.emit('secure'); | |
} | |
}; | |
SecurePair.prototype.destroy = function() { | |
if (this._destroying) return; | |
if (!this._doneFlag) { | |
debug('SecurePair.destroy'); | |
this._destroying = true; | |
// SecurePair should be destroyed only after it's streams | |
this.cleartext.destroy(); | |
this.encrypted.destroy(); | |
this._doneFlag = true; | |
this.ssl.error = null; | |
this.ssl.close(); | |
this.ssl = null; | |
} | |
}; | |
SecurePair.prototype.error = function(returnOnly) { | |
var err = this.ssl.error; | |
this.ssl.error = null; | |
if (!this._secureEstablished) { | |
// Emit ECONNRESET instead of zero return | |
if (!err || err.message === 'ZERO_RETURN') { | |
var connReset = new Error('socket hang up'); | |
connReset.code = 'ECONNRESET'; | |
connReset.sslError = err && err.message; | |
err = connReset; | |
} | |
this.destroy(); | |
if (!returnOnly) this.emit('error', err); | |
} else if (this._isServer && | |
this._rejectUnauthorized && | |
/peer did not return a certificate/.test(err.message)) { | |
// Not really an error. | |
this.destroy(); | |
} else { | |
if (!returnOnly) this.cleartext.emit('error', err); | |
} | |
return err; | |
}; | |
// TODO: support anonymous (nocert) and PSK | |
// AUTHENTICATION MODES | |
// | |
// There are several levels of authentication that TLS/SSL supports. | |
// Read more about this in "man SSL_set_verify". | |
// | |
// 1. The server sends a certificate to the client but does not request a | |
// cert from the client. This is common for most HTTPS servers. The browser | |
// can verify the identity of the server, but the server does not know who | |
// the client is. Authenticating the client is usually done over HTTP using | |
// login boxes and cookies and stuff. | |
// | |
// 2. The server sends a cert to the client and requests that the client | |
// also send it a cert. The client knows who the server is and the server is | |
// requesting the client also identify themselves. There are several | |
// outcomes: | |
// | |
// A) verifyError returns null meaning the client's certificate is signed | |
// by one of the server's CAs. The server know's the client idenity now | |
// and the client is authorized. | |
// | |
// B) For some reason the client's certificate is not acceptable - | |
// verifyError returns a string indicating the problem. The server can | |
// either (i) reject the client or (ii) allow the client to connect as an | |
// unauthorized connection. | |
// | |
// The mode is controlled by two boolean variables. | |
// | |
// requestCert | |
// If true the server requests a certificate from client connections. For | |
// the common HTTPS case, users will want this to be false, which is what | |
// it defaults to. | |
// | |
// rejectUnauthorized | |
// If true clients whose certificates are invalid for any reason will not | |
// be allowed to make connections. If false, they will simply be marked as | |
// unauthorized but secure communication will continue. By default this is | |
// true. | |
// | |
// | |
// | |
// Options: | |
// - requestCert. Send verify request. Default to false. | |
// - rejectUnauthorized. Boolean, default to true. | |
// - key. string. | |
// - cert: string. | |
// - ca: string or array of strings. | |
// | |
// emit 'secureConnection' | |
// function (cleartextStream, encryptedStream) { } | |
// | |
// 'cleartextStream' has the boolean property 'authorized' to determine if | |
// it was verified by the CA. If 'authorized' is false, a property | |
// 'authorizationError' is set on cleartextStream and has the possible | |
// values: | |
// | |
// "UNABLE_TO_GET_ISSUER_CERT", "UNABLE_TO_GET_CRL", | |
// "UNABLE_TO_DECRYPT_CERT_SIGNATURE", "UNABLE_TO_DECRYPT_CRL_SIGNATURE", | |
// "UNABLE_TO_DECODE_ISSUER_PUBLIC_KEY", "CERT_SIGNATURE_FAILURE", | |
// "CRL_SIGNATURE_FAILURE", "CERT_NOT_YET_VALID" "CERT_HAS_EXPIRED", | |
// "CRL_NOT_YET_VALID", "CRL_HAS_EXPIRED" "ERROR_IN_CERT_NOT_BEFORE_FIELD", | |
// "ERROR_IN_CERT_NOT_AFTER_FIELD", "ERROR_IN_CRL_LAST_UPDATE_FIELD", | |
// "ERROR_IN_CRL_NEXT_UPDATE_FIELD", "OUT_OF_MEM", | |
// "DEPTH_ZERO_SELF_SIGNED_CERT", "SELF_SIGNED_CERT_IN_CHAIN", | |
// "UNABLE_TO_GET_ISSUER_CERT_LOCALLY", "UNABLE_TO_VERIFY_LEAF_SIGNATURE", | |
// "CERT_CHAIN_TOO_LONG", "CERT_REVOKED" "INVALID_CA", | |
// "PATH_LENGTH_EXCEEDED", "INVALID_PURPOSE" "CERT_UNTRUSTED", | |
// "CERT_REJECTED" | |
// | |
// | |
// TODO: | |
// cleartext.credentials (by mirroring from pair object) | |
// cleartext.getCertificate() (by mirroring from pair.credentials.context) | |
function Server(/* [options], listener */) { | |
var options, listener; | |
if (typeof arguments[0] == 'object') { | |
options = arguments[0]; | |
listener = arguments[1]; | |
} else if (typeof arguments[0] == 'function') { | |
options = {}; | |
listener = arguments[0]; | |
} | |
if (!(this instanceof Server)) return new Server(options, listener); | |
this._contexts = []; | |
var self = this; | |
// Handle option defaults: | |
this.setOptions(options); | |
if (!self.pfx && (!self.cert || !self.key)) { | |
throw new Error('Missing PFX or certificate + private key.'); | |
} | |
var sharedCreds = crypto.createCredentials({ | |
pfx: self.pfx, | |
key: self.key, | |
passphrase: self.passphrase, | |
cert: self.cert, | |
ca: self.ca, | |
ciphers: self.ciphers || DEFAULT_CIPHERS, | |
secureProtocol: self.secureProtocol, | |
secureOptions: self.secureOptions, | |
crl: self.crl, | |
sessionIdContext: self.sessionIdContext | |
}); | |
var timeout = options.handshakeTimeout || (120 * 1000); | |
if (typeof timeout !== 'number') { | |
throw new TypeError('handshakeTimeout must be a number'); | |
} | |
// constructor call | |
net.Server.call(this, function(socket) { | |
var connOps = { | |
secureProtocol: self.secureProtocol, | |
secureOptions: self.secureOptions | |
}; | |
var creds = crypto.createCredentials(connOps, sharedCreds.context); | |
var pair = new SecurePair(creds, | |
true, | |
self.requestCert, | |
self.rejectUnauthorized, | |
{ | |
server: self, | |
NPNProtocols: self.NPNProtocols, | |
SNICallback: self.SNICallback, | |
// Stream options | |
cleartext: self._cleartext, | |
encrypted: self._encrypted | |
}); | |
var cleartext = pipe(pair, socket); | |
cleartext._controlReleased = false; | |
function listener() { | |
pair.emit('error', new Error('TLS handshake timeout')); | |
} | |
if (timeout > 0) { | |
socket.setTimeout(timeout, listener); | |
} | |
pair.once('secure', function() { | |
socket.setTimeout(0, listener); | |
pair.cleartext.authorized = false; | |
pair.cleartext.npnProtocol = pair.npnProtocol; | |
pair.cleartext.servername = pair.servername; | |
if (!self.requestCert) { | |
cleartext._controlReleased = true; | |
self.emit('secureConnection', pair.cleartext, pair.encrypted); | |
} else { | |
var verifyError = pair.ssl.verifyError(); | |
if (verifyError) { | |
pair.cleartext.authorizationError = verifyError.message; | |
if (self.rejectUnauthorized) { | |
socket.destroy(); | |
pair.destroy(); | |
} else { | |
cleartext._controlReleased = true; | |
self.emit('secureConnection', pair.cleartext, pair.encrypted); | |
} | |
} else { | |
pair.cleartext.authorized = true; | |
cleartext._controlReleased = true; | |
self.emit('secureConnection', pair.cleartext, pair.encrypted); | |
} | |
} | |
}); | |
pair.on('error', function(err) { | |
self.emit('clientError', err, this); | |
}); | |
}); | |
if (listener) { | |
this.on('secureConnection', listener); | |
} | |
} | |
util.inherits(Server, net.Server); | |
exports.Server = Server; | |
exports.createServer = function(options, listener) { | |
return new Server(options, listener); | |
}; | |
Server.prototype.setOptions = function(options) { | |
if (typeof options.requestCert == 'boolean') { | |
this.requestCert = options.requestCert; | |
} else { | |
this.requestCert = false; | |
} | |
if (typeof options.rejectUnauthorized == 'boolean') { | |
this.rejectUnauthorized = options.rejectUnauthorized; | |
} else { | |
this.rejectUnauthorized = false; | |
} | |
if (options.pfx) this.pfx = options.pfx; | |
if (options.key) this.key = options.key; | |
if (options.passphrase) this.passphrase = options.passphrase; | |
if (options.cert) this.cert = options.cert; | |
if (options.ca) this.ca = options.ca; | |
if (options.secureProtocol) this.secureProtocol = options.secureProtocol; | |
if (options.crl) this.crl = options.crl; | |
if (options.ciphers) this.ciphers = options.ciphers; | |
var secureOptions = crypto._getSecureOptions(options.secureProtocol, | |
options.secureOptions); | |
if (options.honorCipherOrder) { | |
secureOptions |= constants.SSL_OP_CIPHER_SERVER_PREFERENCE; | |
} | |
this.secureOptions = secureOptions; | |
if (options.NPNProtocols) convertNPNProtocols(options.NPNProtocols, this); | |
if (options.SNICallback) { | |
this.SNICallback = options.SNICallback; | |
} else { | |
this.SNICallback = this.SNICallback.bind(this); | |
} | |
if (options.sessionIdContext) { | |
this.sessionIdContext = options.sessionIdContext; | |
} else if (this.requestCert) { | |
this.sessionIdContext = crypto.createHash('md5') | |
.update(process.argv.join(' ')) | |
.digest('hex'); | |
} | |
if (options.cleartext) this.cleartext = options.cleartext; | |
if (options.encrypted) this.encrypted = options.encrypted; | |
}; | |
// SNI Contexts High-Level API | |
Server.prototype.addContext = function(servername, credentials) { | |
if (!servername) { | |
throw 'Servername is required parameter for Server.addContext'; | |
} | |
var re = new RegExp('^' + | |
servername.replace(/([\.^$+?\-\\[\]{}])/g, '\\$1') | |
.replace(/\*/g, '.*') + | |
'$'); | |
this._contexts.push([re, crypto.createCredentials(credentials).context]); | |
}; | |
Server.prototype.SNICallback = function(servername) { | |
var ctx; | |
this._contexts.some(function(elem) { | |
if (servername.match(elem[0]) !== null) { | |
ctx = elem[1]; | |
return true; | |
} | |
}); | |
return ctx; | |
}; | |
// Target API: | |
// | |
// var s = tls.connect({port: 8000, host: "google.com"}, function() { | |
// if (!s.authorized) { | |
// s.destroy(); | |
// return; | |
// } | |
// | |
// // s.socket; | |
// | |
// s.end("hello world\n"); | |
// }); | |
// | |
// | |
function normalizeConnectArgs(listArgs) { | |
var args = net._normalizeConnectArgs(listArgs); | |
var options = args[0]; | |
var cb = args[1]; | |
if (typeof listArgs[1] === 'object') { | |
options = util._extend(options, listArgs[1]); | |
} else if (typeof listArgs[2] === 'object') { | |
options = util._extend(options, listArgs[2]); | |
} | |
return (cb) ? [options, cb] : [options]; | |
} | |
exports.connect = function(/* [port, host], options, cb */) { | |
var args = normalizeConnectArgs(arguments); | |
var options = args[0]; | |
var cb = args[1]; | |
var defaults = { | |
rejectUnauthorized: '0' !== process.env.NODE_TLS_REJECT_UNAUTHORIZED | |
}; | |
options = util._extend(defaults, options || {}); | |
options.secureOptions = crypto._getSecureOptions(options.secureProtocol, | |
options.secureOptions); | |
var socket = options.socket ? options.socket : new net.Stream(); | |
var sslcontext = crypto.createCredentials(options); | |
var NPN = {}; | |
convertNPNProtocols(options.NPNProtocols, NPN); | |
var hostname = options.servername || options.host || 'localhost', | |
pair = new SecurePair(sslcontext, false, true, | |
options.rejectUnauthorized === true ? true : false, | |
{ | |
NPNProtocols: NPN.NPNProtocols, | |
servername: hostname, | |
cleartext: options.cleartext, | |
encrypted: options.encrypted | |
}); | |
if (options.session) { | |
var session = options.session; | |
if (typeof session === 'string') | |
session = new Buffer(session, 'binary'); | |
pair.ssl.setSession(session); | |
} | |
var cleartext = pipe(pair, socket); | |
if (cb) { | |
cleartext.once('secureConnect', cb); | |
} | |
if (!options.socket) { | |
var connect_opt = (options.path && !options.port) ? {path: options.path} : { | |
port: options.port, | |
host: options.host, | |
localAddress: options.localAddress | |
}; | |
socket.connect(connect_opt); | |
} | |
pair.on('secure', function() { | |
var verifyError = pair.ssl.verifyError(); | |
cleartext.npnProtocol = pair.npnProtocol; | |
// Verify that server's identity matches it's certificate's names | |
if (!verifyError) { | |
var validCert = checkServerIdentity(hostname, | |
pair.cleartext.getPeerCertificate()); | |
if (!validCert) { | |
verifyError = new Error('Hostname/IP doesn\'t match certificate\'s ' + | |
'altnames'); | |
} | |
} | |
if (verifyError) { | |
cleartext.authorized = false; | |
cleartext.authorizationError = verifyError.message; | |
if (pair._rejectUnauthorized) { | |
cleartext.emit('error', verifyError); | |
pair.destroy(); | |
} else { | |
cleartext.emit('secureConnect'); | |
} | |
} else { | |
cleartext.authorized = true; | |
cleartext.emit('secureConnect'); | |
} | |
}); | |
pair.on('error', function(err) { | |
cleartext.emit('error', err); | |
}); | |
cleartext._controlReleased = true; | |
return cleartext; | |
}; | |
function pipe(pair, socket) { | |
pair.encrypted.pipe(socket); | |
socket.pipe(pair.encrypted); | |
pair.encrypted.on('close', function() { | |
process.nextTick(function() { | |
// Encrypted should be unpiped from socket to prevent possible | |
// write after destroy. | |
pair.encrypted.unpipe(socket); | |
socket.destroySoon(); | |
}); | |
}); | |
pair.fd = socket.fd; | |
var cleartext = pair.cleartext; | |
cleartext.socket = socket; | |
cleartext.encrypted = pair.encrypted; | |
cleartext.authorized = false; | |
// cycle the data whenever the socket drains, so that | |
// we can pull some more into it. normally this would | |
// be handled by the fact that pipe() triggers read() calls | |
// on writable.drain, but CryptoStreams are a bit more | |
// complicated. Since the encrypted side actually gets | |
// its data from the cleartext side, we have to give it a | |
// light kick to get in motion again. | |
socket.on('drain', function() { | |
if (pair.encrypted._pending) | |
pair.encrypted._writePending(); | |
if (pair.cleartext._pending) | |
pair.cleartext._writePending(); | |
pair.encrypted.read(0); | |
pair.cleartext.read(0); | |
}); | |
function onerror(e) { | |
if (cleartext._controlReleased) { | |
cleartext.emit('error', e); | |
} | |
} | |
function onclose() { | |
socket.removeListener('error', onerror); | |
socket.removeListener('timeout', ontimeout); | |
} | |
function ontimeout() { | |
cleartext.emit('timeout'); | |
} | |
socket.on('error', onerror); | |
socket.on('close', onclose); | |
socket.on('timeout', ontimeout); | |
return cleartext; | |
} | |
// END ALMOST VERBATIM NODE'S tls.js | |
if (process.version.localeCompare("v0.11") < 0) { | |
var _ = require('lodash') | |
_.assign(require('tls'), exports) | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment