|
|
|
// 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 common = require('_tls_common');
|
|
|
|
|
|
|
|
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 loadSession(self, hello, cb) {
|
|
|
|
var once = false;
|
|
|
|
function onSession(err, session) {
|
|
|
|
if (once)
|
|
|
|
return cb(new Error('TLS session callback was called 2 times'));
|
|
|
|
once = true;
|
|
|
|
|
|
|
|
if (err)
|
|
|
|
return cb(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);
|
|
|
|
|
|
|
|
cb(null, ret);
|
|
|
|
}
|
|
|
|
|
|
|
|
if (hello.sessionId.length <= 0 ||
|
|
|
|
hello.tlsTicket ||
|
|
|
|
self.server &&
|
|
|
|
!self.server.emit('resumeSession', hello.sessionId, onSession)) {
|
|
|
|
cb(null);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function loadSNI(self, servername, cb) {
|
|
|
|
if (!servername || !self._SNICallback)
|
|
|
|
return cb(null);
|
|
|
|
|
|
|
|
var once = false;
|
|
|
|
self._SNICallback(servername, function(err, context) {
|
|
|
|
if (once)
|
|
|
|
return cb(new Error('TLS SNI callback was called 2 times'));
|
|
|
|
once = true;
|
|
|
|
|
|
|
|
if (err)
|
|
|
|
return cb(err);
|
|
|
|
|
|
|
|
// TODO(indutny): eventually disallow raw `SecureContext`
|
|
|
|
if (context)
|
|
|
|
self.ssl.sni_context = context.context || context;
|
|
|
|
|
|
|
|
cb(null, self.ssl.sni_context);
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function requestOCSP(self, hello, ctx, cb) {
|
|
|
|
if (!hello.OCSPRequest || !self.server)
|
|
|
|
return cb(null);
|
|
|
|
|
|
|
|
if (!ctx)
|
|
|
|
ctx = self.server._sharedCreds;
|
|
|
|
if (ctx.context)
|
|
|
|
ctx = ctx.context;
|
|
|
|
|
|
|
|
if (self.server.listeners('OCSPRequest').length === 0) {
|
|
|
|
return cb(null);
|
|
|
|
} else {
|
|
|
|
self.server.emit('OCSPRequest',
|
|
|
|
ctx.getCertificate(),
|
|
|
|
ctx.getIssuer(),
|
|
|
|
onOCSP);
|
|
|
|
}
|
|
|
|
|
|
|
|
var once = false;
|
|
|
|
function onOCSP(err, response) {
|
|
|
|
if (once)
|
|
|
|
return cb(new Error('TLS OCSP callback was called 2 times'));
|
|
|
|
once = true;
|
|
|
|
|
|
|
|
if (err)
|
|
|
|
return cb(err);
|
|
|
|
|
|
|
|
if (response)
|
|
|
|
self.ssl.setOCSPResponse(response);
|
|
|
|
cb(null);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function onclienthello(hello) {
|
|
|
|
var self = this;
|
|
|
|
|
|
|
|
loadSession(self, hello, function(err, session) {
|
|
|
|
if (err)
|
|
|
|
return self.destroy(err);
|
|
|
|
|
|
|
|
// 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
|
|
|
|
var servername = session && session.servername || hello.servername;
|
|
|
|
loadSNI(self, servername, function(err, ctx) {
|
|
|
|
if (err)
|
|
|
|
return self.destroy(err);
|
|
|
|
requestOCSP(self, hello, ctx, function(err) {
|
|
|
|
if (err)
|
|
|
|
return self.destroy(err);
|
|
|
|
|
|
|
|
self.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;
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function onocspresponse(resp) {
|
|
|
|
this.emit('OCSPResponse', resp);
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
* 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, {
|
|
|
|
handle: socket && socket._handle,
|
|
|
|
allowHalfOpen: socket && socket.allowHalfOpen,
|
|
|
|
readable: false,
|
|
|
|
writable: false
|
|
|
|
});
|
|
|
|
|
|
|
|
// 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);
|
|
|
|
}
|
|
|
|
|
|
|
|
// Make sure to setup all required properties like: `_connecting` before
|
|
|
|
// starting the flow of the data
|
|
|
|
this.readable = true;
|
|
|
|
this.writable = true;
|
|
|
|
this.read(0);
|
|
|
|
}
|
|
|
|
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 context = options.secureContext ||
|
|
|
|
options.credentials ||
|
|
|
|
tls.createSecureContext();
|
|
|
|
this.ssl = tls_wrap.wrap(this._handle, context.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.server.listeners('OCSPRequest').length > 0)) {
|
|
|
|
this.ssl.enableSessionCallbacks();
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
this.ssl.onhandshakestart = function() {};
|
|
|
|
this.ssl.onhandshakedone = this._finishInit.bind(this);
|
|
|
|
this.ssl.onocspresponse = onocspresponse.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() {
|
|
|
|
if (this._tlsOptions.requestOCSP)
|
|
|
|
this.ssl.requestOCSP();
|
|
|
|
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(detailed) {
|
|
|
|
if (this.ssl) {
|
|
|
|
return common.translatePeerCertificate(
|
|
|
|
this.ssl.getPeerCertificate(detailed));
|
|
|
|
}
|
|
|
|
|
|
|
|
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 = tls.createSecureContext({
|
|
|
|
pfx: self.pfx,
|
|
|
|
key: self.key,
|
|
|
|
passphrase: self.passphrase,
|
|
|
|
cert: self.cert,
|
|
|
|
ca: self.ca,
|
|
|
|
ciphers: self.ciphers,
|
|
|
|
ecdhCurve: 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, {
|
|
|
|
secureContext: 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, context) {
|
|
|
|
if (!servername) {
|
|
|
|
throw 'Servername is required parameter for Server.addContext';
|
|
|
|
}
|
|
|
|
|
|
|
|
var re = new RegExp('^' +
|
|
|
|
servername.replace(/([\.^$+?\-\\[\]{}])/g, '\\$1')
|
|
|
|
.replace(/\*/g, '[^\.]*') +
|
|
|
|
'$');
|
|
|
|
this._contexts.push([re, tls.createSecureContext(context).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, context) {
|
|
|
|
assert(options.socket);
|
|
|
|
if (!tls_legacy)
|
|
|
|
tls_legacy = require('_tls_legacy');
|
|
|
|
|
|
|
|
var pair = tls_legacy.createSecurePair(context,
|
|
|
|
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 = {},
|
|
|
|
context = tls.createSecureContext(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, context);
|
|
|
|
result = socket.cleartext;
|
|
|
|
} else {
|
|
|
|
legacy = false;
|
|
|
|
socket = new TLSSocket(options.socket, {
|
|
|
|
secureContext: context,
|
|
|
|
isServer: false,
|
|
|
|
requestCert: true,
|
|
|
|
rejectUnauthorized: options.rejectUnauthorized,
|
|
|
|
session: options.session,
|
|
|
|
NPNProtocols: NPN.NPNProtocols,
|
|
|
|
requestOCSP: options.requestOCSP
|
|
|
|
});
|
|
|
|
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);
|
|
|
|
}
|
|
|
|
};
|