diff --git a/package-lock.json b/package-lock.json index 1932e9a8..68f74f04 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,17 +1,19 @@ { "name": "@revopush/code-push", - "version": "4.6.0", + "version": "4.7.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@revopush/code-push", - "version": "4.6.0", + "version": "4.7.0", "license": "MIT", "dependencies": { - "proxy-agent": "^6.3.0", + "http-proxy-agent": "^7.0.2", + "https-proxy-agent": "^7.0.6", "recursive-fs": "^2.1.0", "slash": "^3.0.0", + "socks-proxy-agent": "^8.0.5", "superagent": "^10.3.0", "yazl": "^2.5.1" }, @@ -73,11 +75,6 @@ "node": ">=14" } }, - "node_modules/@tootallnate/quickjs-emscripten": { - "version": "0.23.0", - "resolved": "https://registry.npmjs.org/@tootallnate/quickjs-emscripten/-/quickjs-emscripten-0.23.0.tgz", - "integrity": "sha512-C5Mc6rdnsaJDjO3UpGW/CQTHtCKaYlScZTly4JIu97Jxo/odCiH0ITnDXSJPTOrEKk/ycSZ0AOgTmkDtkOsvIA==" - }, "node_modules/@types/cookiejar": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@types/cookiejar/-/cookiejar-2.1.5.tgz", @@ -137,6 +134,7 @@ "version": "7.1.4", "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", + "license": "MIT", "engines": { "node": ">= 14" } @@ -179,17 +177,6 @@ "resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz", "integrity": "sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==" }, - "node_modules/ast-types": { - "version": "0.13.4", - "resolved": "https://registry.npmjs.org/ast-types/-/ast-types-0.13.4.tgz", - "integrity": "sha512-x1FCFnFifvYDDzTaLII71vG5uvDwgtmDTEVWAxrgeiR8VjMONcCXJx7E+USjDtHlwFmt9MysbqgF9b9Vjr6w+w==", - "dependencies": { - "tslib": "^2.0.1" - }, - "engines": { - "node": ">=4" - } - }, "node_modules/asynckit": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", @@ -201,14 +188,6 @@ "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", "dev": true }, - "node_modules/basic-ftp": { - "version": "5.3.1", - "resolved": "https://registry.npmjs.org/basic-ftp/-/basic-ftp-5.3.1.tgz", - "integrity": "sha512-bopVNp6ugyA150DDuZfPFdt1KZ5a94ZDiwX4hMgZDzF+GttD80lEy8kj98kbyhLXnPvhtIo93mdnLIjpCAeeOw==", - "engines": { - "node": ">=10.0.0" - } - }, "node_modules/brace-expansion": { "version": "2.1.4", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.4.tgz", @@ -449,14 +428,6 @@ "node": ">= 8" } }, - "node_modules/data-uri-to-buffer": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-6.0.2.tgz", - "integrity": "sha512-7hvf7/GW8e86rW0ptuwS3OcBGDjIi6SZva7hCyWC0yYry2cOPmLIjXAUHI6DK2HsnwJd9ifmt57i8eV2n4YNpw==", - "engines": { - "node": ">= 14" - } - }, "node_modules/debug": { "version": "4.4.3", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", @@ -485,19 +456,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/degenerator": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/degenerator/-/degenerator-5.0.1.tgz", - "integrity": "sha512-TllpMR/t0M5sqCXfj85i4XaAzxmS5tVA16dqvdkMwGmzI+dXLXnw3J+3Vdv7VKw+ThlTMboK6i9rnZ6Nntj5CQ==", - "dependencies": { - "ast-types": "^0.13.4", - "escodegen": "^2.1.0", - "esprima": "^4.0.1" - }, - "engines": { - "node": ">= 14" - } - }, "node_modules/delayed-stream": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", @@ -611,54 +569,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/escodegen": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-2.1.0.tgz", - "integrity": "sha512-2NlIDTwUWJN0mRPQOdtQBzbUHvdGY2P1VXSyU83Q3xKxM7WHX2Ql8dKq782Q9TgQUNOLEzEYu9bzLNj1q88I5w==", - "dependencies": { - "esprima": "^4.0.1", - "estraverse": "^5.2.0", - "esutils": "^2.0.2" - }, - "bin": { - "escodegen": "bin/escodegen.js", - "esgenerate": "bin/esgenerate.js" - }, - "engines": { - "node": ">=6.0" - }, - "optionalDependencies": { - "source-map": "~0.6.1" - } - }, - "node_modules/esprima": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", - "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", - "bin": { - "esparse": "bin/esparse.js", - "esvalidate": "bin/esvalidate.js" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/estraverse": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", - "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", - "engines": { - "node": ">=4.0" - } - }, - "node_modules/esutils": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", - "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/fast-safe-stringify": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/fast-safe-stringify/-/fast-safe-stringify-2.1.1.tgz", @@ -794,19 +704,6 @@ "node": ">= 0.4" } }, - "node_modules/get-uri": { - "version": "6.0.5", - "resolved": "https://registry.npmjs.org/get-uri/-/get-uri-6.0.5.tgz", - "integrity": "sha512-b1O07XYq8eRuVzBNgJLstU6FYc1tS6wnMtF1I1D9lE8LxZSOGZ7LhxN54yPP6mGw5f2CkXY2BQUL9Fx41qvcIg==", - "dependencies": { - "basic-ftp": "^5.0.2", - "data-uri-to-buffer": "^6.0.2", - "debug": "^4.3.4" - }, - "engines": { - "node": ">= 14" - } - }, "node_modules/glob": { "version": "10.5.0", "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", @@ -897,6 +794,7 @@ "version": "7.0.2", "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", + "license": "MIT", "dependencies": { "agent-base": "^7.1.0", "debug": "^4.3.4" @@ -909,6 +807,7 @@ "version": "7.0.6", "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", + "license": "MIT", "dependencies": { "agent-base": "^7.1.2", "debug": "4" @@ -944,9 +843,9 @@ } }, "node_modules/ip-address": { - "version": "10.7.2", - "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.7.2.tgz", - "integrity": "sha512-7H/2gFSIitxc0hG3nOI1glS8QLo/EHBFFLk8vEUjXY/xu0AdL8jZ9U1IzO2PUm0d2D/ofQcAifb0g6OBkt8U7w==", + "version": "10.7.3", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.7.3.tgz", + "integrity": "sha512-A1kdq/tSb5QjvKvAMgIoEvDBIgL7qaqVP/jkvSwYYRZ9iEzvPpopxp2wQfu3SuZRHtpHNxMn8Fs0bS+gf5Xmwg==", "license": "MIT", "engines": { "node": ">= 12" @@ -1207,14 +1106,6 @@ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" }, - "node_modules/netmask": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/netmask/-/netmask-2.1.1.tgz", - "integrity": "sha512-eonl3sLUha+S1GzTPxychyhnUzKyeQkZ7jLjKrBagJgPla13F+uQ71HgpFefyHgqrjEbCPkDArxYsjY8/+gLKA==", - "engines": { - "node": ">= 0.4.0" - } - }, "node_modules/object-inspect": { "version": "1.13.4", "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", @@ -1264,36 +1155,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/pac-proxy-agent": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/pac-proxy-agent/-/pac-proxy-agent-7.2.0.tgz", - "integrity": "sha512-TEB8ESquiLMc0lV8vcd5Ql/JAKAoyzHFXaStwjkzpOpC5Yv+pIzLfHvjTSdf3vpa2bMiUQrg9i6276yn8666aA==", - "dependencies": { - "@tootallnate/quickjs-emscripten": "^0.23.0", - "agent-base": "^7.1.2", - "debug": "^4.3.4", - "get-uri": "^6.0.1", - "http-proxy-agent": "^7.0.0", - "https-proxy-agent": "^7.0.6", - "pac-resolver": "^7.0.1", - "socks-proxy-agent": "^8.0.5" - }, - "engines": { - "node": ">= 14" - } - }, - "node_modules/pac-resolver": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/pac-resolver/-/pac-resolver-7.0.1.tgz", - "integrity": "sha512-5NPgf87AT2STgwa2ntRMr45jTKrYBGkVU36yT0ig/n/GMAa3oPqhZfIQ2kMEimReg0+t9kZViDVZ83qfVUlckg==", - "dependencies": { - "degenerator": "^5.0.0", - "netmask": "^2.0.2" - }, - "engines": { - "node": ">= 14" - } - }, "node_modules/package-json-from-dist": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", @@ -1355,37 +1216,6 @@ "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", "dev": true }, - "node_modules/proxy-agent": { - "version": "6.5.0", - "resolved": "https://registry.npmjs.org/proxy-agent/-/proxy-agent-6.5.0.tgz", - "integrity": "sha512-TmatMXdr2KlRiA2CyDu8GqR8EjahTG3aY3nXjdzFyoZbmB8hrBsTyMezhULIXKnC0jpfjlmiZ3+EaCzoInSu/A==", - "dependencies": { - "agent-base": "^7.1.2", - "debug": "^4.3.4", - "http-proxy-agent": "^7.0.1", - "https-proxy-agent": "^7.0.6", - "lru-cache": "^7.14.1", - "pac-proxy-agent": "^7.1.0", - "proxy-from-env": "^1.1.0", - "socks-proxy-agent": "^8.0.5" - }, - "engines": { - "node": ">= 14" - } - }, - "node_modules/proxy-agent/node_modules/lru-cache": { - "version": "7.18.3", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz", - "integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==", - "engines": { - "node": ">=12" - } - }, - "node_modules/proxy-from-env": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", - "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==" - }, "node_modules/qs": { "version": "6.16.0", "resolved": "https://registry.npmjs.org/qs/-/qs-6.16.0.tgz", @@ -1668,15 +1498,17 @@ "version": "4.2.0", "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz", "integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==", + "license": "MIT", "engines": { "node": ">= 6.0.0", "npm": ">= 3.0.0" } }, "node_modules/socks": { - "version": "2.8.9", - "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.9.tgz", - "integrity": "sha512-LJhUYUvItdQ0LkJTmPeaEObWXAqFyfmP85x0tch/ez9cahmhlBBLbIqDFnvBnUJGagb0JbIQrkBs1wJ+yRYpEw==", + "version": "2.8.10", + "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.10.tgz", + "integrity": "sha512-e0VyvkVTwVYViNovRkZ9aodhxVlyoMn7eJhVUPxZ+eK9P/7CBkxvvsBOHqFPEH416726W8tLXXXjKwqgTErrCQ==", + "license": "MIT", "dependencies": { "ip-address": "^10.1.1", "smart-buffer": "^4.2.0" @@ -1690,6 +1522,7 @@ "version": "8.0.5", "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-8.0.5.tgz", "integrity": "sha512-HehCEsotFqbPW9sJ8WVYB6UbmIMv7kUUORIF2Nncq4VQvBfNBLibW9YZR5dlYCSUhwcD628pRllm7n+E+YTzJw==", + "license": "MIT", "dependencies": { "agent-base": "^7.1.2", "debug": "^4.3.4", @@ -1699,15 +1532,6 @@ "node": ">= 14" } }, - "node_modules/source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "optional": true, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/string-width": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", @@ -1874,11 +1698,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/tslib": { - "version": "2.8.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", - "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==" - }, "node_modules/typescript": { "version": "5.9.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", diff --git a/package.json b/package.json index 25d95c3f..3b7ebbda 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@revopush/code-push", - "version": "4.6.0", + "version": "4.7.0", "description": "Management SDK for the CodePush service", "main": "script/index.js", "types": "script/index.d.ts", @@ -24,9 +24,11 @@ "license": "MIT", "homepage": "https://github.com/revopush/code-push", "dependencies": { - "proxy-agent": "^6.3.0", + "http-proxy-agent": "^7.0.2", + "https-proxy-agent": "^7.0.6", "recursive-fs": "^2.1.0", "slash": "^3.0.0", + "socks-proxy-agent": "^8.0.5", "superagent": "^10.3.0", "yazl": "^2.5.1" }, diff --git a/src/test/request-manager.ts b/src/test/request-manager.ts new file mode 100644 index 00000000..2c63adf4 --- /dev/null +++ b/src/test/request-manager.ts @@ -0,0 +1,87 @@ +import assert from "assert"; +import * as http from "http"; +import { AddressInfo } from "net"; +import { HttpProxyAgent } from "http-proxy-agent"; +import { HttpsProxyAgent } from "https-proxy-agent"; +import { SocksProxyAgent } from "socks-proxy-agent"; + +import RequestManager = require("../utils/request-manager"); + +function proxyAgentFor(proxy: string, targetUrl: string): any { + return (new RequestManager("dummyAccessKey", null, "http://localhost", proxy)).getProxyAgent(targetUrl); +} + +describe("Request Manager proxy agents", () => { + it("uses HttpProxyAgent for http targets and HttpsProxyAgent for https targets behind an http(s) proxy", () => { + for (const proxy of ["http://proxy.local:8080", "https://proxy.local:8443"]) { + assert(proxyAgentFor(proxy, "http://example.com/x") instanceof HttpProxyAgent); + assert(proxyAgentFor(proxy, "https://example.com/x") instanceof HttpsProxyAgent); + } + }); + + it("uses SocksProxyAgent for socks proxies", () => { + for (const protocol of ["socks", "socks4", "socks4a", "socks5", "socks5h"]) { + assert(proxyAgentFor(`${protocol}://proxy.local:1080`, "https://example.com/x") instanceof SocksProxyAgent); + } + }); + + it("reuses the same agents across requests", () => { + const manager: any = new RequestManager("dummyAccessKey", null, "http://localhost", "http://proxy.local:8080"); + assert.strictEqual(manager.getProxyAgent("https://example.com/a"), manager.getProxyAgent("https://example.com/b")); + }); + + it("rejects requests when the proxy protocol is unsupported", () => { + const manager = new RequestManager("dummyAccessKey", null, "http://localhost", "pac+http://proxy.local/proxy.pac"); + return manager.get("/endpoint").then( + () => assert.fail("Expected the request to be rejected"), + (error: any) => assert(/Unsupported protocol for proxy URL/.test(error.message)) + ); + }); + + it("does not leak proxy credentials in the unsupported protocol error", () => { + const manager = new RequestManager("dummyAccessKey", null, "http://localhost", "pac+http://user:secret@proxy.local/proxy.pac"); + return manager.get("/endpoint").then( + () => assert.fail("Expected the request to be rejected"), + (error: any) => { + assert(/Unsupported protocol for proxy URL/.test(error.message)); + assert(!/secret/.test(error.message), `Proxy password leaked: ${error.message}`); + } + ); + }); + + it("does not leak proxy credentials when the proxy URL is malformed", () => { + const manager = new RequestManager("dummyAccessKey", null, "http://localhost", "http://user:secret@[proxy.local:8080"); + return manager.get("/endpoint").then( + () => assert.fail("Expected the request to be rejected"), + (error: any) => assert.strictEqual(error.message, "Invalid proxy URL") + ); + }); + + it("re-picks the proxy agent when a redirect switches from http to https", () => { + const connectTargets: string[] = []; + // Plain-HTTP requests reach the proxy directly: redirect them to https. + const proxy = http.createServer((req, res) => { + res.writeHead(302, { Location: "https://redirected.invalid/target" }); + res.end(); + }); + // HTTPS requests tunnel via CONNECT: record the target, then drop the tunnel. + proxy.on("connect", (req: http.IncomingMessage, socket: any) => { + connectTargets.push(req.url); + socket.destroy(); + }); + + const closeProxy = () => new Promise((resolve) => proxy.close(() => resolve())); + + return new Promise((resolve) => proxy.listen(0, "127.0.0.1", () => resolve())) + .then(() => { + const proxyUrl = `http://127.0.0.1:${(proxy.address()).port}`; + const manager = new RequestManager("dummyAccessKey", null, "http://origin.invalid", proxyUrl); + return manager.get("/start").then( + () => assert.fail("Expected the tunnelled request to fail"), + () => { /* the proxy drops the tunnel */ } + ); + }) + .then(() => assert.deepStrictEqual(connectTargets, ["redirected.invalid:443"])) + .then(closeProxy, (err: any) => closeProxy().then(() => { throw err; })); + }); +}); diff --git a/src/utils/request-manager.ts b/src/utils/request-manager.ts index e9b18ca5..f135aa81 100644 --- a/src/utils/request-manager.ts +++ b/src/utils/request-manager.ts @@ -1,5 +1,8 @@ import superagent = require("superagent"); -import { ProxyAgent } from "proxy-agent"; +import { Agent } from "http"; +import { HttpProxyAgent } from "http-proxy-agent"; +import { HttpsProxyAgent } from "https-proxy-agent"; +import { SocksProxyAgent } from "socks-proxy-agent"; import { CodePushUnauthorizedError } from "../script/code-push-error" import { CodePushError, Headers } from "../script/types"; @@ -8,6 +11,11 @@ interface JsonResponse { body?: any; } +interface ProxyAgents { + http: Agent; + https: Agent; +} + class RequestManager { public static SERVER_URL = "https://api.appcenter.ms/v0.1"; @@ -21,6 +29,7 @@ class RequestManager { private _serverUrl: string; private _customHeaders: Headers; private _proxy: string; + private _proxyAgents: ProxyAgents; constructor(accessKey: string, customHeaders?: Headers, serverUrl?: string, proxy?: string) { if (!accessKey) throw new CodePushUnauthorizedError("A token must be specified."); @@ -49,10 +58,20 @@ class RequestManager { private makeApiRequest(method: string, endpoint: string, requestBody: string, expectResponseBody: boolean, contentType: string): Promise { return new Promise((resolve, reject) => { - var request: superagent.Request = (superagent)[method](this._serverUrl + endpoint); + var url: string = this._serverUrl + endpoint; + var request: superagent.Request = (superagent)[method](url); if (this._proxy) { - (request).agent(new ProxyAgent({ getProxyForUrl: () => this._proxy })) + var proxiedRequest = request; + try { + proxiedRequest.agent(this.getProxyAgent(url)); + } catch (err) { + reject(this.getCodePushError(err)); + return; + } + + // superagent reuses the agent on redirect, so re-pick it in case the scheme changed. + proxiedRequest.on("redirect", () => proxiedRequest.agent(this.getProxyAgent(proxiedRequest.url))); } this.attachCredentials(request); @@ -96,6 +115,44 @@ class RequestManager { }) } + private getProxyAgent(targetUrl: string): Agent { + if (!this._proxyAgents) { + this._proxyAgents = RequestManager.createProxyAgents(this._proxy); + } + + return new URL(targetUrl).protocol === "https:" ? this._proxyAgents.https : this._proxyAgents.http; + } + + // Same agent selection as proxy-agent, minus PAC (its dependencies pull in basic-ftp). + private static createProxyAgents(proxy: string): ProxyAgents { + let proxyUrl: URL; + try { + proxyUrl = new URL(proxy); + } catch { + // Older Node versions put the whole input, credentials included, in the error message. + throw new Error("Invalid proxy URL"); + } + + switch (proxyUrl.protocol) { + case "http:": + case "https:": + return { http: new HttpProxyAgent(proxy), https: new HttpsProxyAgent(proxy) }; + case "socks:": + case "socks4:": + case "socks4a:": + case "socks5:": + case "socks5h:": { + const socksAgent = new SocksProxyAgent(proxy); + return { http: socksAgent, https: socksAgent }; + } + default: + // Strip credentials: the CLI prints this message. + proxyUrl.username = ""; + proxyUrl.password = ""; + throw new Error(`Unsupported protocol for proxy URL: ${proxyUrl.href}`); + } + } + private getCodePushError(error: any, response?: superagent.Response): CodePushError { if (error.syscall === "getaddrinfo") { error.message = `Unable to connect to the CodePush server. Are you offline, or behind a firewall or proxy?\n(${error.message})`;