| // 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 constants = require('constants'); |
| |
| 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 checkServerIdentity(host, cert) { |
| // Create regexp to much hostnames |
| function regexpify(host, wildcards) { |
| // Add trailing dot (make hostnames uniform) |
| if (!/\.$/.test(host)) host += '.'; |
| |
| // The same applies to hostname with more than one wildcard, |
| // if hostname has wildcard when wildcards are not allowed, |
| // or if there are less than two dots after wildcard (i.e. *.com or *d.com) |
| // |
| // also |
| // |
| // "The client SHOULD NOT attempt to match a presented identifier in |
| // which the wildcard character comprises a label other than the |
| // left-most label (e.g., do not match bar.*.example.net)." |
| // RFC6125 |
| if (!wildcards && /\*/.test(host) || /[\.\*].*\*/.test(host) || |
| /\*/.test(host) && !/\*.*\..+\..+/.test(host)) { |
| return /$./; |
| } |
| |
| // Replace wildcard chars with regexp's wildcard and |
| // escape all characters that have special meaning in regexps |
| // (i.e. '.', '[', '{', '*', and others) |
| var re = host.replace( |
| /\*([a-z0-9\\-_\.])|[\.,\-\\\^\$+?*\[\]\(\):!\|{}]/g, |
| function(all, sub) { |
| if (sub) return '[a-z0-9\\-_]*' + (sub === '-' ? '\\-' : sub); |
| return '\\' + all; |
| }); |
| |
| return new RegExp('^' + re + '$', 'i'); |
| } |
| |
| var dnsNames = [], |
| uriNames = [], |
| ips = [], |
| matchCN = true, |
| valid = false; |
| |
| // There're several names to perform check against: |
| // CN and altnames in certificate extension |
| // (DNS names, IP addresses, and URIs) |
| // |
| // Walk through altnames and generate lists of those names |
| if (cert.subjectaltname) { |
| cert.subjectaltname.split(/, /g).forEach(function(altname) { |
| if (/^DNS:/.test(altname)) { |
| dnsNames.push(altname.slice(4)); |
| } else if (/^IP Address:/.test(altname)) { |
| ips.push(altname.slice(11)); |
| } else if (/^URI:/.test(altname)) { |
| var uri = url.parse(altname.slice(4)); |
| if (uri) uriNames.push(uri.hostname); |
| } |
| }); |
| } |
| |
| // If hostname is an IP address, it should be present in the list of IP |
| // addresses. |
| if (net.isIP(host)) { |
| valid = ips.some(function(ip) { |
| return ip === host; |
| }); |
| } else { |
| // Transform hostname to canonical form |
| if (!/\.$/.test(host)) host += '.'; |
| |
| // Otherwise check all DNS/URI records from certificate |
| // (with allowed wildcards) |
| dnsNames = dnsNames.map(function(name) { |
| return regexpify(name, true); |
| }); |
| |
| // Wildcards ain't allowed in URI names |
| uriNames = uriNames.map(function(name) { |
| return regexpify(name, false); |
| }); |
| |
| dnsNames = dnsNames.concat(uriNames); |
| |
| if (dnsNames.length > 0) matchCN = false; |
| |
| // Match against Common Name (CN) only if no supported identifiers are |
| // present. |
| // |
| // "As noted, a client MUST NOT seek a match for a reference identifier |
| // of CN-ID if the presented identifiers include a DNS-ID, SRV-ID, |
| // URI-ID, or any application-specific identifier types supported by the |
| // client." |
| // RFC6125 |
| if (matchCN) { |
| var commonNames = cert.subject.CN; |
| if (Array.isArray(commonNames)) { |
| for (var i = 0, k = commonNames.length; i < k; ++i) { |
| dnsNames.push(regexpify(commonNames[i], true)); |
| } |
| } else { |
| dnsNames.push(regexpify(commonNames, true)); |
| } |
| } |
| |
| valid = dnsNames.some(function(re) { |
| return re.test(host); |
| }); |
| } |
| |
| 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 ' + 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); |
| |
| // Deceive streams2 |
| var self = this; |
| |
| setImmediate(function() { |
| // Force state.reading to set to false |
| self.push(''); |
| |
| // Try reading more, we most likely have some data |
| self.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\nemailAddress=ry@clouds.org |
| 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 = Date.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 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; |
| 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); |
| |
| 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; |
| } |