From 509f38a01f11da7cd0cf01ce55d9ec5027d7dda3 Mon Sep 17 00:00:00 2001 From: Brendan Ashworth Date: Sat, 18 Apr 2015 11:02:11 -0700 Subject: [PATCH] https: refractor Agent into HTTP This commit refractors the Agent implementation into the respective HTTP submodules. For backwards compatibility, some changes were made to the https default agent. A developer can enable HTTPS by default for all requests by setting: http.globalAgent.protocol = 'https:' --- doc/api/http.markdown | 4 + doc/api/https.markdown | 4 +- lib/_http_agent.js | 27 +++- lib/_http_client.js | 15 ++- lib/https.js | 72 +---------- test/parallel/test-http-request-over-https.js | 118 ++++++++++++++++++ ....parse-only-support-http-https-protocol.js | 48 +------ 7 files changed, 171 insertions(+), 117 deletions(-) create mode 100644 test/parallel/test-http-request-over-https.js diff --git a/doc/api/http.markdown b/doc/api/http.markdown index 5cf3b07a9cd778..ad5be85056c5f7 100644 --- a/doc/api/http.markdown +++ b/doc/api/http.markdown @@ -469,6 +469,9 @@ Options: - `family`: IP address family to use when resolving `host` and `hostname`. Valid values are `4` or `6`. When unspecified, both IP v4 and v6 will be used. +- `protocol`: Protocol to use for the request. Defaults to `'http:'`. Valid + values are `'http:'` and `'https:'`. When the protocol is `'https:'`, options + will be passed to [tls.connect()][]. - `port`: Port of remote server. Defaults to 80. - `localAddress`: Local interface to bind for network connections. - `socketPath`: Unix Domain Socket (use one of host:port or socketPath). @@ -1099,4 +1102,5 @@ client's authentication details. [socket.setTimeout()]: net.html#net_socket_settimeout_timeout_callback [request.socket.getPeerCertificate()]: tls.html#tls_tlssocket_getpeercertificate_detailed [stream.setEncoding()]: stream.html#stream_stream_setencoding_encoding +[tls.connect()]: tls.html#tls_tls_connect_port_host_options_callback [url.parse()]: url.html#url_url_parse_urlstr_parsequerystring_slashesdenotehost diff --git a/doc/api/https.markdown b/doc/api/https.markdown index 4a46760b001b6e..7aec88d811540d 100644 --- a/doc/api/https.markdown +++ b/doc/api/https.markdown @@ -208,13 +208,15 @@ Example: ## Class: https.Agent + Stability: 0 - Deprecated. Use [http.Agent][] instead. + An Agent object for HTTPS similar to [http.Agent][]. See [https.request()][] for more information. ## https.globalAgent -Global instance of [https.Agent][] for all HTTPS client requests. +Global instance of [http.Agent][] for all HTTPS client requests. [http.Server#setTimeout()]: http.html#http_server_settimeout_msecs_callback [http.Server#timeout]: http.html#http_server_timeout diff --git a/lib/_http_agent.js b/lib/_http_agent.js index 02f5e0412cb16b..170d3a441fc6be 100644 --- a/lib/_http_agent.js +++ b/lib/_http_agent.js @@ -5,6 +5,8 @@ const util = require('util'); const EventEmitter = require('events').EventEmitter; const debug = util.debuglog('http'); +var tls; // lazy loaded (might not have crypto!) + // New Agent code. // The largest departure from the previous implementation is that @@ -90,7 +92,17 @@ exports.Agent = Agent; Agent.defaultMaxSockets = Infinity; -Agent.prototype.createConnection = net.createConnection; +Agent.prototype.createConnection = function(options) { + if (options.protocol === 'https:') { + if (!tls) + tls = require('tls'); + + return tls.connect(options); + } + + // Wildcard, default to http. + return net.createConnection(options); +}; // Get the key for a given set of request options Agent.prototype.getName = function(options) { @@ -108,6 +120,19 @@ Agent.prototype.getName = function(options) { if (options.localAddress) name += options.localAddress; name += ':'; + + if (this.protocol == 'https:') { + name += ':' + (options.ca || ''); + name += ':' + (options.cert || ''); + name += ':' + (options.ciphers || ''); + name += ':' + (options.key || ''); + name += ':' + (options.pfx || ''); + + name += ':'; + if (options.rejectUnauthorized !== undefined) + name += options.rejectUnauthorized; + } + return name; }; diff --git a/lib/_http_client.js b/lib/_http_client.js index a7d714f7e0b0b2..a5a1bd9a66d8fc 100644 --- a/lib/_http_client.js +++ b/lib/_http_client.js @@ -26,6 +26,11 @@ function ClientRequest(options, cb) { options = util._extend({}, options); } + // Using HTTPS and options.defaultPort remains as default + if (options.protocol === 'https:' && options.defaultPort === 80) { + options.defaultPort = 443; + } + var agent = options.agent; var defaultAgent = options._defaultAgent || Agent.globalAgent; if (agent === false) { @@ -36,10 +41,8 @@ function ClientRequest(options, cb) { } self.agent = agent; - var protocol = options.protocol || defaultAgent.protocol; - var expectedProtocol = defaultAgent.protocol; - if (self.agent && self.agent.protocol) - expectedProtocol = self.agent.protocol; + var protocol = options.protocol = options.protocol || + (agent && agent.protocol) || defaultAgent.protocol; if (options.path && / /.test(options.path)) { // The actual regex is more like /[^A-Za-z0-9\-._~!$&'()*+,;=/:@]/ @@ -49,9 +52,9 @@ function ClientRequest(options, cb) { // 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 (protocol !== expectedProtocol) { + } else if (protocol !== 'http:' && protocol !== 'https:') { throw new Error('Protocol "' + protocol + '" not supported. ' + - 'Expected "' + expectedProtocol + '".'); + 'Expected "http:" or "https:".'); } const defaultPort = options.defaultPort || diff --git a/lib/https.js b/lib/https.js index 21103b71af52b6..c75511879256f2 100644 --- a/lib/https.js +++ b/lib/https.js @@ -38,72 +38,9 @@ exports.createServer = function(opts, requestListener) { }; -// HTTPS agents. - -function createConnection(port, host, options) { - if (port !== null && typeof port === 'object') { - options = port; - } else if (host !== null && typeof host === 'object') { - options = host; - } else if (options === null || typeof options !== 'object') { - options = {}; - } - - if (typeof port === 'number') { - options.port = port; - } - - if (typeof host === 'string') { - options.host = host; - } - - debug('createConnection', options); - return tls.connect(options); -} - - -function Agent(options) { - http.Agent.call(this, options); - this.defaultPort = 443; - this.protocol = 'https:'; -} -inherits(Agent, http.Agent); -Agent.prototype.createConnection = createConnection; - -Agent.prototype.getName = function(options) { - var name = http.Agent.prototype.getName.call(this, options); - - name += ':'; - if (options.ca) - name += options.ca; - - name += ':'; - if (options.cert) - name += options.cert; - - name += ':'; - if (options.ciphers) - name += options.ciphers; - - name += ':'; - if (options.key) - name += options.key; - - name += ':'; - if (options.pfx) - name += options.pfx; - - name += ':'; - if (options.rejectUnauthorized !== undefined) - name += options.rejectUnauthorized; - - return name; -}; - -const globalAgent = new Agent(); - -exports.globalAgent = globalAgent; -exports.Agent = Agent; +// HTTPS request backwards compatibility +exports.globalAgent = new http.Agent(); +exports.Agent = http.Agent; exports.request = function(options, cb) { if (typeof options === 'string') { @@ -111,7 +48,8 @@ exports.request = function(options, cb) { } else { options = util._extend({}, options); } - options._defaultAgent = globalAgent; + options._defaultAgent = exports.globalAgent; + options.protocol = 'https:'; return http.request(options, cb); }; diff --git a/test/parallel/test-http-request-over-https.js b/test/parallel/test-http-request-over-https.js new file mode 100644 index 00000000000000..d73b85804f0690 --- /dev/null +++ b/test/parallel/test-http-request-over-https.js @@ -0,0 +1,118 @@ +'use strict'; + +const common = require('../common'); + +if (!common.hasCrypto) { + console.log('1..0 # Skipped: missing crypto'); + process.exit(); +} + +const http = require('http'); +const https = require('https'); +const assert = require('assert'); +const fs = require('fs'); +const url = require('url'); +const path = require('path'); + +var fixtures = path.resolve(__dirname, '../fixtures/keys'); +var options = { + key: fs.readFileSync(fixtures + '/agent1-key.pem'), + cert: fs.readFileSync(fixtures + '/agent1-cert.pem') +}; + +var localhost = common.localhostIPv4; +var successfulRequests = 0; + +https.createServer(options, function(req, res) { + res.writeHead(200); + res.end('ok'); +}).listen(common.PORT, function() { + + // mimic passing in a URL string + + var opt = url.parse('https://' + localhost + ':' + common.PORT + '/foo'); + opt.rejectUnauthorized = false; + http.get(opt, function(res) { + assert.equal(res.statusCode, 200); + successfulRequests++; + res.resume(); + }); + + // no agent / default agent + + http.get({ + rejectUnauthorized: false, + protocol: 'https:', + host: localhost, + path: '/bar', + port: common.PORT + }, function(res) { + assert.equal(res.statusCode, 200); + successfulRequests++; + res.resume(); + }); + + http.request({ + rejectUnauthorized: false, + host: localhost, + path: '/', + port: common.PORT, + protocol: 'https:', + agent: false + }, function(res) { + assert.equal(res.statusCode, 200); + successfulRequests++; + res.resume(); + }).end(); + + // custom agents + + var agent = new http.Agent(); + agent.defaultPort = common.PORT; + + http.request({ + rejectUnauthorized: false, + host: localhost, + path: '/foo', + protocol: 'https:', + agent: agent + }, function(res) { + assert.equal(res.statusCode, 200); + successfulRequests++; + res.resume(); + }).end(); + + var agent2 = new http.Agent(); + agent2.defaultPort = common.PORT; + agent2.protocol = 'https:'; + + http.request({ + rejectUnauthorized: false, + host: localhost, + path: '/', + agent: agent2 + }, function(res) { + assert.equal(res.statusCode, 200); + successfulRequests++; + res.resume(); + }).end(); + + // http global agent + http.globalAgent.protocol = 'https:'; + + http.request({ + rejectUnauthorized: false, + host: localhost, + path: '/foo', + port: common.PORT + }, function(res) { + assert.equal(res.statusCode, 200); + successfulRequests++; + res.resume(); + }).end(); + +}).unref(); + +process.on('exit', function() { + assert.equal(successfulRequests, 6); +}); diff --git a/test/parallel/test-http-url.parse-only-support-http-https-protocol.js b/test/parallel/test-http-url.parse-only-support-http-https-protocol.js index fd52a0b62d11de..d4ab4e070d3dba 100644 --- a/test/parallel/test-http-url.parse-only-support-http-https-protocol.js +++ b/test/parallel/test-http-url.parse-only-support-http-https-protocol.js @@ -7,60 +7,24 @@ var url = require('url'); assert.throws(function() { http.request(url.parse('file:///whatever')); -}, function(err) { - if (err instanceof Error) { - assert.strictEqual(err.message, 'Protocol "file:" not supported.' + - ' Expected "http:".'); - return true; - } -}); +}, /Protocol "file:" not supported/); assert.throws(function() { http.request(url.parse('mailto:asdf@asdf.com')); -}, function(err) { - if (err instanceof Error) { - assert.strictEqual(err.message, 'Protocol "mailto:" not supported.' + - ' Expected "http:".'); - return true; - } -}); +}, /Protocol "mailto:" not supported/); assert.throws(function() { http.request(url.parse('ftp://www.example.com')); -}, function(err) { - if (err instanceof Error) { - assert.strictEqual(err.message, 'Protocol "ftp:" not supported.' + - ' Expected "http:".'); - return true; - } -}); +}, /Protocol "ftp:" not supported/); assert.throws(function() { http.request(url.parse('javascript:alert(\'hello\');')); -}, function(err) { - if (err instanceof Error) { - assert.strictEqual(err.message, 'Protocol "javascript:" not supported.' + - ' Expected "http:".'); - return true; - } -}); +}, /Protocol "javascript:" not supported/); assert.throws(function() { http.request(url.parse('xmpp:isaacschlueter@jabber.org')); -}, function(err) { - if (err instanceof Error) { - assert.strictEqual(err.message, 'Protocol "xmpp:" not supported.' + - ' Expected "http:".'); - return true; - } -}); +}, /Protocol "xmpp:" not supported/); assert.throws(function() { http.request(url.parse('f://some.host/path')); -}, function(err) { - if (err instanceof Error) { - assert.strictEqual(err.message, 'Protocol "f:" not supported.' + - ' Expected "http:".'); - return true; - } -}); +}, /Protocol "f:" not supported/);