diff options
author | Fedor Indutny <fedor.indutny@gmail.com> | 2013-06-13 17:36:00 +0400 |
---|---|---|
committer | Fedor Indutny <fedor.indutny@gmail.com> | 2013-06-16 11:30:15 +0400 |
commit | af80e7bc6e6f33c582eb1f7d37c7f5bbe9f910f7 (patch) | |
tree | c1dc4f7cbf0600c0ca2f50564b4edeae071b7887 /lib/tls.js | |
parent | 03e008ddb8381b601dd42330ae57efedd5979ce4 (diff) |
tls: introduce TLSSocket based on tls_wrap binding
Split `tls.js` into `_tls_legacy.js`, containing legacy
`createSecurePair` API, and `_tls_wrap.js` containing new code based on
`tls_wrap` binding.
Remove tests that are no longer useful/valid.
Diffstat (limited to 'lib/tls.js')
-rw-r--r-- | lib/tls.js | 1267 |
1 files changed, 16 insertions, 1251 deletions
diff --git a/lib/tls.js b/lib/tls.js index f5b175fb07b..e199cc57054 100644 --- a/lib/tls.js +++ b/lib/tls.js @@ -19,19 +19,13 @@ // 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 constants = require('constants'); - -var Timer = process.binding('timer_wrap').Timer; +var util = require('util'); -var DEFAULT_CIPHERS = 'ECDHE-RSA-AES128-SHA256:AES128-GCM-SHA256:' + // TLS 1.2 - 'RC4:HIGH:!MD5:!aNULL:!EDH'; // TLS 1.0 +exports.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 @@ -53,19 +47,9 @@ exports.getCiphers = function() { return Object.getOwnPropertyNames(ctx).sort(); }; - -var debug = util.debuglog('tls'); - -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) { +exports.convertNPNProtocols = 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) { @@ -87,10 +71,9 @@ function convertNPNProtocols(NPNProtocols, out) { if (Buffer.isBuffer(NPNProtocols)) { out.NPNProtocols = NPNProtocols; } -} - +}; -function checkServerIdentity(host, cert) { +exports.checkServerIdentity = function checkServerIdentity(host, cert) { // Create regexp to much hostnames function regexpify(host, wildcards) { // Add trailing dot (make hostnames uniform) @@ -198,357 +181,11 @@ function checkServerIdentity(host, cert) { } return valid; -} -exports.checkServerIdentity = checkServerIdentity; - - -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 %d bytes', data.length); - written = this.pair.ssl.clearIn(data, 0, data.length); - } else { - debug('encrypted.write called with %d bytes', data.length); - 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 %d bytes', data.length); - } else { - debug('encrypted.write queued with %d bytes', data.length); - } -}; - - -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 %d bytes', size); - out = this.pair.ssl.clearOut; - } else { - debug('encrypted.read called with %d bytes', size); - out = this.pair.ssl.encOut; - } - - var bytesRead = 0, - start = this._buffer.offset; - do { - var read = this._buffer.use(this.pair.ssl, out, size); - if (read > 0) { - bytesRead += read; - size -= read; - } - - // Handle and report errors - if (this.pair.ssl && this.pair.ssl.error) { - this.pair.error(); - break; - } - - // Get NPN and Server name when ready - this.pair.maybeInitFinished(); - } while (read > 0 && !this._buffer.isFull && bytesRead < size); - - // 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 %d bytes', bytesRead); - } else { - debug('encrypted.read succeed with %d bytes', bytesRead); - } - - // 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) { - // Perform graceful shutdown - this._done(); - - // No half-open, sorry! - if (this === this.pair.cleartext) - this._opposite._done(); - - // EOF - this.push(null); - } else { - // Bail out - this.push(''); - } - } else { - // Give them requested data - if (this.ondata) { - var self = this; - this.ondata(pool, start, start + bytesRead); - - // Consume data automatically - // simple/test-https-drain fails without it - process.nextTick(function() { - self.read(bytesRead); - }); - } - 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\nemailAddress=ry@clouds.org -function parseCertString(s) { +exports.parseCertString = function parseCertString(s) { var out = {}; var parts = s.split('\n'); for (var i = 0, len = parts.length; i < len; i++) { @@ -567,886 +204,14 @@ function parseCertString(s) { } } 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 = 2; - function finish() { - if (--waiting === 0) self.destroy(); - } - this._opposite.once('end', finish); - this.once('finish', finish); - } -}; - - -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; -}); - - -CleartextStream.prototype.__defineGetter__('localAddress', function() { - return this.socket && this.socket.localAddress; -}); - - -CleartextStream.prototype.__defineGetter__('localPort', function() { - return this.socket && this.socket.localPort; -}); - - -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(); - } - }); -} - -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. -// - sessionTimeout: integer. -// -// 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'); - } - - if (self.sessionTimeout) { - sharedCreds.context.setSessionTimeout(self.sessionTimeout); - } - - // constructor call - net.Server.call(this, function(socket) { - var creds = crypto.createCredentials(null, 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; - if (options.sessionTimeout) this.sessionTimeout = options.sessionTimeout; - var secureOptions = options.secureOptions || 0; - if (options.honorCipherOrder) { - secureOptions |= constants.SSL_OP_CIPHER_SERVER_PREFERENCE; - } - if (secureOptions) 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 || {}); - - var socket = options.socket ? options.socket : new net.Stream(); - - var sslcontext = crypto.createCredentials(options); - - convertNPNProtocols(options.NPNProtocols, this); - var hostname = options.servername || options.host || 'localhost', - pair = new SecurePair(sslcontext, false, true, - options.rejectUnauthorized === true ? true : false, - { - NPNProtocols: this.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; }; +// Public API +exports.Server = require('_tls_wrap').Server; +exports.createServer = require('_tls_wrap').createServer; +exports.connect = require('_tls_wrap').connect; -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.destroy(); - }); - }); - - 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; -} +// Legacy API +exports.__defineGetter__('createSecurePair', util.deprecate(function() { + return require('_tls_legacy').createSecurePair; +}, 'createSecurePair() is deprecated, use TLSSocket instead')); |