--- /dev/null
- } else if (util.isNullOrUndefined(agent)) {
+// 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 util = require('util');
+var net = require('net');
+var url = require('url');
+var EventEmitter = require('events').EventEmitter;
+var HTTPParser = process.binding('http_parser').HTTPParser;
+var assert = require('assert').ok;
+
+var common = require('_http_common');
+
+var httpSocketSetup = common.httpSocketSetup;
+var parsers = common.parsers;
+var freeParser = common.freeParser;
+var debug = common.debug;
+
+var IncomingMessage = require('_http_incoming').IncomingMessage;
+var OutgoingMessage = require('_http_outgoing').OutgoingMessage;
+
+var Agent = require('_http_agent');
+
+
+function ClientRequest(options, cb) {
+ var self = this;
+ OutgoingMessage.call(self);
+
+ if (util.isString(options)) {
+ options = url.parse(options);
+ } else {
+ options = util._extend({}, options);
+ }
+
+ var agent = options.agent;
+ var defaultAgent = options._defaultAgent || Agent.globalAgent;
+ if (agent === false) {
+ agent = new defaultAgent.constructor();
- var defaultPort = options.defaultPort || self.agent.defaultPort;
++ } else if (util.isNullOrUndefined(agent) && !options.createConnection) {
+ agent = defaultAgent;
+ }
+ self.agent = agent;
+
+ if (options.path && / /.test(options.path)) {
+ // The actual regex is more like /[^A-Za-z0-9\-._~!$&'()*+,;=/:@]/
+ // with an additional rule for ignoring percentage-escaped characters
+ // but that's a) hard to capture in a regular expression that performs
+ // well, and b) possibly too restrictive for real-world usage. That's
+ // why it only scans for spaces because those are guaranteed to create
+ // an invalid request.
+ throw new TypeError('Request path contains unescaped characters.');
+ } else if (options.protocol && options.protocol !== self.agent.protocol) {
+ throw new Error('Protocol:' + options.protocol + ' not supported.');
+ }
+
- var port = options.port = options.port || defaultPort;
++ var defaultPort = options.defaultPort || self.agent && self.agent.defaultPort;
+
++ var port = options.port = options.port || defaultPort || 80;
+ var host = options.host = options.hostname || options.host || 'localhost';
+
+ if (util.isUndefined(options.setHost)) {
+ var setHost = true;
+ }
+
+ self.socketPath = options.socketPath;
+
+ var method = self.method = (options.method || 'GET').toUpperCase();
+ self.path = options.path || '/';
+ if (cb) {
+ self.once('response', cb);
+ }
+
+ if (!util.isArray(options.headers)) {
+ if (options.headers) {
+ var keys = Object.keys(options.headers);
+ for (var i = 0, l = keys.length; i < l; i++) {
+ var key = keys[i];
+ self.setHeader(key, options.headers[key]);
+ }
+ }
+ if (host && !this.getHeader('host') && setHost) {
+ var hostHeader = host;
+ if (port && +port !== defaultPort) {
+ hostHeader += ':' + port;
+ }
+ this.setHeader('Host', hostHeader);
+ }
+ }
+
+ if (options.auth && !this.getHeader('Authorization')) {
+ //basic auth
+ this.setHeader('Authorization', 'Basic ' +
+ new Buffer(options.auth).toString('base64'));
+ }
+
+ if (method === 'GET' ||
+ method === 'HEAD' ||
+ method === 'DELETE' ||
+ method === 'CONNECT') {
+ self.useChunkedEncodingByDefault = false;
+ } else {
+ self.useChunkedEncodingByDefault = true;
+ }
+
+ if (util.isArray(options.headers)) {
+ self._storeHeader(self.method + ' ' + self.path + ' HTTP/1.1\r\n',
+ options.headers);
+ } else if (self.getHeader('expect')) {
+ self._storeHeader(self.method + ' ' + self.path + ' HTTP/1.1\r\n',
+ self._renderHeaders());
+ }
+
+ if (self.socketPath) {
+ self._last = true;
+ self.shouldKeepAlive = false;
+ var conn = self.agent.createConnection({ path: self.socketPath });
+ self.onSocket(conn);
+ } else if (self.agent) {
+ // If there is an agent we should default to Connection:keep-alive,
+ // but only if the Agent will actually reuse the connection!
+ // If it's not a keepAlive agent, and the maxSockets==Infinity, then
+ // there's never a case where this socket will actually be reused
+ if (!self.agent.keepAlive && !Number.isFinite(self.agent.maxSockets)) {
+ self._last = true;
+ self.shouldKeepAlive = false;
+ } else {
+ self._last = false;
+ self.shouldKeepAlive = true;
+ }
+ self.agent.addRequest(self, options);
+ } else {
+ // No agent, default to Connection:close.
+ self._last = true;
+ self.shouldKeepAlive = false;
+ if (options.createConnection) {
+ var conn = options.createConnection(options);
+ } else {
+ debug('CLIENT use net.createConnection', options);
+ var conn = net.createConnection(options);
+ }
+ self.onSocket(conn);
+ }
+
+ self._deferToConnect(null, null, function() {
+ self._flush();
+ self = null;
+ });
+}
+
+util.inherits(ClientRequest, OutgoingMessage);
+
+exports.ClientRequest = ClientRequest;
+
+ClientRequest.prototype._finish = function() {
+ DTRACE_HTTP_CLIENT_REQUEST(this, this.connection);
+ COUNTER_HTTP_CLIENT_REQUEST();
+ OutgoingMessage.prototype._finish.call(this);
+};
+
+ClientRequest.prototype._implicitHeader = function() {
+ this._storeHeader(this.method + ' ' + this.path + ' HTTP/1.1\r\n',
+ this._renderHeaders());
+};
+
+ClientRequest.prototype.abort = function() {
+ // If we're aborting, we don't care about any more response data.
+ if (this.res)
+ this.res._dump();
+ else
+ this.once('response', function(res) {
+ res._dump();
+ });
+
+ if (this.socket) {
+ // in-progress
+ this.socket.destroy();
+ } else {
+ // haven't been assigned a socket yet.
+ // this could be more efficient, it could
+ // remove itself from the pending requests
+ this._deferToConnect('destroy', []);
+ }
+};
+
+
+function createHangUpError() {
+ var error = new Error('socket hang up');
+ error.code = 'ECONNRESET';
+ return error;
+}
+
+
+function socketCloseListener() {
+ var socket = this;
+ var req = socket._httpMessage;
+ debug('HTTP socket close');
+
+ // Pull through final chunk, if anything is buffered.
+ // the ondata function will handle it properly, and this
+ // is a no-op if no final chunk remains.
+ socket.read();
+
+ // NOTE: Its important to get parser here, because it could be freed by
+ // the `socketOnData`.
+ var parser = socket.parser;
+ req.emit('close');
+ if (req.res && req.res.readable) {
+ // Socket closed before we emitted 'end' below.
+ req.res.emit('aborted');
+ var res = req.res;
+ res.on('end', function() {
+ res.emit('close');
+ });
+ res.push(null);
+ } else if (!req.res && !req.socket._hadError) {
+ // This socket error fired before we started to
+ // receive a response. The error needs to
+ // fire on the request.
+ req.emit('error', createHangUpError());
+ req.socket._hadError = true;
+ }
+
+ // Too bad. That output wasn't getting written.
+ // This is pretty terrible that it doesn't raise an error.
+ // Fixed better in v0.10
+ if (req.output)
+ req.output.length = 0;
+ if (req.outputEncodings)
+ req.outputEncodings.length = 0;
+
+ if (parser) {
+ parser.finish();
+ freeParser(parser, req);
+ }
+}
+
+function socketErrorListener(err) {
+ var socket = this;
+ var parser = socket.parser;
+ var req = socket._httpMessage;
+ debug('SOCKET ERROR:', err.message, err.stack);
+
+ if (req) {
+ req.emit('error', err);
+ // For Safety. Some additional errors might fire later on
+ // and we need to make sure we don't double-fire the error event.
+ req.socket._hadError = true;
+ }
+
+ if (parser) {
+ parser.finish();
+ freeParser(parser, req);
+ }
+ socket.destroy();
+}
+
+function socketOnEnd() {
+ var socket = this;
+ var req = this._httpMessage;
+ var parser = this.parser;
+
+ if (!req.res && !req.socket._hadError) {
+ // If we don't have a response then we know that the socket
+ // ended prematurely and we need to emit an error on the request.
+ req.emit('error', createHangUpError());
+ req.socket._hadError = true;
+ }
+ if (parser) {
+ parser.finish();
+ freeParser(parser, req);
+ }
+ socket.destroy();
+}
+
+function socketOnData(d) {
+ var socket = this;
+ var req = this._httpMessage;
+ var parser = this.parser;
+
+ assert(parser && parser.socket === socket);
+
+ var ret = parser.execute(d);
+ if (ret instanceof Error) {
+ debug('parse error');
+ freeParser(parser, req);
+ socket.destroy();
+ req.emit('error', ret);
+ req.socket._hadError = true;
+ } else if (parser.incoming && parser.incoming.upgrade) {
+ // Upgrade or CONNECT
+ var bytesParsed = ret;
+ var res = parser.incoming;
+ req.res = res;
+
+ socket.removeListener('data', socketOnData);
+ socket.removeListener('end', socketOnEnd);
+ parser.finish();
+
+ var bodyHead = d.slice(bytesParsed, d.length);
+
+ var eventName = req.method === 'CONNECT' ? 'connect' : 'upgrade';
+ if (EventEmitter.listenerCount(req, eventName) > 0) {
+ req.upgradeOrConnect = true;
+
+ // detach the socket
+ socket.emit('agentRemove');
+ socket.removeListener('close', socketCloseListener);
+ socket.removeListener('error', socketErrorListener);
+
+ // TODO(isaacs): Need a way to reset a stream to fresh state
+ // IE, not flowing, and not explicitly paused.
+ socket._readableState.flowing = null;
+
+ req.emit(eventName, res, socket, bodyHead);
+ req.emit('close');
+ } else {
+ // Got Upgrade header or CONNECT method, but have no handler.
+ socket.destroy();
+ }
+ freeParser(parser, req);
+ } else if (parser.incoming && parser.incoming.complete &&
+ // When the status code is 100 (Continue), the server will
+ // send a final response after this client sends a request
+ // body. So, we must not free the parser.
+ parser.incoming.statusCode !== 100) {
+ socket.removeListener('data', socketOnData);
+ socket.removeListener('end', socketOnEnd);
+ freeParser(parser, req);
+ }
+}
+
+
+// client
+function parserOnIncomingClient(res, shouldKeepAlive) {
+ var socket = this.socket;
+ var req = socket._httpMessage;
+
+
+ // propogate "domain" setting...
+ if (req.domain && !res.domain) {
+ debug('setting "res.domain"');
+ res.domain = req.domain;
+ }
+
+ debug('AGENT incoming response!');
+
+ if (req.res) {
+ // We already have a response object, this means the server
+ // sent a double response.
+ socket.destroy();
+ return;
+ }
+ req.res = res;
+
+ // Responses to CONNECT request is handled as Upgrade.
+ if (req.method === 'CONNECT') {
+ res.upgrade = true;
+ return true; // skip body
+ }
+
+ // Responses to HEAD requests are crazy.
+ // HEAD responses aren't allowed to have an entity-body
+ // but *can* have a content-length which actually corresponds
+ // to the content-length of the entity-body had the request
+ // been a GET.
+ var isHeadResponse = req.method == 'HEAD';
+ debug('AGENT isHeadResponse', isHeadResponse);
+
+ if (res.statusCode == 100) {
+ // restart the parser, as this is a continue message.
+ delete req.res; // Clear res so that we don't hit double-responses.
+ req.emit('continue');
+ return true;
+ }
+
+ if (req.shouldKeepAlive && !shouldKeepAlive && !req.upgradeOrConnect) {
+ // Server MUST respond with Connection:keep-alive for us to enable it.
+ // If we've been upgraded (via WebSockets) we also shouldn't try to
+ // keep the connection open.
+ req.shouldKeepAlive = false;
+ }
+
+
+ DTRACE_HTTP_CLIENT_RESPONSE(socket, req);
+ COUNTER_HTTP_CLIENT_RESPONSE();
+ req.res = res;
+ res.req = req;
+
+ // add our listener first, so that we guarantee socket cleanup
+ res.on('end', responseOnEnd);
+ var handled = req.emit('response', res);
+
+ // If the user did not listen for the 'response' event, then they
+ // can't possibly read the data, so we ._dump() it into the void
+ // so that the socket doesn't hang there in a paused state.
+ if (!handled)
+ res._dump();
+
+ return isHeadResponse;
+}
+
+// client
+function responseOnEnd() {
+ var res = this;
+ var req = res.req;
+ var socket = req.socket;
+
+ if (!req.shouldKeepAlive) {
+ if (socket.writable) {
+ debug('AGENT socket.destroySoon()');
+ socket.destroySoon();
+ }
+ assert(!socket.writable);
+ } else {
+ debug('AGENT socket keep-alive');
+ if (req.timeoutCb) {
+ socket.setTimeout(0, req.timeoutCb);
+ req.timeoutCb = null;
+ }
+ socket.removeListener('close', socketCloseListener);
+ socket.removeListener('error', socketErrorListener);
+ // Mark this socket as available, AFTER user-added end
+ // handlers have a chance to run.
+ process.nextTick(function() {
+ socket.emit('free');
+ });
+ }
+}
+
+function tickOnSocket(req, socket) {
+ var parser = parsers.alloc();
+ req.socket = socket;
+ req.connection = socket;
+ parser.reinitialize(HTTPParser.RESPONSE);
+ parser.socket = socket;
+ parser.incoming = null;
+ req.parser = parser;
+
+ socket.parser = parser;
+ socket._httpMessage = req;
+
+ // Setup "drain" propogation.
+ httpSocketSetup(socket);
+
+ // Propagate headers limit from request object to parser
+ if (util.isNumber(req.maxHeadersCount)) {
+ parser.maxHeaderPairs = req.maxHeadersCount << 1;
+ } else {
+ // Set default value because parser may be reused from FreeList
+ parser.maxHeaderPairs = 2000;
+ }
+
+ parser.onIncoming = parserOnIncomingClient;
+ socket.on('error', socketErrorListener);
+ socket.on('data', socketOnData);
+ socket.on('end', socketOnEnd);
+ socket.on('close', socketCloseListener);
+ req.emit('socket', socket);
+}
+
+ClientRequest.prototype.onSocket = function(socket) {
+ var req = this;
+
+ process.nextTick(function() {
+ tickOnSocket(req, socket);
+ });
+};
+
+ClientRequest.prototype._deferToConnect = function(method, arguments_, cb) {
+ // This function is for calls that need to happen once the socket is
+ // connected and writable. It's an important promisy thing for all the socket
+ // calls that happen either now (when a socket is assigned) or
+ // in the future (when a socket gets assigned out of the pool and is
+ // eventually writable).
+ var self = this;
+ var onSocket = function() {
+ if (self.socket.writable) {
+ if (method) {
+ self.socket[method].apply(self.socket, arguments_);
+ }
+ if (cb) { cb(); }
+ } else {
+ self.socket.once('connect', function() {
+ if (method) {
+ self.socket[method].apply(self.socket, arguments_);
+ }
+ if (cb) { cb(); }
+ });
+ }
+ }
+ if (!self.socket) {
+ self.once('socket', onSocket);
+ } else {
+ onSocket();
+ }
+};
+
+ClientRequest.prototype.setTimeout = function(msecs, callback) {
+ if (callback) this.once('timeout', callback);
+
+ var self = this;
+ function emitTimeout() {
+ self.emit('timeout');
+ }
+
+ if (this.socket && this.socket.writable) {
+ if (this.timeoutCb)
+ this.socket.setTimeout(0, this.timeoutCb);
+ this.timeoutCb = emitTimeout;
+ this.socket.setTimeout(msecs, emitTimeout);
+ return;
+ }
+
+ // Set timeoutCb so that it'll get cleaned up on request end
+ this.timeoutCb = emitTimeout;
+ if (this.socket) {
+ var sock = this.socket;
+ this.socket.once('connect', function() {
+ sock.setTimeout(msecs, emitTimeout);
+ });
+ return;
+ }
+
+ this.once('socket', function(sock) {
+ sock.setTimeout(msecs, emitTimeout);
+ });
+};
+
+ClientRequest.prototype.setNoDelay = function() {
+ this._deferToConnect('setNoDelay', arguments);
+};
+ClientRequest.prototype.setSocketKeepAlive = function() {
+ this._deferToConnect('setKeepAlive', arguments);
+};
+
+ClientRequest.prototype.clearTimeout = function(cb) {
+ this.setTimeout(0, cb);
+};
--- /dev/null
+// Copyright Joyent, Inc. and other Node contributors.
+//
++// // Emit `beforeExit` if the loop became alive either after emitting
++// event, or after running some callbacks.
++//
+// 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 assert = require('assert');
+var constants = require('constants');
+var crypto = require('crypto');
+var net = require('net');
+var tls = require('tls');
+var util = require('util');
+
+var Timer = process.binding('timer_wrap').Timer;
+var tls_wrap = process.binding('tls_wrap');
+
+// Lazy load
+var tls_legacy;
+
+var debug = util.debuglog('tls');
+
+function onhandshakestart() {
+ debug('onhandshakestart');
+
+ var self = this;
+ var ssl = self.ssl;
+ var now = Timer.now();
+
+ assert(now >= ssl.lastHandshakeTime);
+
+ if ((now - ssl.lastHandshakeTime) >= tls.CLIENT_RENEG_WINDOW * 1000) {
+ ssl.handshakes = 0;
+ }
+
+ var first = (ssl.lastHandshakeTime === 0);
+ ssl.lastHandshakeTime = now;
+ if (first) return;
+
+ if (++ssl.handshakes > tls.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.');
+ self._tlsError(err);
+ });
+ }
+}
+
+
+function onhandshakedone() {
+ // for future use
+ debug('onhandshakedone');
+ this._finishInit();
+}
+
+
+function onclienthello(hello) {
+ var self = this,
+ onceSession = false,
+ onceSNI = false;
+
+ function callback(err, session) {
+ if (onceSession)
+ return self.destroy(new Error('TLS session callback was called 2 times'));
+ onceSession = true;
+
+ if (err)
+ return self.destroy(err);
+
+ // NOTE: That we have disabled OpenSSL's internal session storage in
+ // `node_crypto.cc` and hence its safe to rely on getting servername only
+ // from clienthello or this place.
+ var ret = self.ssl.loadSession(session);
+
+ // Servername came from SSL session
+ // NOTE: TLS Session ticket doesn't include servername information
+ //
+ // Another note, From RFC3546:
+ //
+ // If, on the other hand, the older
+ // session is resumed, then the server MUST ignore extensions appearing
+ // in the client hello, and send a server hello containing no
+ // extensions; in this case the extension functionality negotiated
+ // during the original session initiation is applied to the resumed
+ // session.
+ //
+ // Therefore we should account session loading when dealing with servername
+ if (ret && ret.servername) {
+ self._SNICallback(ret.servername, onSNIResult);
+ } else if (hello.servername && self._SNICallback) {
+ self._SNICallback(hello.servername, onSNIResult);
+ } else {
+ self.ssl.endParser();
+ }
+ }
+
+ function onSNIResult(err, context) {
+ if (onceSNI)
+ return self.destroy(new Error('TLS SNI callback was called 2 times'));
+ onceSNI = true;
+
+ if (err)
+ return self.destroy(err);
+
+ if (context)
+ self.ssl.sni_context = context;
+
+ self.ssl.endParser();
+ }
+
+ if (hello.sessionId.length <= 0 ||
+ hello.tlsTicket ||
+ this.server &&
+ !this.server.emit('resumeSession', hello.sessionId, callback)) {
+ // Invoke SNI callback, since we've no session to resume
+ if (hello.servername && this._SNICallback)
+ this._SNICallback(hello.servername, onSNIResult);
+ else
+ this.ssl.endParser();
+ }
+}
+
+
+function onnewsession(key, session) {
+ if (!this.server)
+ return;
+
+ var self = this;
+ var once = false;
+
+ this._newSessionPending = true;
+ this.server.emit('newSession', key, session, function() {
+ if (once)
+ return;
+ once = true;
+
+ self.ssl.newSessionDone();
+
+ self._newSessionPending = false;
+ if (self._securePending)
+ self._finishInit();
+ self._securePending = false;
+ });
+}
+
+
+/**
+ * Provides a wrap of socket stream to do encrypted communication.
+ */
+
+function TLSSocket(socket, options) {
+ // Disallow wrapping TLSSocket in TLSSocket
+ assert(!(socket instanceof TLSSocket));
+
+ net.Socket.call(this, socket && {
+ handle: socket._handle,
+ allowHalfOpen: socket.allowHalfOpen,
+ readable: socket.readable,
+ writable: socket.writable
+ });
+
+ // To prevent assertion in afterConnect()
+ if (socket)
+ this._connecting = socket._connecting;
+
+ this._tlsOptions = options;
+ this._secureEstablished = false;
+ this._securePending = false;
+ this._newSessionPending = false;
+ this._controlReleased = false;
+ this._SNICallback = null;
+ this.ssl = null;
+ this.servername = null;
+ this.npnProtocol = null;
+ this.authorized = false;
+ this.authorizationError = null;
+
+ // Just a documented property to make secure sockets
+ // distinguishable from regular ones.
+ this.encrypted = true;
+
+ this.on('error', this._tlsError);
+
+ if (!this._handle) {
+ this.once('connect', function() {
+ this._init(null);
+ });
+ } else {
+ this._init(socket);
+ }
+}
+util.inherits(TLSSocket, net.Socket);
+exports.TLSSocket = TLSSocket;
+
+TLSSocket.prototype._init = function(socket) {
+ assert(this._handle);
+
+ // lib/net.js expect this value to be non-zero if write hasn't been flushed
+ // immediately
+ // TODO(indutny): rewise this solution, it might be 1 before handshake and
+ // represent real writeQueueSize during regular writes.
+ this._handle.writeQueueSize = 1;
+
+ var self = this;
+ var options = this._tlsOptions;
+
+ // Wrap socket's handle
+ var credentials = options.credentials || crypto.createCredentials();
+ this.ssl = tls_wrap.wrap(this._handle, credentials.context, options.isServer);
+ this.server = options.server || null;
+
+ // For clients, we will always have either a given ca list or be using
+ // default one
+ var requestCert = !!options.requestCert || !options.isServer,
+ rejectUnauthorized = !!options.rejectUnauthorized;
+
+ this._requestCert = requestCert;
+ this._rejectUnauthorized = rejectUnauthorized;
+ if (requestCert || rejectUnauthorized)
+ this.ssl.setVerifyMode(requestCert, rejectUnauthorized);
+
+ if (options.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 (this.server &&
+ (this.server.listeners('resumeSession').length > 0 ||
+ this.server.listeners('newSession').length > 0)) {
+ this.ssl.enableSessionCallbacks();
+ }
+ } else {
+ this.ssl.onhandshakestart = function() {};
+ this.ssl.onhandshakedone = this._finishInit.bind(this);
+
+ if (options.session)
+ this.ssl.setSession(options.session);
+ }
+
+ this.ssl.onerror = function(err) {
+ if (self._writableState.errorEmitted)
+ return;
+ self._writableState.errorEmitted = true;
+
+ // Destroy socket if error happened before handshake's finish
+ if (!this._secureEstablished) {
+ self._tlsError(err);
+ self.destroy();
+ } else if (options.isServer &&
+ rejectUnauthorized &&
+ /peer did not return a certificate/.test(err.message)) {
+ // Ignore server's authorization errors
+ self.destroy();
+ } else {
+ // Throw error
+ self._tlsError(err);
+ }
+ };
+
+ // If custom SNICallback was given, or if
+ // there're SNI contexts to perform match against -
+ // set `.onsniselect` callback.
+ if (process.features.tls_sni &&
+ options.isServer &&
+ options.server &&
+ (options.SNICallback !== SNICallback ||
+ options.server._contexts.length)) {
+ assert(typeof options.SNICallback === 'function');
+ this._SNICallback = options.SNICallback;
+ this.ssl.enableHelloParser();
+ }
+
+ if (process.features.tls_npn && options.NPNProtocols)
+ this.ssl.setNPNProtocols(options.NPNProtocols);
+
+ if (options.handshakeTimeout > 0)
+ this.setTimeout(options.handshakeTimeout, this._handleTimeout);
+
+ // Socket already has some buffered data - emulate receiving it
+ if (socket && socket._readableState.length) {
+ var buf;
+ while ((buf = socket.read()) !== null)
+ this.ssl.receive(buf);
+ }
+};
+
+TLSSocket.prototype.renegotiate = function(options, callback) {
+ var requestCert = this._requestCert,
+ rejectUnauthorized = this._rejectUnauthorized;
+
+ if (typeof options.requestCert !== 'undefined')
+ requestCert = !!options.requestCert;
+ if (typeof options.rejectUnauthorized !== 'undefined')
+ rejectUnauthorized = !!options.rejectUnauthorized;
+
+ if (requestCert !== this._requestCert ||
+ rejectUnauthorized !== this._rejectUnauthorized) {
+ this.ssl.setVerifyMode(requestCert, rejectUnauthorized);
+ this._requestCert = requestCert;
+ this._rejectUnauthorized = rejectUnauthorized;
+ }
+ if (!this.ssl.renegotiate()) {
+ if (callback) {
+ process.nextTick(function() {
+ callback(new Error('Failed to renegotiate'));
+ });
+ }
+ return false;
+ }
+
+ // Ensure that we'll cycle through internal openssl's state
+ this.write('');
+
+ if (callback) {
+ this.once('secure', function() {
+ callback(null);
+ });
+ }
+
+ return true;
+};
+
+TLSSocket.prototype.setMaxSendFragment = function setMaxSendFragment(size) {
+ return this.ssl.setMaxSendFragment(size) == 1;
+};
+
+TLSSocket.prototype.getTLSTicket = function getTLSTicket() {
+ return this.ssl.getTLSTicket();
+};
+
+TLSSocket.prototype._handleTimeout = function() {
+ this._tlsError(new Error('TLS handshake timeout'));
+};
+
+TLSSocket.prototype._tlsError = function(err) {
+ this.emit('_tlsError', err);
+ if (this._controlReleased)
+ this.emit('error', err);
+};
+
+TLSSocket.prototype._releaseControl = function() {
+ if (this._controlReleased)
+ return false;
+ this._controlReleased = true;
+ this.removeListener('error', this._tlsError);
+ return true;
+};
+
+TLSSocket.prototype._finishInit = function() {
+ // `newSession` callback wasn't called yet
+ if (this._newSessionPending) {
+ this._securePending = true;
+ return;
+ }
+
+ if (process.features.tls_npn) {
+ this.npnProtocol = this.ssl.getNegotiatedProtocol();
+ }
+
+ if (process.features.tls_sni && this._tlsOptions.isServer) {
+ this.servername = this.ssl.getServername();
+ }
+
+ debug('secure established');
+ this._secureEstablished = true;
+ if (this._tlsOptions.handshakeTimeout > 0)
+ this.setTimeout(0, this._handleTimeout);
+ this.emit('secure');
+};
+
+TLSSocket.prototype._start = function() {
+ this.ssl.start();
+};
+
+TLSSocket.prototype.setServername = function(name) {
+ this.ssl.setServername(name);
+};
+
+TLSSocket.prototype.setSession = function(session) {
+ if (util.isString(session))
+ session = new Buffer(session, 'binary');
+ this.ssl.setSession(session);
+};
+
+TLSSocket.prototype.getPeerCertificate = function() {
+ if (this.ssl) {
+ var c = this.ssl.getPeerCertificate();
+
+ if (c) {
+ if (c.issuer) c.issuer = tls.parseCertString(c.issuer);
+ if (c.subject) c.subject = tls.parseCertString(c.subject);
+ return c;
+ }
+ }
+
+ return null;
+};
+
+TLSSocket.prototype.getSession = function() {
+ if (this.ssl) {
+ return this.ssl.getSession();
+ }
+
+ return null;
+};
+
+TLSSocket.prototype.isSessionReused = function() {
+ if (this.ssl) {
+ return this.ssl.isSessionReused();
+ }
+
+ return null;
+};
+
+TLSSocket.prototype.getCipher = function(err) {
+ if (this.ssl) {
+ return this.ssl.getCurrentCipher();
+ } else {
+ return null;
+ }
+};
+
+// 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 (tlsSocket) { }
+//
+// "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"
+//
+function Server(/* [options], listener */) {
+ var options, listener;
+ if (util.isObject(arguments[0])) {
+ options = arguments[0];
+ listener = arguments[1];
+ } else if (util.isFunction(arguments[0])) {
+ options = {};
+ listener = arguments[0];
+ }
+
+ if (!(this instanceof Server)) return new Server(options, listener);
+
+ this._contexts = [];
+
+ var self = this;
+
+ // Handle option defaults:
+ this.setOptions(options);
+
+ var sharedCreds = crypto.createCredentials({
+ pfx: self.pfx,
+ key: self.key,
+ passphrase: self.passphrase,
+ cert: self.cert,
+ ca: self.ca,
+ ciphers: self.ciphers || tls.DEFAULT_CIPHERS,
+ ecdhCurve: util.isUndefined(self.ecdhCurve) ?
+ tls.DEFAULT_ECDH_CURVE : self.ecdhCurve,
+ secureProtocol: self.secureProtocol,
+ secureOptions: self.secureOptions,
+ crl: self.crl,
+ sessionIdContext: self.sessionIdContext
+ });
+ this._sharedCreds = sharedCreds;
+
+ var timeout = options.handshakeTimeout || (120 * 1000);
+
+ if (!util.isNumber(timeout)) {
+ throw new TypeError('handshakeTimeout must be a number');
+ }
+
+ if (self.sessionTimeout) {
+ sharedCreds.context.setSessionTimeout(self.sessionTimeout);
+ }
+
+ if (self.ticketKeys) {
+ sharedCreds.context.setTicketKeys(self.ticketKeys);
+ }
+
+ // constructor call
+ net.Server.call(this, function(raw_socket) {
+ var socket = new TLSSocket(raw_socket, {
+ credentials: sharedCreds,
+ isServer: true,
+ server: self,
+ requestCert: self.requestCert,
+ rejectUnauthorized: self.rejectUnauthorized,
+ handshakeTimeout: timeout,
+ NPNProtocols: self.NPNProtocols,
+ SNICallback: options.SNICallback || SNICallback
+ });
+
+ socket.on('secure', function() {
+ if (socket._requestCert) {
+ var verifyError = socket.ssl.verifyError();
+ if (verifyError) {
+ socket.authorizationError = verifyError.code;
+
+ if (socket._rejectUnauthorized)
+ socket.destroy();
+ } else {
+ socket.authorized = true;
+ }
+ }
+
+ if (!socket.destroyed && socket._releaseControl())
+ self.emit('secureConnection', socket);
+ });
+
+ var errorEmitted = false;
+ socket.on('close', function() {
+ // Emit ECONNRESET
+ if (!socket._controlReleased && !errorEmitted) {
+ errorEmitted = true;
+ var connReset = new Error('socket hang up');
+ connReset.code = 'ECONNRESET';
+ self.emit('clientError', connReset, socket);
+ }
+ });
+
+ socket.on('_tlsError', function(err) {
+ if (!socket._controlReleased && !errorEmitted) {
+ errorEmitted = true;
+ self.emit('clientError', err, socket);
+ }
+ });
+ });
+
+ 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._getServerData = function() {
+ return {
+ ticketKeys: this._sharedCreds.context.getTicketKeys().toString('hex')
+ };
+};
+
+
+Server.prototype._setServerData = function(data) {
+ this._sharedCreds.context.setTicketKeys(new Buffer(data.ticketKeys, 'hex'));
+};
+
+
+Server.prototype.setOptions = function(options) {
+ if (util.isBoolean(options.requestCert)) {
+ this.requestCert = options.requestCert;
+ } else {
+ this.requestCert = false;
+ }
+
+ if (util.isBoolean(options.rejectUnauthorized)) {
+ 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 (!util.isUndefined(options.ecdhCurve))
+ this.ecdhCurve = options.ecdhCurve;
+ if (options.sessionTimeout) this.sessionTimeout = options.sessionTimeout;
+ if (options.ticketKeys) this.ticketKeys = options.ticketKeys;
+ var secureOptions = options.secureOptions || 0;
+ if (options.honorCipherOrder) {
+ secureOptions |= constants.SSL_OP_CIPHER_SERVER_PREFERENCE;
+ }
+ if (secureOptions) this.secureOptions = secureOptions;
+ if (options.NPNProtocols) tls.convertNPNProtocols(options.NPNProtocols, this);
+ if (options.sessionIdContext) {
+ this.sessionIdContext = options.sessionIdContext;
+ } else {
+ this.sessionIdContext = crypto.createHash('md5')
+ .update(process.argv.join(' '))
+ .digest('hex');
+ }
+};
+
+// 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]);
+};
+
+function SNICallback(servername, callback) {
+ var ctx;
+
+ this.server._contexts.some(function(elem) {
+ if (!util.isNull(servername.match(elem[0]))) {
+ ctx = elem[1];
+ return true;
+ }
+ });
+
+ callback(null, 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 (util.isObject(listArgs[1])) {
+ options = util._extend(options, listArgs[1]);
+ } else if (util.isObject(listArgs[2])) {
+ options = util._extend(options, listArgs[2]);
+ }
+
+ return (cb) ? [options, cb] : [options];
+}
+
+function legacyConnect(hostname, options, NPN, credentials) {
+ assert(options.socket);
+ if (!tls_legacy)
+ tls_legacy = require('_tls_legacy');
+
+ var pair = tls_legacy.createSecurePair(credentials,
+ false,
+ true,
+ !!options.rejectUnauthorized,
+ {
+ NPNProtocols: NPN.NPNProtocols,
+ servername: hostname
+ });
+ tls_legacy.pipe(pair, options.socket);
+ pair.cleartext._controlReleased = true;
+ pair.on('error', function(err) {
+ pair.cleartext.emit('error', err);
+ });
+
+ return pair;
+}
+
+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,
+ ciphers: tls.DEFAULT_CIPHERS
+ };
+ options = util._extend(defaults, options || {});
+
+ var hostname = options.servername ||
+ options.host ||
+ options.socket && options.socket._host,
+ NPN = {},
+ credentials = crypto.createCredentials(options);
+ tls.convertNPNProtocols(options.NPNProtocols, NPN);
+
+ // Wrapping TLS socket inside another TLS socket was requested -
+ // create legacy secure pair
+ var socket;
+ var legacy;
+ var result;
+ if (options.socket instanceof TLSSocket) {
+ debug('legacy connect');
+ legacy = true;
+ socket = legacyConnect(hostname, options, NPN, credentials);
+ result = socket.cleartext;
+ } else {
+ legacy = false;
+ socket = new TLSSocket(options.socket, {
+ credentials: credentials,
+ isServer: false,
+ requestCert: true,
+ rejectUnauthorized: options.rejectUnauthorized,
+ session: options.session,
+ NPNProtocols: NPN.NPNProtocols
+ });
+ result = socket;
+ }
+
+ if (socket._handle && !socket._connecting) {
+ onHandle();
+ } else {
+ // Not even started connecting yet (or probably resolving dns address),
+ // catch socket errors and assign handle.
+ if (!legacy && options.socket) {
+ options.socket.once('connect', function() {
+ assert(options.socket._handle);
+ socket._handle = options.socket._handle;
+ socket._handle.owner = socket;
+ socket.emit('connect');
+ });
+ }
+ socket.once('connect', onHandle);
+ }
+
+ if (cb)
+ result.once('secureConnect', cb);
+
+ if (!options.socket) {
+ assert(!legacy);
+ var connect_opt;
+ if (options.path && !options.port) {
+ connect_opt = { path: options.path };
+ } else {
+ connect_opt = {
+ port: options.port,
+ host: options.host,
+ localAddress: options.localAddress
+ };
+ }
+ socket.connect(connect_opt);
+ }
+
+ return result;
+
+ function onHandle() {
+ if (!legacy)
+ socket._releaseControl();
+
+ if (options.session)
+ socket.setSession(options.session);
+
+ if (!legacy) {
+ if (options.servername)
+ socket.setServername(options.servername);
+
+ socket._start();
+ }
+ socket.on('secure', function() {
+ var verifyError = socket.ssl.verifyError();
+
+ // Verify that server's identity matches it's certificate's names
+ if (!verifyError) {
+ var cert = result.getPeerCertificate();
+ var validCert = tls.checkServerIdentity(hostname, cert);
+ if (!validCert) {
+ verifyError = new Error('Hostname/IP doesn\'t match certificate\'s ' +
+ 'altnames');
+ }
+ }
+
+ if (verifyError) {
+ result.authorized = false;
+ result.authorizationError = verifyError.code || verifyError.message;
+
+ if (options.rejectUnauthorized) {
+ result.emit('error', verifyError);
+ result.destroy();
+ return;
+ } else {
+ result.emit('secureConnect');
+ }
+ } else {
+ result.authorized = true;
+ result.emit('secureConnect');
+ }
+
+ // Uncork incoming data
+ result.removeListener('end', onHangUp);
+ });
+
+ function onHangUp() {
+ // NOTE: This logic is shared with _http_client.js
+ if (!socket._hadError) {
+ socket._hadError = true;
+ var error = new Error('socket hang up');
+ error.code = 'ECONNRESET';
+ socket.destroy();
+ socket.emit('error', error);
+ }
+ }
+ result.once('end', onHangUp);
+ }
+};