process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0' const { request, expect } = require('../spec_helper') const DebugProxy = require('@cypress/debugging-proxy') const https = require('https') const net = require('net') const network = require('@packages/network') const Promise = require('bluebird') const proxy = require('../helpers/proxy') const httpServer = require('../helpers/http_server') const httpsServer = require('../helpers/https_server') const fs = require('fs').promises describe('Proxy', () => { beforeEach(function () { return Promise.join( httpServer.start(), httpsServer.start(8443), httpsServer.start(8444), proxy.start(3333) .then((proxy1) => { this.proxy = proxy1 }), ) }) afterEach(() => { return Promise.join( httpServer.stop(), httpsServer.stop(), proxy.stop(), ) }) it('can request the googles', function () { // give some padding to external // network request this.timeout(10000) return Promise.all([ request({ strictSSL: false, proxy: 'http://localhost:3333', url: 'https://www.google.com', }), request({ strictSSL: false, proxy: 'http://localhost:3333', url: 'https://mail.google.com', }), request({ strictSSL: false, proxy: 'http://localhost:3333', url: 'https://google.com', }), ]) }) it('can call the httpsDirectly without a proxy', () => { return request({ strictSSL: false, url: 'https://localhost:8443', }) }) it('can boot the httpsServer', () => { return request({ strictSSL: false, url: 'https://localhost:8443/', proxy: 'http://localhost:3333', }) .then((html) => { expect(html).to.include('https server') }) }) it('yields the onRequest callback', () => { return request({ strictSSL: false, url: 'https://localhost:8443/replace', proxy: 'http://localhost:3333', }) .then((html) => { expect(html).to.include('replaced content') }) }) it('closes outgoing connections when client disconnects', function () { this.sandbox.spy(net, 'connect') return request({ strictSSL: false, url: 'https://localhost:8444/replace', proxy: 'http://localhost:3333', resolveWithFullResponse: true, }) .then(() => { // ensure the outgoing socket created for this connection was destroyed expect(net.connect).calledOnce const socket = net.connect.getCalls()[0].returnValue return new Promise((resolve) => { socket.on('close', () => { expect(socket.destroyed).to.be.true resolve() }) }) }) }) it('can boot the httpServer', () => { return request({ strictSSL: false, url: 'http://localhost:8080/', proxy: 'http://localhost:3333', }) .then((html) => { expect(html).to.include('http server') }) }) context('generating certificates', () => { it('reuses existing certificates', function () { return request({ strictSSL: false, url: 'https://localhost:8443/', proxy: 'http://localhost:3333', }) .then(() => { proxy.reset() // force this to reject if its called this.sandbox.stub(this.proxy, '_generateMissingCertificates').rejects(new Error('should not call')) return request({ strictSSL: false, url: 'https://localhost:8443/', proxy: 'http://localhost:3333', }) }) }) // @see https://github.com/cypress-io/cypress/issues/8705 it('handles errors with reusing existing certificates', async function () { await this.proxy._ca.removeAll() proxy.reset() const genSpy = this.sandbox.spy(this.proxy, '_generateMissingCertificates') await request({ strictSSL: false, url: 'https://localhost:8443/', proxy: 'http://localhost:3333', }) proxy.reset() expect(genSpy).to.be.calledWith('localhost').and.calledOnce const privateKeyPath = this.proxy._ca.getPrivateKeyPath('localhost') const key = (await fs.readFile(privateKeyPath)).toString().trim() expect(key).to.match(/^-----BEGIN RSA PRIVATE KEY-----/) .and.match(/-----END RSA PRIVATE KEY-----$/) await fs.writeFile(privateKeyPath, 'some random garbage') await request({ strictSSL: false, url: 'https://localhost:8443/', proxy: 'http://localhost:3333', }) expect(genSpy).to.always.have.been.calledWith('localhost').and.calledTwice const key2 = (await fs.readFile(privateKeyPath)).toString().trim() expect(key2).to.match(/^-----BEGIN RSA PRIVATE KEY-----/) .and.match(/-----END RSA PRIVATE KEY-----$/) }) // https://github.com/cypress-io/cypress/issues/771 it('generates certs and can proxy requests for HTTPS requests to IPs', function () { this.timeout(5000) this.sandbox.spy(this.proxy, '_generateMissingCertificates') this.sandbox.spy(this.proxy, '_getServerPortForIp') return Promise.all([ httpsServer.start(8445), this.proxy._ca.removeAll(), ]) .then(() => { return request({ strictSSL: false, url: 'https://127.0.0.1:8445/', proxy: 'http://localhost:3333', }) }).then(() => { // this should not stand up its own https server return request({ strictSSL: false, url: 'https://localhost:8443/', proxy: 'http://localhost:3333', }) }).then(() => { expect(this.proxy._ipServers['127.0.0.1']).to.be.an.instanceOf(https.Server) expect(this.proxy._getServerPortForIp).to.be.calledWith('127.0.0.1').and.be.calledOnce expect(this.proxy._generateMissingCertificates).to.be.calledTwice }) }) // https://github.com/cypress-io/cypress/issues/9220 it('handles errors with addContext', async function () { this.sandbox.spy(this.proxy, 'connect') this.sandbox.stub(this.proxy._sniServer, 'addContext').throws(new Error('error adding context')) return request({ strictSSL: false, url: 'https://localhost:8443/', proxy: 'http://localhost:3333', }).catch(() => { // This scenario will cause an error but we should clean // ensure the outgoing socket created for this connection was destroyed expect(this.proxy.connect).calledOnce const socket = this.proxy.connect.getCalls()[0].args[1] expect(socket.destroyed).to.be.true }) }) }) context('closing', () => { it('resets sslServers and can reopen', function () { return request({ strictSSL: false, url: 'https://localhost:8443/', proxy: 'http://localhost:3333', }) .then(() => { return proxy.stop() }).then(() => { return proxy.start(3333) }).then(() => { // force this to reject if its called this.sandbox.stub(this.proxy, '_generateMissingCertificates').rejects(new Error('should not call')) return request({ strictSSL: false, url: 'https://localhost:8443/', proxy: 'http://localhost:3333', }) }) }) }) context('with an upstream proxy', () => { beforeEach(function () { // PROXY vars should override npm_config vars, so set them to cause failures if they are used // @see https://github.com/cypress-io/cypress/pull/8295 process.env.npm_config_proxy = process.env.npm_config_https_proxy = 'http://erroneously-used-npm-proxy.invalid' process.env.npm_config_noproxy = 'just,some,nonsense' process.env.NO_PROXY = '' process.env.HTTP_PROXY = process.env.HTTPS_PROXY = 'http://localhost:9001' this.upstream = new DebugProxy({ keepRequests: true, }) return this.upstream.start(9001) }) it('passes a request to an https server through the upstream', function () { this.upstream._onConnect = function (domain, port) { expect(domain).to.eq('localhost') expect(port).to.eq('8444') return true } return request({ strictSSL: false, url: 'https://localhost:8444/', proxy: 'http://localhost:3333', }).then((res) => { expect(res).to.contain('https server') }) }) it('uses HTTP basic auth when provided', function () { this.upstream.setAuth({ username: 'foo', password: 'bar', }) this.upstream._onConnect = function (domain, port) { expect(domain).to.eq('localhost') expect(port).to.eq('8444') return true } process.env.HTTP_PROXY = (process.env.HTTPS_PROXY = 'http://foo:bar@localhost:9001') return request({ strictSSL: false, url: 'https://localhost:8444/', proxy: 'http://localhost:3333', }).then((res) => { expect(res).to.contain('https server') }) }) it('closes outgoing connections when client disconnects', function () { this.sandbox.spy(net, 'connect') return request({ strictSSL: false, url: 'https://localhost:8444/replace', proxy: 'http://localhost:3333', resolveWithFullResponse: true, forever: false, }) .then(() => { // ensure the outgoing socket created for this connection was destroyed expect(net.connect).calledOnce const socket = net.connect.getCalls()[0].returnValue return new Promise((resolve) => { return socket.on('close', () => { expect(socket.destroyed).to.be.true resolve() }) }) }) }) // https://github.com/cypress-io/cypress/issues/4257 it('passes through to SNI when it is intercepted and not through proxy', function () { const createSocket = this.sandbox.stub(network.connect, 'createRetryingSocket').callsArgWith(1, new Error('stub')) const createProxyConn = this.sandbox.spy(network.agent.httpsAgent, 'createUpstreamProxyConnection') return request({ strictSSL: false, url: 'https://localhost:8443', proxy: 'http://localhost:3333', resolveWithFullResponse: true, forever: false, }) .then(() => { throw new Error('should not succeed') }).catch({ message: 'Error: Client network socket disconnected before secure TLS connection was established' }, () => { expect(createProxyConn).to.not.be.called expect(createSocket).to.be.calledWith({ port: this.proxy._sniPort, host: 'localhost', }) }) }) return afterEach(function () { this.upstream.stop() delete process.env.HTTP_PROXY delete process.env.HTTPS_PROXY delete process.env.NO_PROXY }) }) })