https-proxy-agent
Advanced tools
Comparing version 0.2.0 to 0.3.0
@@ -0,1 +1,16 @@ | ||
0.3.0 / 2013-09-16 | ||
================== | ||
- https-proxy-agent: use "debug" module | ||
- https-proxy-agent: update to the "agent-base" v1 API | ||
- https-proxy-agent: default the "port" to 443 if not set | ||
- https-proxy-agent: augment the `opts` object for the `tls.connect` function | ||
- https-proxy-agent: use "extend" module | ||
- https-proxy-agent: remove use of `this` as much as possible | ||
- https-proxy-agent: listen for the "error" event of the socket | ||
- test: refactor of tests to use "proxy" module | ||
- test: add "error" event catching test | ||
- test: add 407 proxy response test | ||
- test: use "semver" module, disable the HTTPS over HTTPS test for node >= v0.11.3 | ||
0.2.0 / 2013-09-03 | ||
@@ -2,0 +17,0 @@ ================== |
@@ -9,4 +9,6 @@ | ||
var url = require('url'); | ||
var extend = require('extend'); | ||
var Agent = require('agent-base'); | ||
var inherits = require('util').inherits; | ||
var debug = require('debug')('https-proxy-agent'); | ||
@@ -30,11 +32,13 @@ /** | ||
if (!opts) throw new Error('an HTTP(S) proxy server `host` and `port` must be specified!'); | ||
Agent.call(this); | ||
debug('creating new HttpsProxyAgent instance: %j', opts); | ||
Agent.call(this, connect); | ||
var proxy = clone(opts, {}); | ||
this.secureProxy = proxy.protocol && proxy.protocol == 'https:'; | ||
this.secureEndpoint = opts.secureEndpoint !== false; // `true` by default | ||
if (!this.secureEndpoint) { | ||
this.defaultPort = 80; | ||
} | ||
var proxy = extend({}, opts); | ||
// if `true`, then connect to the proxy server over TLS. defaults to `false`. | ||
this.secureProxy = proxy.protocol ? proxy.protocol == 'https:' : false; | ||
// if `true`, then connect to the destination endpoint over TLS, defaults to `true` | ||
this.secureEndpoint = opts.secureEndpoint !== false; | ||
// prefer `hostname` over `host`, and set the `port` if needed | ||
@@ -56,21 +60,37 @@ proxy.host = proxy.hostname || proxy.host; | ||
/** | ||
* Default port to connect to. | ||
* Default options for the "connect" opts object. | ||
*/ | ||
Agent.prototype.defaultPort = 443; | ||
var defaults = { port: 80 }; | ||
var secureDefaults = { port: 443 }; | ||
/** | ||
* Initiates a TCP connection to the specified HTTP proxy server. | ||
* Called when the node-core HTTP client library is creating a new HTTP request. | ||
* | ||
* @api protected | ||
* @api public | ||
*/ | ||
HttpsProxyAgent.prototype.createConnection = function (opts, fn) { | ||
function connect (req, _opts, fn) { | ||
var proxy = this.proxy; | ||
var secureProxy = this.secureProxy; | ||
var secureEndpoint = this.secureEndpoint; | ||
// these `opts` are the connect options to connect to the destination endpoint | ||
var opts = extend({}, proxy, secureEndpoint ? secureDefaults : defaults, _opts); | ||
var socket; | ||
if (this.secureProxy) { | ||
socket = tls.connect(this.proxy); | ||
if (secureProxy) { | ||
socket = tls.connect(proxy); | ||
} else { | ||
socket = net.connect(this.proxy); | ||
socket = net.connect(proxy); | ||
} | ||
// we need to buffer any HTTP traffic that happens with the proxy before we get | ||
// the CONNECT response, so that if the response is anything other than an "200" | ||
// response code, then we can re-play the "data" events on the socket once the | ||
// HTTP parser is hooked up... | ||
var buffers = []; | ||
var buffersLength = 0; | ||
function read () { | ||
@@ -82,21 +102,114 @@ var b = socket.read(); | ||
var self = this; | ||
function cleanup () { | ||
socket.removeListener('data', ondata); | ||
socket.removeListener('end', onend); | ||
socket.removeListener('error', onerror); | ||
socket.removeListener('close', onclose); | ||
socket.removeListener('readable', read); | ||
} | ||
function onclose (err) { | ||
console.error('onclose'); | ||
} | ||
function onend (err) { | ||
console.error('onend'); | ||
} | ||
function onerror (err) { | ||
cleanup(); | ||
fn(err); | ||
} | ||
function ondata (b) { | ||
buffers.push(b); | ||
buffersLength += b.length; | ||
var buffered = Buffer.concat(buffers, buffersLength); | ||
var str = buffered.toString('ascii'); | ||
if (!~str.indexOf('\r\n\r\n')) { | ||
// keep buffering | ||
debug('have not received end of HTTP headers yet... %j'); | ||
if (socket.read) { | ||
read(); | ||
} else { | ||
socket.once('data', ondata); | ||
} | ||
return; | ||
} | ||
var firstLine = str.substring(0, str.indexOf('\r\n')); | ||
var statusCode = +firstLine.split(' ')[1]; | ||
debug('got proxy server response: "%s"', firstLine); | ||
//console.log('statusCode: %d', statusCode); | ||
//console.log(b.length, b, b.toString()); | ||
// TODO: verify that the socket is properly connected, check response... | ||
var sock = socket; | ||
if (200 == statusCode) { | ||
// 200 Connected status code! | ||
var sock = socket; | ||
if (self.secureEndpoint) { | ||
// since the proxy is connecting to an SSL server, we have | ||
// to upgrade this socket connection to an SSL connection | ||
sock = tls.connect({ | ||
socket: socket, | ||
servername: opts.host | ||
}); | ||
// nullify the buffered data since we won't be needing it | ||
buffers = buffered = null; | ||
if (secureEndpoint) { | ||
// since the proxy is connecting to an SSL server, we have | ||
// to upgrade this socket connection to an SSL connection | ||
debug('upgrading proxy-connected socket to TLS connection: "%s"', opts.host); | ||
opts.socket = socket; | ||
opts.servername = opts.host; | ||
opts.host = null; | ||
opts.hostname = null; | ||
opts.port = null; | ||
sock = tls.connect(opts); | ||
} | ||
cleanup(); | ||
fn(null, sock); | ||
} else { | ||
// some other status code that's not 200... need to re-play the HTTP header | ||
// "data" events onto the socket once the HTTP machinery is attached so that | ||
// the user can parse and handle the error status code | ||
cleanup(); | ||
// save a reference to the concat'd Buffer for the `onsocket` callback | ||
buffers = buffered; | ||
// need to wait for the "socket" event to re-play the "data" events | ||
req.once('socket', onsocket); | ||
fn(null, socket); | ||
} | ||
} | ||
fn(null, sock); | ||
function onsocket (socket) { | ||
// replay the "buffers" Buffer onto the `socket`, since at this point | ||
// the HTTP module machinery has been hooked up for the user | ||
if ('function' == typeof socket.ondata) { | ||
// node <= v0.11.3, the `ondata` function is set on the socket | ||
socket.ondata(buffers, 0, buffers.length); | ||
} else if (socket.listeners('data').length > 0) { | ||
// node > v0.11.3, the "data" event is listened for directly | ||
socket.emit('data', buffers); | ||
} else { | ||
// never? | ||
throw new Error('should not happen...'); | ||
} | ||
buffers = null; | ||
// XXX: not sure if forcing "end" here is appropriate, but otherwise the | ||
// socket never closes and the tests fail since the proxy server never shuts | ||
// down... | ||
/* | ||
if ('function' == typeof socket.onend) { | ||
socket.onend(); | ||
} else { | ||
socket.emit('end'); | ||
} | ||
*/ | ||
//socket.destroy(); | ||
} | ||
socket.on('error', onerror); | ||
socket.on('close', onclose); | ||
socket.on('end', onend); | ||
if (socket.read) { | ||
@@ -110,3 +223,3 @@ read(); | ||
var msg = 'CONNECT ' + hostname + ' HTTP/1.1\r\n'; | ||
var auth = this.proxy.auth; | ||
var auth = proxy.auth; | ||
if (auth) { | ||
@@ -116,9 +229,5 @@ msg += 'Proxy-Authorization: Basic ' + new Buffer(auth).toString('base64') + '\r\n'; | ||
msg += 'Host: ' + hostname + '\r\n' + | ||
'\r\n'; | ||
'Connection: close\r\n' + | ||
'\r\n'; | ||
socket.write(msg); | ||
}; | ||
function clone (src, dest) { | ||
for (var i in src) dest[i] = src[i]; | ||
return dest; | ||
} |
{ | ||
"name": "https-proxy-agent", | ||
"version": "0.2.0", | ||
"version": "0.3.0", | ||
"description": "An HTTP(s) proxy `http.Agent` implementation for HTTPS", | ||
@@ -25,7 +25,11 @@ "main": "https-proxy-agent.js", | ||
"dependencies": { | ||
"agent-base": "~0.0.1" | ||
"agent-base": "~1.0.1", | ||
"debug": "~0.7.2", | ||
"extend": "~1.2.0" | ||
}, | ||
"devDependencies": { | ||
"mocha": "~1.12.0" | ||
"mocha": "~1.12.0", | ||
"proxy": "~0.2.1", | ||
"semver": "~2.1.0" | ||
} | ||
} |
https-proxy-agent | ||
================ | ||
### An HTTP(s) proxy `http.Agent` implementation for HTTPS | ||
[![Build Status](https://travis-ci.org/TooTallNate/node-https-proxy-agent.png?branch=master)](https://travis-ci.org/TooTallNate/node-https-proxy-agent) | ||
@@ -9,4 +10,4 @@ This module provides an `http.Agent` implementation that connects to a specified | ||
Specifically, this `Agent` implementation connects to an intermediary "proxy" | ||
server and issues the CONNECT HTTP method, which tells the proxy to open a | ||
direct TCP connection to the endpoint server. | ||
server and issues the [CONNECT HTTP method][CONNECT], which tells the proxy to | ||
open a direct TCP connection to the destination server. | ||
@@ -78,3 +79,3 @@ Since this agent implements the CONNECT HTTP method, it also works with other | ||
// over "ws://", but `true` when connecting over "wss://" | ||
opts.secureEndpoint = parsed.protocol && parsed.protocol == 'wss:'; | ||
opts.secureEndpoint = parsed.protocol ? parsed.protocol == 'wss:' : false; | ||
@@ -97,2 +98,21 @@ var agent = new HttpsProxyAgent(opts); | ||
API | ||
--- | ||
### new HttpsProxyAgent(opts) | ||
The `HttpsProxyAgent` class implements an `http.Agent` subclass that connects | ||
to the specified "HTTP(s) proxy server" in order to proxy HTTPS and/or WebSocket | ||
requests. This is achieved by using the [HTTP `CONNECT` method][CONNECT]. | ||
The `opts` argument may either be a string URI of the proxy server to use, or an | ||
"options" object with more specific properties: | ||
* `host` - String - Proxy host to connect to (may use `hostname` as well). Required. | ||
* `port` - Number - Proxy port to connect to. Required. | ||
* `secureProxy` - Boolean - If `true`, then use TLS to connect to the proxy. Defaults to `false`. | ||
* `secureEndpoint` - Boolean - If `true` then then a TLS connection to the endpoint will be established on top of the proxy socket. Defaults to `true`. | ||
* Any other options given are passed to the `net.connect()`/`tls.connect()` functions. | ||
License | ||
@@ -123,1 +143,3 @@ ------- | ||
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. | ||
[CONNECT]: http://en.wikipedia.org/wiki/HTTP_tunnel#HTTP_CONNECT_Tunneling |
269
test/test.js
@@ -6,5 +6,10 @@ | ||
var fs = require('fs'); | ||
var url = require('url'); | ||
var http = require('http'); | ||
var https = require('https'); | ||
var assert = require('assert'); | ||
var Proxy = require('proxy'); | ||
var Semver = require('semver'); | ||
var version = new Semver(process.version); | ||
var HttpsProxyAgent = require('../'); | ||
@@ -14,53 +19,170 @@ | ||
this.slow(5000); | ||
this.timeout(10000); | ||
var server; | ||
var serverPort; | ||
var link = process.env.LINK || 'https://graph.facebook.com/tootallnate'; | ||
var sslServer; | ||
var sslServerPort; | ||
it('should throw an Error if no "proxy" is given', function () { | ||
assert.throws(function () { | ||
new HttpsProxyAgent(); | ||
var proxy; | ||
var proxyPort; | ||
var sslProxy; | ||
var sslProxyPort; | ||
before(function (done) { | ||
// setup target HTTP server | ||
server = http.createServer(); | ||
server.listen(function () { | ||
serverPort = server.address().port; | ||
done(); | ||
}); | ||
}); | ||
it('should work over an HTTP proxy', function (done) { | ||
var proxy = process.env.HTTP_PROXY || process.env.http_proxy || 'http://10.1.10.200:3128'; | ||
var agent = new HttpsProxyAgent(proxy); | ||
before(function (done) { | ||
// setup HTTP proxy server | ||
proxy = Proxy(); | ||
proxy.listen(function () { | ||
proxyPort = proxy.address().port; | ||
done(); | ||
}); | ||
}); | ||
var opts = url.parse(link); | ||
opts.agent = agent; | ||
before(function (done) { | ||
// setup target HTTPS server | ||
var options = { | ||
key: fs.readFileSync(__dirname + '/server.key'), | ||
cert: fs.readFileSync(__dirname + '/server.crt') | ||
}; | ||
sslServer = https.createServer(options); | ||
sslServer.listen(function () { | ||
sslServerPort = sslServer.address().port; | ||
done(); | ||
}); | ||
}); | ||
https.get(opts, function (res) { | ||
var data = ''; | ||
res.setEncoding('utf8'); | ||
res.on('data', function (b) { | ||
data += b; | ||
before(function (done) { | ||
// setup SSL HTTP proxy server | ||
var options = { | ||
key: fs.readFileSync(__dirname + '/server.key'), | ||
cert: fs.readFileSync(__dirname + '/server.crt') | ||
}; | ||
sslProxy = Proxy(https.createServer(options)); | ||
sslProxy.listen(function () { | ||
sslProxyPort = sslProxy.address().port; | ||
done(); | ||
}); | ||
}); | ||
// shut down test HTTP server | ||
after(function (done) { | ||
server.once('close', function () { done(); }); | ||
server.close(); | ||
}); | ||
after(function (done) { | ||
proxy.once('close', function () { done(); }); | ||
proxy.close(); | ||
}); | ||
after(function (done) { | ||
sslServer.once('close', function () { done(); }); | ||
sslServer.close(); | ||
}); | ||
after(function (done) { | ||
sslProxy.once('close', function () { done(); }); | ||
sslProxy.close(); | ||
}); | ||
describe('constructor', function () { | ||
it('should throw an Error if no "proxy" argument is given', function () { | ||
assert.throws(function () { | ||
new HttpsProxyAgent(); | ||
}); | ||
res.on('end', function () { | ||
data = JSON.parse(data); | ||
assert.equal('tootallnate', data.username); | ||
done(); | ||
}); | ||
it('should accept a "string" proxy argument', function () { | ||
var agent = new HttpsProxyAgent('http://127.0.0.1:' + proxyPort); | ||
assert.equal('127.0.0.1', agent.proxy.host); | ||
assert.equal(proxyPort, agent.proxy.port); | ||
}); | ||
it('should accept a `url.parse()` result object argument', function () { | ||
var opts = url.parse('http://127.0.0.1:' + proxyPort); | ||
var agent = new HttpsProxyAgent(opts); | ||
assert.equal('127.0.0.1', agent.proxy.host); | ||
assert.equal(proxyPort, agent.proxy.port); | ||
}); | ||
describe('secureEndpoint', function () { | ||
it('should default to `true`', function () { | ||
var agent = new HttpsProxyAgent('http://127.0.0.1:' + proxyPort); | ||
assert.equal(true, agent.secureEndpoint); | ||
}); | ||
it('should be `false` when passed in as an option', function () { | ||
var opts = url.parse('http://127.0.0.1:' + proxyPort); | ||
opts.secureEndpoint = false; | ||
var agent = new HttpsProxyAgent(opts); | ||
assert.equal(false, agent.secureEndpoint); | ||
}); | ||
it('should be `true` when passed in as an option', function () { | ||
var opts = url.parse('http://127.0.0.1:' + proxyPort); | ||
opts.secureEndpoint = true; | ||
var agent = new HttpsProxyAgent(opts); | ||
assert.equal(true, agent.secureEndpoint); | ||
}); | ||
}); | ||
describe('secureProxy', function () { | ||
it('should default to `false`', function () { | ||
var agent = new HttpsProxyAgent({ port: proxyPort }); | ||
assert.equal(false, agent.secureProxy); | ||
}); | ||
it('should be `false` when "http:" protocol is used', function () { | ||
var agent = new HttpsProxyAgent({ port: proxyPort, protocol: 'http:' }); | ||
assert.equal(false, agent.secureProxy); | ||
}); | ||
it('should be `true` when "https:" protocol is used', function () { | ||
var agent = new HttpsProxyAgent({ port: proxyPort, protocol: 'https:' }); | ||
assert.equal(true, agent.secureProxy); | ||
}); | ||
}); | ||
}); | ||
it('should work over an HTTPS proxy', function (done) { | ||
var proxy = process.env.HTTPS_PROXY || process.env.https_proxy || 'https://10.1.10.200:3130'; | ||
proxy = url.parse(proxy); | ||
proxy.rejectUnauthorized = false; | ||
var agent = new HttpsProxyAgent(proxy); | ||
describe('"http" module', function () { | ||
var opts = url.parse(link); | ||
opts.agent = agent; | ||
opts.rejectUnauthorized = false; | ||
beforeEach(function () { | ||
delete proxy.authenticate; | ||
}); | ||
https.get(opts, function (res) { | ||
var data = ''; | ||
res.setEncoding('utf8'); | ||
res.on('data', function (b) { | ||
data += b; | ||
it('should receive the 407 authorization code on the `http.ClientResponse`', function (done) { | ||
// set a proxy authentication function for this test | ||
proxy.authenticate = function (req, fn) { | ||
// reject all requests | ||
fn(null, false); | ||
}; | ||
var proxyUri = process.env.HTTP_PROXY || process.env.http_proxy || 'http://127.0.0.1:' + proxyPort; | ||
var agent = new HttpsProxyAgent(proxyUri); | ||
var opts = {}; | ||
// `host` and `port` don't really matter since the proxy will reject anyways | ||
opts.host = '127.0.0.1'; | ||
opts.port = 80; | ||
opts.agent = agent; | ||
var req = http.get(opts, function (res) { | ||
assert.equal(407, res.statusCode); | ||
assert('proxy-authenticate' in res.headers); | ||
done(); | ||
}); | ||
res.on('end', function () { | ||
data = JSON.parse(data); | ||
assert.equal('tootallnate', data.username); | ||
}); | ||
it('should emit an "error" event on the `http.ClientRequest` if the proxy does not exist', function (done) { | ||
// port 4 is a reserved, but "unassigned" port | ||
var proxyUri = 'http://127.0.0.1:4'; | ||
var agent = new HttpsProxyAgent(proxyUri); | ||
var opts = url.parse('http://nodejs.org'); | ||
opts.agent = agent; | ||
var req = http.get(opts); | ||
req.once('error', function (err) { | ||
assert.equal('ECONNREFUSED', err.code); | ||
req.abort(); | ||
done(); | ||
@@ -71,2 +193,77 @@ }); | ||
describe('"https" module', function () { | ||
it('should work over an HTTP proxy', function (done) { | ||
// set HTTP "request" event handler for this test | ||
sslServer.once('request', function (req, res) { | ||
res.end(JSON.stringify(req.headers)); | ||
}); | ||
var proxy = process.env.HTTP_PROXY || process.env.http_proxy || 'http://127.0.0.1:' + proxyPort; | ||
proxy = url.parse(proxy); | ||
// `rejectUnauthorized` shoudn't *technically* be necessary here, | ||
// but up until node v0.11.6, the `http.Agent` class didn't have | ||
// access to the *entire* request "options" object. Thus, | ||
// `https-proxy-agent` will *also* merge in options you pass here | ||
// to the destination endpoints… | ||
proxy.rejectUnauthorized = false; | ||
var agent = new HttpsProxyAgent(proxy); | ||
var opts = url.parse('https://127.0.0.1:' + sslServerPort); | ||
opts.rejectUnauthorized = false; | ||
opts.agent = agent; | ||
https.get(opts, function (res) { | ||
var data = ''; | ||
res.setEncoding('utf8'); | ||
res.on('data', function (b) { | ||
data += b; | ||
}); | ||
res.on('end', function () { | ||
data = JSON.parse(data); | ||
assert.equal('127.0.0.1:' + sslServerPort, data.host); | ||
done(); | ||
}); | ||
}); | ||
}); | ||
if (version.compare('0.11.3') < 0) { | ||
// This test is disabled on node >= 0.11.3, since it currently segfaults :( | ||
// See: https://github.com/joyent/node/issues/6204 | ||
it('should work over an HTTPS proxy', function (done) { | ||
// set HTTP "request" event handler for this test | ||
sslServer.once('request', function (req, res) { | ||
res.end(JSON.stringify(req.headers)); | ||
}); | ||
var proxy = process.env.HTTPS_PROXY || process.env.https_proxy || 'https://127.0.0.1:' + sslProxyPort; | ||
proxy = url.parse(proxy); | ||
// `rejectUnauthorized` is actually necessary this time since the HTTPS | ||
// proxy server itself is using a self-signed SSL certificate… | ||
proxy.rejectUnauthorized = false; | ||
var agent = new HttpsProxyAgent(proxy); | ||
var opts = url.parse('https://127.0.0.1:' + sslServerPort); | ||
opts.agent = agent; | ||
opts.rejectUnauthorized = false; | ||
https.get(opts, function (res) { | ||
var data = ''; | ||
res.setEncoding('utf8'); | ||
res.on('data', function (b) { | ||
data += b; | ||
}); | ||
res.on('end', function () { | ||
data = JSON.parse(data); | ||
assert.equal('127.0.0.1:' + sslServerPort, data.host); | ||
done(); | ||
}); | ||
}); | ||
}); | ||
} else { | ||
it('should work over an HTTPS proxy'); | ||
} | ||
}); | ||
}); |
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
Network access
Supply chain riskThis module accesses the network.
Found 1 instance in 1 package
Filesystem access
Supply chain riskAccesses the file system, and could potentially read sensitive data.
Found 1 instance in 1 package
Environment variable access
Supply chain riskPackage accesses environment variables, which may be a sign of credential stuffing or data theft.
Found 1 instance in 1 package
25256
11
419
142
3
3
7
5
+ Addeddebug@~0.7.2
+ Addedextend@~1.2.0
+ Addedagent-base@1.0.2(transitive)
+ Addeddebug@0.7.4(transitive)
+ Addedextend@1.2.1(transitive)
- Removedagent-base@0.0.1(transitive)
Updatedagent-base@~1.0.1