From c17ccdcd22df711bc1e994934848708c595450b5 Mon Sep 17 00:00:00 2001 From: stoically Date: Wed, 28 Aug 2019 00:51:51 +0200 Subject: [PATCH 01/22] Update README with actual secureProxy behavior (#65) --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 5e0419cf..20fda1e2 100644 --- a/README.md +++ b/README.md @@ -103,7 +103,7 @@ The `options` argument may either be a string URI of the proxy server to use, or * `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`. + * `protocol` - String - If `https:`, then use TLS to connect to the proxy. * `headers` - Object - Additional HTTP headers to be sent on the HTTP CONNECT method. * Any other options given are passed to the `net.connect()`/`tls.connect()` functions. From d0e3c18079119057b05582cb72d4fda21dfc2546 Mon Sep 17 00:00:00 2001 From: Nathan Rajlich Date: Thu, 3 Oct 2019 21:30:07 -0700 Subject: [PATCH 02/22] Update `proxy` to v1.0.0 --- package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index e035fba4..d0fb0632 100644 --- a/package.json +++ b/package.json @@ -27,8 +27,8 @@ "debug": "^3.1.0" }, "devDependencies": { - "mocha": "^3.4.2", - "proxy": "^0.2.4" + "mocha": "^6.2.0", + "proxy": "1" }, "engines": { "node": ">= 4.5.0" From 46aad0988b471f042856436cf3192b0e09e36fe6 Mon Sep 17 00:00:00 2001 From: Luigi Pinca Date: Fri, 26 Jul 2019 16:30:23 +0200 Subject: [PATCH 03/22] Remove unreachable code Node.js < 4.5.0 is no longer supported as per https://github.com/TooTallNate/node-https-proxy-agent/commit/a2779222. --- index.js | 19 +++---------------- 1 file changed, 3 insertions(+), 16 deletions(-) diff --git a/index.js b/index.js index 0a2fdabe..72936452 100644 --- a/index.js +++ b/index.js @@ -91,7 +91,6 @@ HttpsProxyAgent.prototype.callback = function connect(req, opts, fn) { } function cleanup() { - socket.removeListener('data', ondata); socket.removeListener('end', onend); socket.removeListener('error', onerror); socket.removeListener('close', onclose); @@ -120,11 +119,7 @@ HttpsProxyAgent.prototype.callback = function connect(req, opts, fn) { if (!~str.indexOf('\r\n\r\n')) { // keep buffering debug('have not received end of HTTP headers yet...'); - if (socket.read) { - read(); - } else { - socket.once('data', ondata); - } + read(); return; } @@ -174,11 +169,7 @@ HttpsProxyAgent.prototype.callback = function connect(req, opts, fn) { 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 + if (socket.listenerCount('data') > 0) { socket.emit('data', buffers); } else { // never? @@ -193,11 +184,7 @@ HttpsProxyAgent.prototype.callback = function connect(req, opts, fn) { socket.on('close', onclose); socket.on('end', onend); - if (socket.read) { - read(); - } else { - socket.once('data', ondata); - } + read(); var hostname = opts.host + ':' + opts.port; var msg = 'CONNECT ' + hostname + ' HTTP/1.1\r\n'; From 2629ba63d6cf209db44c3ad7d4e714a1b2ea91b7 Mon Sep 17 00:00:00 2001 From: Luigi Pinca Date: Fri, 26 Jul 2019 16:34:00 +0200 Subject: [PATCH 04/22] Fix compatibility with Node.js >= 10.0.0 Resume the socket after the `'socket'` event is emitted on the `ClientRequest` object. Refs: https://github.com/nodejs/node/issues/24474#issuecomment-511963799 Fixes: https://github.com/TooTallNate/node-https-proxy-agent/pull/58 --- index.js | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/index.js b/index.js index 72936452..64526639 100644 --- a/index.js +++ b/index.js @@ -150,6 +150,7 @@ HttpsProxyAgent.prototype.callback = function connect(req, opts, fn) { } cleanup(); + req.once('socket', resume); fn(null, sock); } else { // some other status code that's not 200... need to re-play the HTTP header @@ -176,6 +177,7 @@ HttpsProxyAgent.prototype.callback = function connect(req, opts, fn) { throw new Error('should not happen...'); } + socket.resume(); // nullify the cached Buffer instance buffers = null; } @@ -211,6 +213,17 @@ HttpsProxyAgent.prototype.callback = function connect(req, opts, fn) { socket.write(msg + '\r\n'); }; +/** + * Resumes a socket. + * + * @param {(net.Socket|tls.Socket)} socket The socket to resume + * @api public + */ + +function resume(socket) { + socket.resume(); +} + function isDefaultPort(port, secure) { return Boolean((!secure && port === 80) || (secure && port === 443)); } From 3535951e482ea52af4888938f59649ed92e81b2b Mon Sep 17 00:00:00 2001 From: Luigi Pinca Date: Fri, 26 Jul 2019 16:44:53 +0200 Subject: [PATCH 05/22] Test on Node.js 10 and 12 --- .travis.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.travis.yml b/.travis.yml index 805d3d50..4472d5b8 100644 --- a/.travis.yml +++ b/.travis.yml @@ -8,6 +8,8 @@ node_js: - "6" - "7" - "8" + - "10" + - "12" install: - PATH="`npm bin`:`npm bin -g`:$PATH" From 2590f76d6a1a39391ef7dd5046d2132bcd85ff61 Mon Sep 17 00:00:00 2001 From: Nathan Rajlich Date: Thu, 3 Oct 2019 22:15:26 -0700 Subject: [PATCH 06/22] =?UTF-8?q?Meh=E2=80=A6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- test/test.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/test/test.js b/test/test.js index b3684958..6ed1e88e 100644 --- a/test/test.js +++ b/test/test.js @@ -71,23 +71,23 @@ describe('HttpsProxyAgent', function () { // shut down test HTTP server after(function (done) { - server.once('close', function () { done(); }); server.close(); + done(); }); after(function (done) { - proxy.once('close', function () { done(); }); proxy.close(); + done(); }); after(function (done) { - sslServer.once('close', function () { done(); }); sslServer.close(); + done(); }); after(function (done) { - sslProxy.once('close', function () { done(); }); sslProxy.close(); + done(); }); describe('constructor', function () { From 590bc8bed1348de6543f8d34d482c7e12a0a21ae Mon Sep 17 00:00:00 2001 From: Nathan Rajlich Date: Thu, 3 Oct 2019 22:17:47 -0700 Subject: [PATCH 07/22] Remove Node 5 and 7 from Travis --- .travis.yml | 2 -- 1 file changed, 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index 4472d5b8..55c74033 100644 --- a/.travis.yml +++ b/.travis.yml @@ -4,9 +4,7 @@ language: node_js node_js: - "4" - - "5" - "6" - - "7" - "8" - "10" - "12" From 2170151b36e9bfd18e119c0a8bf37b90e8f4420e Mon Sep 17 00:00:00 2001 From: Zoltan Kochan Date: Fri, 4 Oct 2019 08:22:22 +0300 Subject: [PATCH 08/22] [TypeScript] Allow `port` to be a string (#72) Fixing a regression spotted in pnpm: https://github.com/pnpm/pnpm/pull/1905 The port in the Node.js URL object is a string, so it is more convenient to use string for the port. https://nodejs.org/api/url.html#url_url_port --- index.d.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/index.d.ts b/index.d.ts index 7b9e2b16..d21a974f 100644 --- a/index.d.ts +++ b/index.d.ts @@ -4,7 +4,7 @@ declare module 'https-proxy-agent' { namespace HttpsProxyAgent { interface HttpsProxyAgentOptions { host: string - port: number + port: number | string secureProxy?: boolean headers?: { [key: string]: string From 6c804a2c919b53d29030340da8b02fd8225fd258 Mon Sep 17 00:00:00 2001 From: Nathan Rajlich Date: Thu, 3 Oct 2019 22:25:15 -0700 Subject: [PATCH 09/22] Remove Node 4 from Travis --- .gitignore | 1 + .travis.yml | 1 - 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index c12f3a80..c650246c 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ /node_modules +/yarn.lock /?.js diff --git a/.travis.yml b/.travis.yml index 55c74033..ebaf7192 100644 --- a/.travis.yml +++ b/.travis.yml @@ -3,7 +3,6 @@ sudo: false language: node_js node_js: - - "4" - "6" - "8" - "10" From 5252bb9355ad12802d7e0846e5e7cf4ced54fc63 Mon Sep 17 00:00:00 2001 From: Luigi Pinca Date: Fri, 4 Oct 2019 22:15:15 +0200 Subject: [PATCH 10/22] =?UTF-8?q?Revert=20"Meh=E2=80=A6"=20(#79)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Revert "Meh…" This reverts commit 2590f76d6a1a39391ef7dd5046d2132bcd85ff61. * Test on macOS and Windows --- .travis.yml | 9 ++++++--- test/test.js | 8 ++++---- 2 files changed, 10 insertions(+), 7 deletions(-) diff --git a/.travis.yml b/.travis.yml index ebaf7192..bb8ff352 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,5 +1,3 @@ -sudo: false - language: node_js node_js: @@ -8,8 +6,13 @@ node_js: - "10" - "12" +os: + - linux + - osx + - windows + install: - - PATH="`npm bin`:`npm bin -g`:$PATH" + # - PATH="`npm bin`:`npm bin -g`:$PATH" # Install dependencies and build - npm install diff --git a/test/test.js b/test/test.js index 6ed1e88e..b3684958 100644 --- a/test/test.js +++ b/test/test.js @@ -71,23 +71,23 @@ describe('HttpsProxyAgent', function () { // shut down test HTTP server after(function (done) { + server.once('close', function () { done(); }); server.close(); - done(); }); after(function (done) { + proxy.once('close', function () { done(); }); proxy.close(); - done(); }); after(function (done) { + sslServer.once('close', function () { done(); }); sslServer.close(); - done(); }); after(function (done) { + sslProxy.once('close', function () { done(); }); sslProxy.close(); - done(); }); describe('constructor', function () { From 36d8cf509f877fa44f4404fce57ebaf9410fe51b Mon Sep 17 00:00:00 2001 From: Nathan Rajlich Date: Mon, 7 Oct 2019 12:53:24 -0700 Subject: [PATCH 11/22] Use an `EventEmitter` to replay failed proxy connect HTTP requests (#77) * Use an `EventEmitter` to replay failed proxy connect HTTP requests This is a fix for https://hackerone.com/reports/541502. Aborts the upstream proxy connection and instead uses a vanilla `EventEmitter` instance to replay the "data" events on to. This way, the node core `http` Client doesn't attempt to write the HTTP request that is intended to go to the destination server to the proxy server. Closes #76. * Adjust comment --- index.js | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/index.js b/index.js index 64526639..2791aea8 100644 --- a/index.js +++ b/index.js @@ -5,6 +5,7 @@ var net = require('net'); var tls = require('tls'); var url = require('url'); +var events = require('events'); var Agent = require('agent-base'); var inherits = require('util').inherits; var debug = require('debug')('https-proxy-agent'); @@ -154,20 +155,32 @@ HttpsProxyAgent.prototype.callback = function connect(req, opts, fn) { 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 + // "data" events onto the socket once the HTTP machinery is attached so + // that the node core `http` can parse and handle the error status code cleanup(); + // the original socket is closed, and a "fake socket" EventEmitter is + // returned instead, so that the proxy doesn't get the HTTP request + // written to it (which may contain `Authorization` headers or other + // sensitive data). + // + // See: https://hackerone.com/reports/541502 + socket.destroy(); + socket = new events.EventEmitter(); + // 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); } } function onsocket(socket) { + debug('replaying proxy buffer for failed request'); + // replay the "buffers" Buffer onto the `socket`, since at this point // the HTTP module machinery has been hooked up for the user if (socket.listenerCount('data') > 0) { @@ -177,7 +190,6 @@ HttpsProxyAgent.prototype.callback = function connect(req, opts, fn) { throw new Error('should not happen...'); } - socket.resume(); // nullify the cached Buffer instance buffers = null; } From 1e34e0d888488bb60c0e8e9860389ef0bf0c9b3c Mon Sep 17 00:00:00 2001 From: Nathan Rajlich Date: Fri, 18 Oct 2019 11:11:41 -0700 Subject: [PATCH 12/22] Use Mocha 5 for Node 4 support --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index d0fb0632..aba7d3d2 100644 --- a/package.json +++ b/package.json @@ -27,7 +27,7 @@ "debug": "^3.1.0" }, "devDependencies": { - "mocha": "^6.2.0", + "mocha": "5", "proxy": "1" }, "engines": { From bb837b984bd868ad69080812eb8eab01181b21d7 Mon Sep 17 00:00:00 2001 From: Nathan Rajlich Date: Fri, 18 Oct 2019 11:11:53 -0700 Subject: [PATCH 13/22] Revert "Remove Node 4 from Travis" This reverts commit 6c804a2c919b53d29030340da8b02fd8225fd258. --- .gitignore | 1 - .travis.yml | 1 + 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index c650246c..c12f3a80 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,2 @@ /node_modules -/yarn.lock /?.js diff --git a/.travis.yml b/.travis.yml index bb8ff352..a161c11e 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,6 +1,7 @@ language: node_js node_js: + - "4" - "6" - "8" - "10" From f5f56fa48ea4d2a61c385938e7753f5c1fe049d6 Mon Sep 17 00:00:00 2001 From: Nathan Rajlich Date: Thu, 3 Oct 2019 22:25:15 -0700 Subject: [PATCH 14/22] Remove Node 4 from Travis --- .gitignore | 1 + .travis.yml | 1 - 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index c12f3a80..c650246c 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ /node_modules +/yarn.lock /?.js diff --git a/.travis.yml b/.travis.yml index a161c11e..bb8ff352 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,7 +1,6 @@ language: node_js node_js: - - "4" - "6" - "8" - "10" From 850b8359b7d0467d721705106b58f4c7cfb937dd Mon Sep 17 00:00:00 2001 From: Nathan Rajlich Date: Fri, 18 Oct 2019 11:16:15 -0700 Subject: [PATCH 15/22] Revert "Use Mocha 5 for Node 4 support" This reverts commit 1e34e0d888488bb60c0e8e9860389ef0bf0c9b3c. --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index aba7d3d2..d0fb0632 100644 --- a/package.json +++ b/package.json @@ -27,7 +27,7 @@ "debug": "^3.1.0" }, "devDependencies": { - "mocha": "5", + "mocha": "^6.2.0", "proxy": "1" }, "engines": { From 0d8e8bfe8b12e6ffe79a39eb93068cdf64c17e78 Mon Sep 17 00:00:00 2001 From: Nathan Rajlich Date: Mon, 21 Oct 2019 18:24:04 -0700 Subject: [PATCH 16/22] 2.2.3 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index d0fb0632..e4adb3e6 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "https-proxy-agent", - "version": "2.2.2", + "version": "2.2.3", "description": "An HTTP(s) proxy `http.Agent` implementation for HTTPS", "main": "./index.js", "types": "./index.d.ts", From a0d4a20458498fc31e5721471bd2b655e992d44b Mon Sep 17 00:00:00 2001 From: Nathan Rajlich Date: Mon, 7 Oct 2019 12:58:18 -0700 Subject: [PATCH 17/22] Add `.editorconfig` file --- .editorconfig | 37 +++++++++++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) create mode 100644 .editorconfig diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 00000000..12b4b9a3 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,37 @@ +root = true + +[*] +indent_style = tab +indent_size = 4 +tab_width = 4 +end_of_line = lf +charset = utf-8 +trim_trailing_whitespace = true +insert_final_newline = true + +[{*.json,*.json.example,*.gyp,*.yml,*.yaml,*.workflow}] +indent_style = space +indent_size = 2 + +[{*.py,*.asm}] +indent_style = space + +[*.py] +indent_size = 4 + +[*.asm] +indent_size = 8 + +[*.md] +trim_trailing_whitespace = false + +# Ideal settings - some plugins might support these. +[*.js] +quote_type = single + +[{*.c,*.cc,*.h,*.hh,*.cpp,*.hpp,*.m,*.mm,*.mpp,*.js,*.java,*.go,*.rs,*.php,*.ng,*.jsx,*.ts,*.d,*.cs,*.swift}] +curly_bracket_next_line = false +spaces_around_operators = true +spaces_around_brackets = outside +# close enough to 1TB +indent_brace_style = K&R From eecea74a1db1c943eaa4f667a561fd47c33da897 Mon Sep 17 00:00:00 2001 From: Nathan Rajlich Date: Mon, 7 Oct 2019 12:58:26 -0700 Subject: [PATCH 18/22] Add `.eslintrc.js` file --- .eslintrc.js | 86 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 86 insertions(+) create mode 100644 .eslintrc.js diff --git a/.eslintrc.js b/.eslintrc.js new file mode 100644 index 00000000..62743f2c --- /dev/null +++ b/.eslintrc.js @@ -0,0 +1,86 @@ +module.exports = { + 'extends': [ + 'airbnb', + 'prettier' + ], + 'parser': '@typescript-eslint/parser', + 'parserOptions': { + 'ecmaVersion': 2018, + 'sourceType': 'module', + 'modules': true + }, + 'plugins': [ + '@typescript-eslint' + ], + 'settings': { + 'import/resolver': { + 'typescript': { + } + } + }, + 'rules': { + 'quotes': [ + 2, + 'single', + { + 'allowTemplateLiterals': true + } + ], + 'class-methods-use-this': 0, + 'consistent-return': 0, + 'func-names': 0, + 'global-require': 0, + 'guard-for-in': 0, + 'import/no-duplicates': 0, + 'import/no-dynamic-require': 0, + 'import/no-extraneous-dependencies': 0, + 'import/prefer-default-export': 0, + 'lines-between-class-members': 0, + 'no-await-in-loop': 0, + 'no-bitwise': 0, + 'no-console': 0, + 'no-continue': 0, + 'no-control-regex': 0, + 'no-empty': 0, + 'no-loop-func': 0, + 'no-nested-ternary': 0, + 'no-param-reassign': 0, + 'no-plusplus': 0, + 'no-restricted-globals': 0, + 'no-restricted-syntax': 0, + 'no-shadow': 0, + 'no-underscore-dangle': 0, + 'no-use-before-define': 0, + 'prefer-const': 0, + 'prefer-destructuring': 0, + 'camelcase': 0, + 'no-unused-vars': 0, // in favor of '@typescript-eslint/no-unused-vars' + // 'indent': 0 // in favor of '@typescript-eslint/indent' + '@typescript-eslint/no-unused-vars': 'warn', + // '@typescript-eslint/indent': ['error', 2] // this might conflict with a lot ongoing changes + '@typescript-eslint/no-array-constructor': 'error', + '@typescript-eslint/adjacent-overload-signatures': 'error', + '@typescript-eslint/class-name-casing': 'error', + '@typescript-eslint/interface-name-prefix': 'error', + '@typescript-eslint/no-empty-interface': 'error', + '@typescript-eslint/no-inferrable-types': 'error', + '@typescript-eslint/no-misused-new': 'error', + '@typescript-eslint/no-namespace': 'error', + '@typescript-eslint/no-non-null-assertion': 'error', + '@typescript-eslint/no-parameter-properties': 'error', + '@typescript-eslint/no-triple-slash-reference': 'error', + '@typescript-eslint/prefer-namespace-keyword': 'error', + '@typescript-eslint/type-annotation-spacing': 'error', + // '@typescript-eslint/array-type': 'error', + // '@typescript-eslint/ban-types': 'error', + // '@typescript-eslint/explicit-function-return-type': 'warn', + // '@typescript-eslint/explicit-member-accessibility': 'error', + // '@typescript-eslint/member-delimiter-style': 'error', + // '@typescript-eslint/no-angle-bracket-type-assertion': 'error', + // '@typescript-eslint/no-explicit-any': 'warn', + // '@typescript-eslint/no-object-literal-type-assertion': 'error', + // '@typescript-eslint/no-use-before-define': 'error', + // '@typescript-eslint/no-var-requires': 'error', + // '@typescript-eslint/prefer-interface': 'error' + } +} From 4296770b6a0e631e3f8e7bd6cfd41ac8e91a3ec4 Mon Sep 17 00:00:00 2001 From: Nathan Rajlich Date: Fri, 25 Oct 2019 12:48:00 -0700 Subject: [PATCH 19/22] Prettier --- index.d.ts | 36 +-- index.js | 384 ++++++++++++++-------------- test/test.js | 687 +++++++++++++++++++++++++++------------------------ 3 files changed, 569 insertions(+), 538 deletions(-) diff --git a/index.d.ts b/index.d.ts index d21a974f..cec35d85 100644 --- a/index.d.ts +++ b/index.d.ts @@ -1,22 +1,22 @@ declare module 'https-proxy-agent' { - import * as https from 'https' + import * as https from 'https'; - namespace HttpsProxyAgent { - interface HttpsProxyAgentOptions { - host: string - port: number | string - secureProxy?: boolean - headers?: { - [key: string]: string - } - [key: string]: any - } - } - - // HttpsProxyAgent doesnt *actually* extend https.Agent, but for my purposes I want it to pretend that it does - class HttpsProxyAgent extends https.Agent { - constructor(opts: HttpsProxyAgent.HttpsProxyAgentOptions | string) - } + namespace HttpsProxyAgent { + interface HttpsProxyAgentOptions { + host: string; + port: number | string; + secureProxy?: boolean; + headers?: { + [key: string]: string; + }; + [key: string]: any; + } + } - export = HttpsProxyAgent + // HttpsProxyAgent doesnt *actually* extend https.Agent, but for my purposes I want it to pretend that it does + class HttpsProxyAgent extends https.Agent { + constructor(opts: HttpsProxyAgent.HttpsProxyAgentOptions | string); + } + + export = HttpsProxyAgent; } diff --git a/index.js b/index.js index 2791aea8..aeb624db 100644 --- a/index.js +++ b/index.js @@ -24,40 +24,42 @@ module.exports = HttpsProxyAgent; */ function HttpsProxyAgent(opts) { - if (!(this instanceof HttpsProxyAgent)) return new HttpsProxyAgent(opts); - if ('string' == typeof opts) opts = url.parse(opts); - if (!opts) - throw new Error( - 'an HTTP(S) proxy server `host` and `port` must be specified!' - ); - debug('creating new HttpsProxyAgent instance: %o', opts); - Agent.call(this, opts); - - var proxy = Object.assign({}, opts); - - // if `true`, then connect to the proxy server over TLS. defaults to `false`. - this.secureProxy = proxy.protocol ? /^https:?$/i.test(proxy.protocol) : false; - - // prefer `hostname` over `host`, and set the `port` if needed - proxy.host = proxy.hostname || proxy.host; - proxy.port = +proxy.port || (this.secureProxy ? 443 : 80); - - // ALPN is supported by Node.js >= v5. - // attempt to negotiate http/1.1 for proxy servers that support http/2 - if (this.secureProxy && !('ALPNProtocols' in proxy)) { - proxy.ALPNProtocols = ['http 1.1'] - } - - if (proxy.host && proxy.path) { - // if both a `host` and `path` are specified then it's most likely the - // result of a `url.parse()` call... we need to remove the `path` portion so - // that `net.connect()` doesn't attempt to open that as a unix socket file. - delete proxy.path; - delete proxy.pathname; - } - - this.proxy = proxy; - this.defaultPort = 443; + if (!(this instanceof HttpsProxyAgent)) return new HttpsProxyAgent(opts); + if ('string' == typeof opts) opts = url.parse(opts); + if (!opts) + throw new Error( + 'an HTTP(S) proxy server `host` and `port` must be specified!' + ); + debug('creating new HttpsProxyAgent instance: %o', opts); + Agent.call(this, opts); + + var proxy = Object.assign({}, opts); + + // if `true`, then connect to the proxy server over TLS. defaults to `false`. + this.secureProxy = proxy.protocol + ? /^https:?$/i.test(proxy.protocol) + : false; + + // prefer `hostname` over `host`, and set the `port` if needed + proxy.host = proxy.hostname || proxy.host; + proxy.port = +proxy.port || (this.secureProxy ? 443 : 80); + + // ALPN is supported by Node.js >= v5. + // attempt to negotiate http/1.1 for proxy servers that support http/2 + if (this.secureProxy && !('ALPNProtocols' in proxy)) { + proxy.ALPNProtocols = ['http 1.1']; + } + + if (proxy.host && proxy.path) { + // if both a `host` and `path` are specified then it's most likely the + // result of a `url.parse()` call... we need to remove the `path` portion so + // that `net.connect()` doesn't attempt to open that as a unix socket file. + delete proxy.path; + delete proxy.pathname; + } + + this.proxy = proxy; + this.defaultPort = 443; } inherits(HttpsProxyAgent, Agent); @@ -68,161 +70,161 @@ inherits(HttpsProxyAgent, Agent); */ HttpsProxyAgent.prototype.callback = function connect(req, opts, fn) { - var proxy = this.proxy; - - // create a socket connection to the proxy server - var socket; - if (this.secureProxy) { - socket = tls.connect(proxy); - } else { - 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() { - var b = socket.read(); - if (b) ondata(b); - else socket.once('readable', read); - } - - function cleanup() { - socket.removeListener('end', onend); - socket.removeListener('error', onerror); - socket.removeListener('close', onclose); - socket.removeListener('readable', read); - } - - function onclose(err) { - debug('onclose had error %o', err); - } - - function onend() { - debug('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...'); - read(); - return; - } - - var firstLine = str.substring(0, str.indexOf('\r\n')); - var statusCode = +firstLine.split(' ')[1]; - debug('got proxy server response: %o', firstLine); - - if (200 == statusCode) { - // 200 Connected status code! - var sock = socket; - - // nullify the buffered data since we won't be needing it - buffers = buffered = null; - - if (opts.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: %o', - opts.host - ); - opts.socket = socket; - opts.servername = opts.servername || opts.host; - opts.host = null; - opts.hostname = null; - opts.port = null; - sock = tls.connect(opts); - } - - cleanup(); - req.once('socket', resume); - 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 node core `http` can parse and handle the error status code - cleanup(); - - // the original socket is closed, and a "fake socket" EventEmitter is - // returned instead, so that the proxy doesn't get the HTTP request - // written to it (which may contain `Authorization` headers or other - // sensitive data). - // - // See: https://hackerone.com/reports/541502 - socket.destroy(); - socket = new events.EventEmitter(); - - // 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); - } - } - - function onsocket(socket) { - debug('replaying proxy buffer for failed request'); - - // replay the "buffers" Buffer onto the `socket`, since at this point - // the HTTP module machinery has been hooked up for the user - if (socket.listenerCount('data') > 0) { - socket.emit('data', buffers); - } else { - // never? - throw new Error('should not happen...'); - } - - // nullify the cached Buffer instance - buffers = null; - } - - socket.on('error', onerror); - socket.on('close', onclose); - socket.on('end', onend); - - read(); - - var hostname = opts.host + ':' + opts.port; - var msg = 'CONNECT ' + hostname + ' HTTP/1.1\r\n'; - - var headers = Object.assign({}, proxy.headers); - if (proxy.auth) { - headers['Proxy-Authorization'] = - 'Basic ' + Buffer.from(proxy.auth).toString('base64'); - } - - // the Host header should only include the port - // number when it is a non-standard port - var host = opts.host; - if (!isDefaultPort(opts.port, opts.secureEndpoint)) { - host += ':' + opts.port; - } - headers['Host'] = host; - - headers['Connection'] = 'close'; - Object.keys(headers).forEach(function(name) { - msg += name + ': ' + headers[name] + '\r\n'; - }); - - socket.write(msg + '\r\n'); + var proxy = this.proxy; + + // create a socket connection to the proxy server + var socket; + if (this.secureProxy) { + socket = tls.connect(proxy); + } else { + 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() { + var b = socket.read(); + if (b) ondata(b); + else socket.once('readable', read); + } + + function cleanup() { + socket.removeListener('end', onend); + socket.removeListener('error', onerror); + socket.removeListener('close', onclose); + socket.removeListener('readable', read); + } + + function onclose(err) { + debug('onclose had error %o', err); + } + + function onend() { + debug('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...'); + read(); + return; + } + + var firstLine = str.substring(0, str.indexOf('\r\n')); + var statusCode = +firstLine.split(' ')[1]; + debug('got proxy server response: %o', firstLine); + + if (200 == statusCode) { + // 200 Connected status code! + var sock = socket; + + // nullify the buffered data since we won't be needing it + buffers = buffered = null; + + if (opts.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: %o', + opts.host + ); + opts.socket = socket; + opts.servername = opts.servername || opts.host; + opts.host = null; + opts.hostname = null; + opts.port = null; + sock = tls.connect(opts); + } + + cleanup(); + req.once('socket', resume); + 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 node core `http` can parse and handle the error status code + cleanup(); + + // the original socket is closed, and a "fake socket" EventEmitter is + // returned instead, so that the proxy doesn't get the HTTP request + // written to it (which may contain `Authorization` headers or other + // sensitive data). + // + // See: https://hackerone.com/reports/541502 + socket.destroy(); + socket = new events.EventEmitter(); + + // 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); + } + } + + function onsocket(socket) { + debug('replaying proxy buffer for failed request'); + + // replay the "buffers" Buffer onto the `socket`, since at this point + // the HTTP module machinery has been hooked up for the user + if (socket.listenerCount('data') > 0) { + socket.emit('data', buffers); + } else { + // never? + throw new Error('should not happen...'); + } + + // nullify the cached Buffer instance + buffers = null; + } + + socket.on('error', onerror); + socket.on('close', onclose); + socket.on('end', onend); + + read(); + + var hostname = opts.host + ':' + opts.port; + var msg = 'CONNECT ' + hostname + ' HTTP/1.1\r\n'; + + var headers = Object.assign({}, proxy.headers); + if (proxy.auth) { + headers['Proxy-Authorization'] = + 'Basic ' + Buffer.from(proxy.auth).toString('base64'); + } + + // the Host header should only include the port + // number when it is a non-standard port + var host = opts.host; + if (!isDefaultPort(opts.port, opts.secureEndpoint)) { + host += ':' + opts.port; + } + headers['Host'] = host; + + headers['Connection'] = 'close'; + Object.keys(headers).forEach(function(name) { + msg += name + ': ' + headers[name] + '\r\n'; + }); + + socket.write(msg + '\r\n'); }; /** @@ -233,9 +235,9 @@ HttpsProxyAgent.prototype.callback = function connect(req, opts, fn) { */ function resume(socket) { - socket.resume(); + socket.resume(); } function isDefaultPort(port, secure) { - return Boolean((!secure && port === 80) || (secure && port === 443)); + return Boolean((!secure && port === 80) || (secure && port === 443)); } diff --git a/test/test.js b/test/test.js index b3684958..513aae0a 100644 --- a/test/test.js +++ b/test/test.js @@ -1,4 +1,3 @@ - /** * Module dependencies. */ @@ -11,332 +10,362 @@ var assert = require('assert'); var Proxy = require('proxy'); var HttpsProxyAgent = require('../'); -describe('HttpsProxyAgent', function () { - - var server; - var serverPort; - - var sslServer; - var sslServerPort; - - 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(); - }); - }); - - before(function (done) { - // setup HTTP proxy server - proxy = Proxy(); - proxy.listen(function () { - proxyPort = proxy.address().port; - done(); - }); - }); - - before(function (done) { - // setup target HTTPS server - var options = { - key: fs.readFileSync(__dirname + '/ssl-cert-snakeoil.key'), - cert: fs.readFileSync(__dirname + '/ssl-cert-snakeoil.pem') - }; - sslServer = https.createServer(options); - sslServer.listen(function () { - sslServerPort = sslServer.address().port; - done(); - }); - }); - - before(function (done) { - // setup SSL HTTP proxy server - var options = { - key: fs.readFileSync(__dirname + '/ssl-cert-snakeoil.key'), - cert: fs.readFileSync(__dirname + '/ssl-cert-snakeoil.pem') - }; - 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(); - }); - }); - 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); - }); - it('should set a `defaultPort` property', function () { - var opts = url.parse("http://127.0.0.1:" + proxyPort); - var agent = new HttpsProxyAgent(opts); - assert.equal(443, agent.defaultPort); - }); - 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 be `true` when "https" protocol is used', function () { - var agent = new HttpsProxyAgent({ port: proxyPort, protocol: 'https' }); - assert.equal(true, agent.secureProxy); - }); - }); - }); - - describe('"http" module', function () { - - beforeEach(function () { - delete proxy.authenticate; - }); - - it('should work over an HTTP proxy', function (done) { - server.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; - var agent = new HttpsProxyAgent(proxy); - - var opts = url.parse('http://127.0.0.1:' + serverPort); - opts.agent = agent; - - var req = http.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:' + serverPort, data.host); - done(); - }); - }); - req.once('error', done); - }); - it('should work over an HTTPS proxy', function (done) { - server.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); - proxy.rejectUnauthorized = false; - var agent = new HttpsProxyAgent(proxy); - - var opts = url.parse('http://127.0.0.1:' + serverPort); - opts.agent = agent; - - http.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:' + serverPort, data.host); - done(); - }); - }); - }); - 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(); - }); - }); - 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(); - }); - }); - - it('should allow custom proxy "headers"', function (done) { - server.once('connect', function (req, socket, head) { - assert.equal('CONNECT', req.method); - assert.equal('bar', req.headers.foo); - socket.destroy(); - done(); - }); - - var uri = 'http://127.0.0.1:' + serverPort; - var proxyOpts = url.parse(uri); - proxyOpts.headers = { - 'Foo': 'bar' - }; - var agent = new HttpsProxyAgent(proxyOpts); - - 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; - - http.get(opts); - }); - - }); - - describe('"https" module', function () { - it('should work over an HTTP proxy', function (done) { - 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; - 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(); - }); - }); - }); - - it('should work over an HTTPS proxy', function (done) { - 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); - 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(); - }); - }); - }); - - it('should not send a port number for the default port', function (done) { - 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); - proxy.rejectUnauthorized = false; - var agent = new HttpsProxyAgent(proxy); - agent.defaultPort = sslServerPort; - - 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", data.host); - done(); - }); - }); - }); - - }); - +describe('HttpsProxyAgent', function() { + var server; + var serverPort; + + var sslServer; + var sslServerPort; + + 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(); + }); + }); + + before(function(done) { + // setup HTTP proxy server + proxy = Proxy(); + proxy.listen(function() { + proxyPort = proxy.address().port; + done(); + }); + }); + + before(function(done) { + // setup target HTTPS server + var options = { + key: fs.readFileSync(__dirname + '/ssl-cert-snakeoil.key'), + cert: fs.readFileSync(__dirname + '/ssl-cert-snakeoil.pem') + }; + sslServer = https.createServer(options); + sslServer.listen(function() { + sslServerPort = sslServer.address().port; + done(); + }); + }); + + before(function(done) { + // setup SSL HTTP proxy server + var options = { + key: fs.readFileSync(__dirname + '/ssl-cert-snakeoil.key'), + cert: fs.readFileSync(__dirname + '/ssl-cert-snakeoil.pem') + }; + 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(); + }); + }); + 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); + }); + it('should set a `defaultPort` property', function() { + var opts = url.parse('http://127.0.0.1:' + proxyPort); + var agent = new HttpsProxyAgent(opts); + assert.equal(443, agent.defaultPort); + }); + 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 be `true` when "https" protocol is used', function() { + var agent = new HttpsProxyAgent({ + port: proxyPort, + protocol: 'https' + }); + assert.equal(true, agent.secureProxy); + }); + }); + }); + + describe('"http" module', function() { + beforeEach(function() { + delete proxy.authenticate; + }); + + it('should work over an HTTP proxy', function(done) { + server.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; + var agent = new HttpsProxyAgent(proxy); + + var opts = url.parse('http://127.0.0.1:' + serverPort); + opts.agent = agent; + + var req = http.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:' + serverPort, data.host); + done(); + }); + }); + req.once('error', done); + }); + it('should work over an HTTPS proxy', function(done) { + server.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); + proxy.rejectUnauthorized = false; + var agent = new HttpsProxyAgent(proxy); + + var opts = url.parse('http://127.0.0.1:' + serverPort); + opts.agent = agent; + + http.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:' + serverPort, data.host); + done(); + }); + }); + }); + 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(); + }); + }); + 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(); + }); + }); + + it('should allow custom proxy "headers"', function(done) { + server.once('connect', function(req, socket, head) { + assert.equal('CONNECT', req.method); + assert.equal('bar', req.headers.foo); + socket.destroy(); + done(); + }); + + var uri = 'http://127.0.0.1:' + serverPort; + var proxyOpts = url.parse(uri); + proxyOpts.headers = { + Foo: 'bar' + }; + var agent = new HttpsProxyAgent(proxyOpts); + + 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; + + http.get(opts); + }); + }); + + describe('"https" module', function() { + it('should work over an HTTP proxy', function(done) { + 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; + 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(); + }); + }); + }); + + it('should work over an HTTPS proxy', function(done) { + 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); + 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(); + }); + }); + }); + + it('should not send a port number for the default port', function(done) { + 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); + proxy.rejectUnauthorized = false; + var agent = new HttpsProxyAgent(proxy); + agent.defaultPort = sslServerPort; + + 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', data.host); + done(); + }); + }); + }); + }); }); From 34ea8841922fb6447563b0521f972ac3a6062303 Mon Sep 17 00:00:00 2001 From: Luigi Pinca Date: Thu, 17 Oct 2019 03:00:17 +0200 Subject: [PATCH 20/22] Use a `net.Socket` instead of a plain `EventEmitter` for replaying proxy errors (#83) * Run CI on pull requests * Use a `Duplex` instead of a plain `EventEmitter` Fixes: https://github.com/TooTallNate/node-https-proxy-agent/issues/81 * Use a new and closed `net.Socket` instead of a `Duplex` --- index.js | 17 ++++++++--------- test/test.js | 19 +++++++++++++++++++ 2 files changed, 27 insertions(+), 9 deletions(-) diff --git a/index.js b/index.js index aeb624db..916e333e 100644 --- a/index.js +++ b/index.js @@ -2,10 +2,11 @@ * Module dependencies. */ +var assert = require('assert'); var net = require('net'); var tls = require('tls'); var url = require('url'); -var events = require('events'); +var stream = require('stream'); var Agent = require('agent-base'); var inherits = require('util').inherits; var debug = require('debug')('https-proxy-agent'); @@ -161,14 +162,16 @@ HttpsProxyAgent.prototype.callback = function connect(req, opts, fn) { // that the node core `http` can parse and handle the error status code cleanup(); - // the original socket is closed, and a "fake socket" EventEmitter is + // the original socket is closed, and a new closed socket is // returned instead, so that the proxy doesn't get the HTTP request // written to it (which may contain `Authorization` headers or other // sensitive data). // // See: https://hackerone.com/reports/541502 socket.destroy(); - socket = new events.EventEmitter(); + socket = new net.Socket(); + socket.readable = true; + // save a reference to the concat'd Buffer for the `onsocket` callback buffers = buffered; @@ -182,15 +185,11 @@ HttpsProxyAgent.prototype.callback = function connect(req, opts, fn) { function onsocket(socket) { debug('replaying proxy buffer for failed request'); + assert(socket.listenerCount('data') > 0); // replay the "buffers" Buffer onto the `socket`, since at this point // the HTTP module machinery has been hooked up for the user - if (socket.listenerCount('data') > 0) { - socket.emit('data', buffers); - } else { - // never? - throw new Error('should not happen...'); - } + socket.push(buffers); // nullify the cached Buffer instance buffers = null; diff --git a/test/test.js b/test/test.js index 513aae0a..61a02320 100644 --- a/test/test.js +++ b/test/test.js @@ -234,6 +234,25 @@ describe('HttpsProxyAgent', function() { done(); }); }); + it('should not error if the proxy responds with 407 and the request is aborted', function(done) { + proxy.authenticate = function(req, fn) { + fn(null, false); + }; + + const proxyUri = + process.env.HTTP_PROXY || + process.env.http_proxy || + 'http://127.0.0.1:' + proxyPort; + + const req = http.get({ + agent: new HttpsProxyAgent(proxyUri) + }, function(res) { + assert.equal(407, res.statusCode); + req.abort(); + }); + + req.on('abort', done); + }); 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'; From 9fdcd47bd813e9979ee57920c69e2ee2e0683cd4 Mon Sep 17 00:00:00 2001 From: Nathan Rajlich Date: Wed, 16 Oct 2019 18:25:32 -0700 Subject: [PATCH 21/22] Remove unused `stream` module --- index.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/index.js b/index.js index 916e333e..817a0a92 100644 --- a/index.js +++ b/index.js @@ -2,11 +2,10 @@ * Module dependencies. */ -var assert = require('assert'); var net = require('net'); var tls = require('tls'); var url = require('url'); -var stream = require('stream'); +var assert = require('assert'); var Agent = require('agent-base'); var inherits = require('util').inherits; var debug = require('debug')('https-proxy-agent'); From 4c4cce8cb60fd3ac6171e4428f972698eb49f45a Mon Sep 17 00:00:00 2001 From: Nathan Rajlich Date: Fri, 25 Oct 2019 13:12:09 -0700 Subject: [PATCH 22/22] 2.2.4 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index e4adb3e6..2ff1870a 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "https-proxy-agent", - "version": "2.2.3", + "version": "2.2.4", "description": "An HTTP(s) proxy `http.Agent` implementation for HTTPS", "main": "./index.js", "types": "./index.d.ts",