diff --git a/cli/__snapshots__/download_spec.js b/cli/__snapshots__/download_spec.js index 1d1e063a4a67..e5a519fa9e94 100644 --- a/cli/__snapshots__/download_spec.js +++ b/cli/__snapshots__/download_spec.js @@ -17,7 +17,9 @@ https://cypress.example.com/desktop/0.20.2?platform=OS&arch=ARCH exports['download status errors 1'] = ` Error: The Cypress App could not be downloaded. -Please check network connectivity and try again: +Does your workplace require a proxy to be used to access the Internet? If so, you must configure the HTTP_PROXY environment variable before downloading Cypress. Read more: https://on.cypress.io/proxy-configuration + +Otherwise, please check network connectivity and try again: ---------- diff --git a/cli/lib/errors.js b/cli/lib/errors.js index 3727c7014ef3..1a83b96ce434 100644 --- a/cli/lib/errors.js +++ b/cli/lib/errors.js @@ -19,7 +19,10 @@ const hr = '----------' // common errors Cypress application can encounter const failedDownload = { description: 'The Cypress App could not be downloaded.', - solution: 'Please check network connectivity and try again:', + solution: stripIndent` + Does your workplace require a proxy to be used to access the Internet? If so, you must configure the HTTP_PROXY environment variable before downloading Cypress. Read more: https://on.cypress.io/proxy-configuration + + Otherwise, please check network connectivity and try again:`, } const failedUnzip = { diff --git a/cli/lib/tasks/download.js b/cli/lib/tasks/download.js index 681c7cc39eb2..3bdda4ffd474 100644 --- a/cli/lib/tasks/download.js +++ b/cli/lib/tasks/download.js @@ -16,6 +16,16 @@ const util = require('../util') const defaultBaseUrl = 'https://download.cypress.io/' +const getProxyUrl = () => { + return process.env.HTTPS_PROXY || + process.env.https_proxy || + process.env.npm_config_https_proxy || + process.env.HTTP_PROXY || + process.env.http_proxy || + process.env.npm_config_proxy || + null +} + const getRealOsArch = () => { // os.arch() returns the arch for which this node was compiled // we want the operating system's arch instead: x64 or x86 @@ -174,13 +184,19 @@ const verifyDownloadedFile = (filename, expectedSize, expectedChecksum) => { // {filename: ..., downloaded: true} const downloadFromUrl = ({ url, downloadDestination, progress }) => { return new Promise((resolve, reject) => { - debug('Downloading from', url) - debug('Saving file to', downloadDestination) + const proxy = getProxyUrl() + + debug('Downloading package', { + url, + proxy, + downloadDestination, + }) let redirectVersion const req = request({ url, + proxy, followRedirect (response) { const version = response.headers['x-version'] @@ -310,4 +326,5 @@ const start = (opts) => { module.exports = { start, getUrl, + getProxyUrl, } diff --git a/cli/package.json b/cli/package.json index 577cb3ea371b..e38ee0140caf 100644 --- a/cli/package.json +++ b/cli/package.json @@ -90,7 +90,7 @@ "proxyquire": "2.1.0", "shelljs": "0.8.3", "sinon": "7.2.2", - "snap-shot-it": "7.7.1", + "snap-shot-it": "7.8.0", "spawn-mock": "1.0.0" }, "bin": { diff --git a/cli/test/lib/tasks/download_spec.js b/cli/test/lib/tasks/download_spec.js index 2d5e107bcca0..4b237ac7916e 100644 --- a/cli/test/lib/tasks/download_spec.js +++ b/cli/test/lib/tasks/download_spec.js @@ -1,5 +1,6 @@ require('../../spec_helper') +const _ = require('lodash') const os = require('os') const la = require('lazy-ass') const is = require('check-more-types') @@ -138,7 +139,7 @@ describe('lib/tasks/download', function () { }) }) - describe('verify downloaded file', function () { + context('verify downloaded file', function () { before(function () { this.expectedChecksum = hasha.fromFileSync(examplePath) this.expectedFileSize = fs.statSync(examplePath).size @@ -310,4 +311,30 @@ describe('lib/tasks/download', function () { return snapshot('download status errors 1', normalize(ctx.stdout.toString())) }) }) + + context('with proxy env vars', () => { + beforeEach(function () { + this.env = _.clone(process.env) + }) + + afterEach(function () { + process.env = this.env + }) + + it('prefers https_proxy over http_proxy', () => { + process.env.HTTP_PROXY = 'foo' + expect(download.getProxyUrl()).to.eq('foo') + process.env.https_proxy = 'bar' + expect(download.getProxyUrl()).to.eq('bar') + }) + + it('falls back to npm_config_proxy', () => { + process.env.npm_config_proxy = 'foo' + expect(download.getProxyUrl()).to.eq('foo') + process.env.npm_config_https_proxy = 'bar' + expect(download.getProxyUrl()).to.eq('bar') + process.env.https_proxy = 'baz' + expect(download.getProxyUrl()).to.eq('baz') + }) + }) }) diff --git a/package.json b/package.json index dbbc775c7458..cb917ed5596f 100644 --- a/package.json +++ b/package.json @@ -145,7 +145,7 @@ "ramda": "0.24.1", "shelljs": "0.8.3", "sinon": "7.3.2", - "snap-shot-it": "7.7.1", + "snap-shot-it": "7.8.0", "stop-only": "3.0.1", "strip-ansi": "4.0.0", "terminal-banner": "1.1.0", diff --git a/packages/server/lib/util/proxy.ts b/packages/server/lib/util/proxy.ts index 884d745184a6..fa1463733ec0 100644 --- a/packages/server/lib/util/proxy.ts +++ b/packages/server/lib/util/proxy.ts @@ -1,37 +1,75 @@ +import _ from 'lodash' +import debugModule from 'debug' import os from 'os' import { getWindowsProxy } from './get-windows-proxy' +const debug = debugModule('cypress:server:util:proxy') + +const falsyEnv = (v) => { + return v === 'false' || v === '0' || !v +} + const copyLowercaseEnvToUppercase = (name: string) => { // uppercase environment variables are used throughout Cypress and dependencies // but users sometimes supply these vars as lowercase const lowerEnv = process.env[name.toLowerCase()] if (lowerEnv) { + debug('overriding uppercase env var with lowercase %o', { name }) process.env[name.toUpperCase()] = lowerEnv } } const normalizeEnvironmentProxy = () => { - if (process.env.HTTP_PROXY === 'false' || process.env.HTTP_PROXY === '0' || !process.env.HTTP_PROXY) { + if (falsyEnv(process.env.HTTP_PROXY)) { + debug('HTTP_PROXY is falsy, disabling HTTP_PROXY') delete process.env.HTTP_PROXY } if (!process.env.HTTPS_PROXY && process.env.HTTP_PROXY) { // request library will use HTTP_PROXY as a fallback for HTTPS urls, but // proxy-from-env will not, so let's just force it to fall back like this + debug('setting HTTPS_PROXY to HTTP_PROXY since it does not exist') process.env.HTTPS_PROXY = process.env.HTTP_PROXY } if (!process.env.hasOwnProperty('NO_PROXY')) { // don't proxy localhost, to match Chrome's default behavior and user expectation + debug('setting default NO_PROXY of `localhost`') process.env.NO_PROXY = 'localhost' } + + debug('normalized proxy environment variables %o', _.pick(process.env, [ + 'NO_PROXY', 'HTTP_PROXY', 'HTTPS_PROXY', + ])) +} + +const mergeNpmProxyVars = () => { + // copy npm's `proxy` and `https-proxy` config if they are set + // https://github.com/cypress-io/cypress/pull/4705 + [ + ['npm_config_proxy', 'HTTP_PROXY'], + ['npm_config_https_proxy', 'HTTPS_PROXY'], + ].forEach(([from, to]) => { + if (!falsyEnv(process.env[from]) && _.isUndefined(process.env[to])) { + debug('using npm\'s %s as %s', from, to) + process.env[to] = process.env[from] + } + }) } export const loadSystemProxySettings = () => { - ['NO_PROXY', 'HTTP_PROXY', 'HTTPS_PROXY'].forEach(copyLowercaseEnvToUppercase) + debug('found proxy environment variables %o', _.pick(process.env, [ + 'NO_PROXY', 'HTTP_PROXY', 'HTTPS_PROXY', + 'no_proxy', 'http_proxy', 'https_proxy', + 'npm_config_proxy', 'npm_config_https_proxy', + ])) + + ;['NO_PROXY', 'HTTP_PROXY', 'HTTPS_PROXY'].forEach(copyLowercaseEnvToUppercase) + + mergeNpmProxyVars() - if (typeof process.env.HTTP_PROXY !== 'undefined') { + if (!_.isUndefined(process.env.HTTP_PROXY)) { normalizeEnvironmentProxy() return diff --git a/packages/server/package.json b/packages/server/package.json index dbef85ba16be..d69d675b7ac8 100644 --- a/packages/server/package.json +++ b/packages/server/package.json @@ -182,7 +182,7 @@ "react": "15.6.2", "repl.history": "0.1.4", "run-sequence": "1.2.2", - "snap-shot-it": "7.7.1", + "snap-shot-it": "7.8.0", "ssestream": "1.0.1", "stream-to-promise": "1.1.1", "supertest": "4.0.2", diff --git a/packages/server/test/unit/args_spec.coffee b/packages/server/test/unit/args_spec.coffee index 4f258c7bfe7b..9d7f8f78e5ba 100644 --- a/packages/server/test/unit/args_spec.coffee +++ b/packages/server/test/unit/args_spec.coffee @@ -394,3 +394,29 @@ describe "lib/util/args", -> expect(options.proxySource).to.be.undefined expect(options.proxyServer).to.eq process.env.HTTP_PROXY expect(options.proxyBypassList).to.eq process.env.NO_PROXY + + it "can use npm_config_proxy", -> + process.env.npm_config_proxy = "http://foo-bar.baz:123" + expect(process.env.HTTP_PROXY).to.be.undefined + + options = @setup() + + expect(process.env.HTTP_PROXY).to.eq "http://foo-bar.baz:123" + expect(process.env.HTTPS_PROXY).to.eq "http://foo-bar.baz:123" + expect(process.env.NO_PROXY).to.eq "localhost" + expect(options.proxySource).to.be.undefined + expect(options.proxyServer).to.eq process.env.HTTP_PROXY + expect(options.proxyBypassList).to.eq process.env.NO_PROXY + + it "can override npm_config_proxy with falsy HTTP_PROXY", -> + process.env.npm_config_proxy = "http://foo-bar.baz:123" + process.env.HTTP_PROXY = "" + + options = @setup() + + expect(process.env.HTTP_PROXY).to.be.undefined + expect(process.env.HTTPS_PROXY).to.be.undefined + expect(process.env.NO_PROXY).to.eq "localhost" + expect(options.proxySource).to.be.undefined + expect(options.proxyServer).to.eq process.env.HTTP_PROXY + expect(options.proxyBypassList).to.be.undefined