Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

http: allow http.request() over tls #1616

Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions doc/api/http.markdown
Original file line number Diff line number Diff line change
Expand Up @@ -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).
Expand Down Expand Up @@ -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
4 changes: 3 additions & 1 deletion doc/api/https.markdown
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
27 changes: 26 additions & 1 deletion lib/_http_agent.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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) {
Expand All @@ -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;
};

Expand Down
15 changes: 9 additions & 6 deletions lib/_http_client.js
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand All @@ -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\-._~!$&'()*+,;=/:@]/
Expand All @@ -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 ||
Expand Down
72 changes: 5 additions & 67 deletions lib/https.js
Original file line number Diff line number Diff line change
Expand Up @@ -38,80 +38,18 @@ 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') {
options = url.parse(options);
} else {
options = util._extend({}, options);
}
options._defaultAgent = globalAgent;
options._defaultAgent = exports.globalAgent;
options.protocol = 'https:';
return http.request(options, cb);
};

Expand Down
118 changes: 118 additions & 0 deletions test/parallel/test-http-request-over-https.js
Original file line number Diff line number Diff line change
@@ -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);
});
Original file line number Diff line number Diff line change
Expand Up @@ -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/);