From a1105c20b558f0ddc738d5193be144ea78c73c9d Mon Sep 17 00:00:00 2001 From: Zach Bloomquist Date: Tue, 30 Jul 2019 13:03:29 -0400 Subject: [PATCH 01/10] use own server-destroy implementation that supports secureConnect events --- packages/https-proxy/lib/server.coffee | 3 +- packages/https-proxy/package.json | 3 +- .../test/helpers/https_server.coffee | 2 +- .../https-proxy/test/helpers/proxy.coffee | 3 ++ packages/network/lib/allow-destroy.ts | 31 +++++++++++++++++++ packages/network/lib/index.ts | 9 ++++-- packages/network/test/support/servers.ts | 28 ++--------------- packages/network/test/unit/agent_spec.ts | 9 ++++-- packages/server/lib/util/server_destroy.js | 11 ++----- packages/server/package.json | 1 - 10 files changed, 54 insertions(+), 46 deletions(-) create mode 100644 packages/network/lib/allow-destroy.ts diff --git a/packages/https-proxy/lib/server.coffee b/packages/https-proxy/lib/server.coffee index fe68de726546..e04b31ccc2c6 100644 --- a/packages/https-proxy/lib/server.coffee +++ b/packages/https-proxy/lib/server.coffee @@ -1,6 +1,5 @@ _ = require("lodash") -{ agent, connect } = require("@packages/network") -allowDestroy = require("server-destroy-vvo") +{ agent, allowDestroy, connect } = require("@packages/network") debug = require("debug")("cypress:https-proxy") fs = require("fs-extra") getProxyForUrl = require("proxy-from-env").getProxyForUrl diff --git a/packages/https-proxy/package.json b/packages/https-proxy/package.json index ee843f60118c..707fdc144407 100644 --- a/packages/https-proxy/package.json +++ b/packages/https-proxy/package.json @@ -23,8 +23,7 @@ "lodash": "4.17.15", "node-forge": "0.6.49", "proxy-from-env": "1.0.0", - "semaphore": "1.1.0", - "server-destroy-vvo": "1.0.1" + "semaphore": "1.1.0" }, "devDependencies": { "@cypress/debugging-proxy": "2.0.1", diff --git a/packages/https-proxy/test/helpers/https_server.coffee b/packages/https-proxy/test/helpers/https_server.coffee index 58b4e53ac419..c148cfb7a411 100644 --- a/packages/https-proxy/test/helpers/https_server.coffee +++ b/packages/https-proxy/test/helpers/https_server.coffee @@ -1,6 +1,6 @@ https = require("https") Promise = require("bluebird") -allowDestroy = require("server-destroy-vvo") +{ allowDestroy } = require("@packages/network") certs = require("./certs") defaultOnRequest = (req, res) -> diff --git a/packages/https-proxy/test/helpers/proxy.coffee b/packages/https-proxy/test/helpers/proxy.coffee index fcc2078793cc..a3ad72731c6a 100644 --- a/packages/https-proxy/test/helpers/proxy.coffee +++ b/packages/https-proxy/test/helpers/proxy.coffee @@ -1,3 +1,4 @@ +allowDestroy = require("@packages/network") http = require("http") path = require("path") httpsProxy = require("../../lib/proxy") @@ -28,6 +29,8 @@ module.exports = { start: (port) -> prx = http.createServer() + allowDestroy(prx) + dir = path.join(process.cwd(), "ca") httpsProxy.create(dir, port, { diff --git a/packages/network/lib/allow-destroy.ts b/packages/network/lib/allow-destroy.ts new file mode 100644 index 000000000000..967aabdf878c --- /dev/null +++ b/packages/network/lib/allow-destroy.ts @@ -0,0 +1,31 @@ +import net from 'net' + +/** + * `allowDestroy` adds a `destroy` method to a `net.Server`. `destroy(cb)` + * will kill all open connections and call `cb` when the server is closed. + * + * Note: `server-destroy` NPM package cannot be used - it does not track + * `secureConnection` events. + */ +export function allowDestroy (server: net.Server) { + let connections: net.Socket[] = [] + + function trackConn (conn) { + connections.push(conn) + + conn.on('close', () => { + connections = connections.filter((connection) => connection !== conn) + }) + } + + server.on('connection', trackConn) + server.on('secureConnection', trackConn) + + // @ts-ignore Property 'destroy' does not exist on type 'Server'. + server.destroy = function (cb) { + server.close(cb) + connections.map((connection) => connection.destroy()) + } + + return server +} diff --git a/packages/network/lib/index.ts b/packages/network/lib/index.ts index 1198c70085b6..d2b58d7f42e8 100644 --- a/packages/network/lib/index.ts +++ b/packages/network/lib/index.ts @@ -1,6 +1,9 @@ import agent from './agent' import * as connect from './connect' +import { allowDestroy } from './allow-destroy' -export { agent } - -export { connect } +export { + agent, + allowDestroy, + connect, +} diff --git a/packages/network/test/support/servers.ts b/packages/network/test/support/servers.ts index 79288f490848..4dabfa5b7b6d 100644 --- a/packages/network/test/support/servers.ts +++ b/packages/network/test/support/servers.ts @@ -4,6 +4,7 @@ import http from 'http' import https from 'https' import Io from '@packages/socket' import Promise from 'bluebird' +import { allowDestroy } from '../../lib/allow-destroy' export interface AsyncServer { closeAsync: () => Promise @@ -11,29 +12,6 @@ export interface AsyncServer { listenAsync: (port) => Promise } -function addDestroy (server: http.Server | https.Server) { - let connections = [] - - function trackConn (conn) { - connections.push(conn) - - conn.on('close', () => { - connections = connections.filter((connection) => connection !== conn) - }) - } - - server.on('connection', trackConn) - server.on('secureConnection', trackConn) - - // @ts-ignore Property 'destroy' does not exist on type 'Server'. - server.destroy = function (cb) { - server.close(cb) - connections.map((connection) => connection.destroy()) - } - - return server -} - function createExpressApp () { const app: express.Application = express() @@ -72,14 +50,14 @@ export class Servers { ) .spread((app: Express.Application, [cert, key]: string[]) => { this.httpServer = Promise.promisifyAll( - addDestroy(http.createServer(app)) + allowDestroy(http.createServer(app)) ) as http.Server & AsyncServer this.wsServer = Io.server(this.httpServer) this.https = { cert, key } this.httpsServer = Promise.promisifyAll( - addDestroy(https.createServer(this.https, app)) + allowDestroy(https.createServer(this.https, app)) ) as https.Server & AsyncServer this.wssServer = Io.server(this.httpsServer) diff --git a/packages/network/test/unit/agent_spec.ts b/packages/network/test/unit/agent_spec.ts index 21f384d4a9e2..67c874aa55d5 100644 --- a/packages/network/test/unit/agent_spec.ts +++ b/packages/network/test/unit/agent_spec.ts @@ -15,6 +15,7 @@ import { regenerateRequestHead, CombinedAgent, } from '../../lib/agent' import { AsyncServer, Servers } from '../support/servers' +import { allowDestroy } from '../../lib/allow-destroy' const expect = chai.expect @@ -273,9 +274,11 @@ describe('lib/agent', function () { it('#createUpstreamProxyConnection throws when connection is accepted then closed', function () { const proxy = Bluebird.promisifyAll( - net.createServer((socket) => { - socket.end() - }) + allowDestroy( + net.createServer((socket) => { + socket.end() + }) + ) ) as net.Server & AsyncServer const proxyPort = PROXY_PORT + 2 diff --git a/packages/server/lib/util/server_destroy.js b/packages/server/lib/util/server_destroy.js index 802539e376b3..92d45668f89c 100644 --- a/packages/server/lib/util/server_destroy.js +++ b/packages/server/lib/util/server_destroy.js @@ -1,12 +1,5 @@ -// TODO: This file was created by bulk-decaffeinate. -// Sanity-check the conversion and remove this comment. -/* - * decaffeinate suggestions: - * DS102: Remove unnecessary code created because of implicit returns - * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md - */ const Promise = require('bluebird') -const allowDestroy = require('server-destroy') +const { allowDestroy } = require('@packages/network') module.exports = function (server) { allowDestroy(server) @@ -16,4 +9,4 @@ module.exports = function (server) { .catch(() => {}) } } -//# dont catch any errors +// dont catch any errors diff --git a/packages/server/package.json b/packages/server/package.json index 51a25ca7d82d..d8e3414487fa 100644 --- a/packages/server/package.json +++ b/packages/server/package.json @@ -118,7 +118,6 @@ "sanitize-filename": "1.6.1", "semver": "6.3.0", "send": "0.17.0", - "server-destroy": "1.0.1", "shell-env": "3.0.0", "signal-exit": "3.0.2", "sinon": "5.1.1", From 8d10e161238b7326744b992d9b7d7c39858eaac0 Mon Sep 17 00:00:00 2001 From: Zach Bloomquist Date: Wed, 7 Aug 2019 16:45:44 -0400 Subject: [PATCH 02/10] stand up HTTPS server for requests over ssl to IPs --- packages/https-proxy/lib/server.coffee | 48 ++++++++++++++----- packages/https-proxy/package.json | 2 +- .../https-proxy/test/helpers/proxy.coffee | 4 +- .../test/integration/proxy_spec.coffee | 24 +++++++++- 4 files changed, 63 insertions(+), 15 deletions(-) diff --git a/packages/https-proxy/lib/server.coffee b/packages/https-proxy/lib/server.coffee index e04b31ccc2c6..9c38efd21c84 100644 --- a/packages/https-proxy/lib/server.coffee +++ b/packages/https-proxy/lib/server.coffee @@ -22,8 +22,9 @@ SSL_RECORD_TYPES = [ ] class Server - constructor: (@_ca, @_port) -> + constructor: (@_ca, @_port, @_options) -> @_onError = null + @_ipServers = {} connect: (req, browserSocket, head, options = {}) -> ## don't buffer writes - thanks a lot, Nagle @@ -216,25 +217,48 @@ class Server _getPortFor: (hostname) -> @_getCertificatePathsFor(hostname) - .catch (err) => @_generateMissingCertificates(hostname) - .then (data = {}) => + if net.isIP(hostname) + return @_getServerPortForIp(hostname, data) + @_sniServer.addContext(hostname, data) return @_sniPort - listen: (options = {}) -> + ## browsers will not do SNI for an IP address, so we need to serve + ## 1 HTTPS server per IP + ## https://github.com/cypress-io/cypress/issues/771 + _getServerPortForIp: (ip, data) => + if server = @_ipServers[ip] + return Promise.resolve(server.address().port) + + new Promise (resolve) => + @_ipServers[ip] = server = https.createServer(data) + + allowDestroy(server) + + server.on "upgrade", @_onUpgrade.bind(@, @_options.onUpgrade) + server.on "request", @_onRequest.bind(@, @_options.onRequest) + server.listen 0, '127.0.0.1', => + port = server.address().port + + debug("Created HTTPS Proxy for IP %s on port %s", ip, port) + + resolve(port) + + + listen: -> new Promise (resolve) => - @_onError = options.onError + @_onError = @_options.onError @_sniServer = https.createServer({}) allowDestroy(@_sniServer) - @_sniServer.on "upgrade", @_onUpgrade.bind(@, options.onUpgrade) - @_sniServer.on "request", @_onRequest.bind(@, options.onRequest) + @_sniServer.on "upgrade", @_onUpgrade.bind(@, @_options.onUpgrade) + @_sniServer.on "request", @_onRequest.bind(@, @_options.onRequest) @_sniServer.listen 0, '127.0.0.1', => ## store the port of our current sniServer @_sniPort = @_sniServer.address().port @@ -245,8 +269,10 @@ class Server close: -> close = => - new Promise (resolve) => - @_sniServer.destroy(resolve) + servers = _.values(@_ipServers).concat([@_sniServer]) + Promise.map servers, (server) -> + Promise.fromCallback (cb) -> + server.destroy(cb) close() .finally -> @@ -257,9 +283,9 @@ module.exports = { sslServers = {} create: (ca, port, options = {}) -> - srv = new Server(ca, port) + srv = new Server(ca, port, options) srv - .listen(options) + .listen() .return(srv) } diff --git a/packages/https-proxy/package.json b/packages/https-proxy/package.json index 707fdc144407..4e4bfb4c1426 100644 --- a/packages/https-proxy/package.json +++ b/packages/https-proxy/package.json @@ -21,7 +21,7 @@ "debug": "4.1.1", "fs-extra": "8.1.0", "lodash": "4.17.15", - "node-forge": "0.6.49", + "node-forge": "0.8.5", "proxy-from-env": "1.0.0", "semaphore": "1.1.0" }, diff --git a/packages/https-proxy/test/helpers/proxy.coffee b/packages/https-proxy/test/helpers/proxy.coffee index a3ad72731c6a..fa6b51abdfdb 100644 --- a/packages/https-proxy/test/helpers/proxy.coffee +++ b/packages/https-proxy/test/helpers/proxy.coffee @@ -1,4 +1,4 @@ -allowDestroy = require("@packages/network") +{ allowDestroy } = require("@packages/network") http = require("http") path = require("path") httpsProxy = require("../../lib/proxy") @@ -64,7 +64,7 @@ module.exports = { stop: -> new Promise (resolve) -> - prx.close(resolve) + prx.destroy(resolve) .then -> prx.proxy.close() } diff --git a/packages/https-proxy/test/integration/proxy_spec.coffee b/packages/https-proxy/test/integration/proxy_spec.coffee index d3c5f3111827..72395d2707a0 100644 --- a/packages/https-proxy/test/integration/proxy_spec.coffee +++ b/packages/https-proxy/test/integration/proxy_spec.coffee @@ -152,6 +152,28 @@ describe "Proxy", -> proxy: "http://localhost:3333" }) + ## https://github.com/cypress-io/cypress/issues/771 + it "generates certs and can proxy requests for HTTPS requests to IPs", -> + @sandbox.spy(@proxy, "_generateMissingCertificates") + @sandbox.spy(@proxy, "_getServerPortForIp") + + request({ + strictSSL: false + url: "https://1.1.1.1/" + proxy: "http://localhost:3333" + }) + .then => + proxy.reset() + + request({ + strictSSL: false + url: "https://localhost:8443/" + proxy: "http://localhost:3333" + }) + .then => + expect(@proxy._generateMissingCertificates).to.be.calledOnce + expect(@proxy._getServerPortForIp).to.be.calledWith('1.1.1.1', sinon.match.any) + context "closing", -> it "resets sslServers and can reopen", -> request({ @@ -256,7 +278,7 @@ describe "Proxy", -> }) .then => throw new Error('should not succeed') - .catch { message: 'Error: socket hang up' }, => + .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: @proxy._sniPort From 8d9ecd43fde35289d67f30f19f156b8bea938c78 Mon Sep 17 00:00:00 2001 From: Zach Bloomquist Date: Wed, 7 Aug 2019 16:50:21 -0400 Subject: [PATCH 03/10] don't need to resolve with --- packages/https-proxy/lib/server.coffee | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/https-proxy/lib/server.coffee b/packages/https-proxy/lib/server.coffee index 9c38efd21c84..b960ccc9d9ab 100644 --- a/packages/https-proxy/lib/server.coffee +++ b/packages/https-proxy/lib/server.coffee @@ -232,7 +232,7 @@ class Server ## https://github.com/cypress-io/cypress/issues/771 _getServerPortForIp: (ip, data) => if server = @_ipServers[ip] - return Promise.resolve(server.address().port) + return server.address().port new Promise (resolve) => @_ipServers[ip] = server = https.createServer(data) @@ -248,7 +248,6 @@ class Server resolve(port) - listen: -> new Promise (resolve) => @_onError = @_options.onError From 561ea0259a69d26180af7cac064ff24804c09a6c Mon Sep 17 00:00:00 2001 From: Zach Bloomquist Date: Thu, 8 Aug 2019 16:19:30 -0400 Subject: [PATCH 04/10] fix tests --- packages/https-proxy/test/integration/proxy_spec.coffee | 2 +- packages/server/lib/server.coffee | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/https-proxy/test/integration/proxy_spec.coffee b/packages/https-proxy/test/integration/proxy_spec.coffee index 72395d2707a0..289974e2b48f 100644 --- a/packages/https-proxy/test/integration/proxy_spec.coffee +++ b/packages/https-proxy/test/integration/proxy_spec.coffee @@ -278,7 +278,7 @@ describe "Proxy", -> }) .then => throw new Error('should not succeed') - .catch { message: 'Error: Client network socket disconnected before secure TLS connection was established' }, => + .catch { message: 'Error: socket hang up' }, => expect(createProxyConn).to.not.be.called expect(createSocket).to.be.calledWith({ port: @proxy._sniPort diff --git a/packages/server/lib/server.coffee b/packages/server/lib/server.coffee index a06491932dfe..e2d1548827a3 100644 --- a/packages/server/lib/server.coffee +++ b/packages/server/lib/server.coffee @@ -701,6 +701,7 @@ class Server @_fileServer?.close() @_httpsProxy?.close() ) + .catch { message: "Not running" }, _.noop .then => ## reset any middleware @_middleware = null From a1668ebab8cf26ecab34752abded2e3d7348ffd7 Mon Sep 17 00:00:00 2001 From: Zach Bloomquist Date: Fri, 9 Aug 2019 09:53:52 -0400 Subject: [PATCH 05/10] stand up a server on 127.0.0.1 for test --- .../test/integration/proxy_spec.coffee | 31 +++++++++++++------ 1 file changed, 21 insertions(+), 10 deletions(-) diff --git a/packages/https-proxy/test/integration/proxy_spec.coffee b/packages/https-proxy/test/integration/proxy_spec.coffee index 289974e2b48f..036575ff864d 100644 --- a/packages/https-proxy/test/integration/proxy_spec.coffee +++ b/packages/https-proxy/test/integration/proxy_spec.coffee @@ -4,6 +4,8 @@ process.env.NODE_TLS_REJECT_UNAUTHORIZED = "0" _ = require("lodash") DebugProxy = require("@cypress/debugging-proxy") +fs = require("fs-extra") +https = require("https") net = require("net") network = require("@packages/network") path = require("path") @@ -157,22 +159,31 @@ describe "Proxy", -> @sandbox.spy(@proxy, "_generateMissingCertificates") @sandbox.spy(@proxy, "_getServerPortForIp") - request({ - strictSSL: false - url: "https://1.1.1.1/" - proxy: "http://localhost:3333" - }) - .then => - proxy.reset() + remove = (folder, file) => + fs.removeAsync(path.join(folder, file)) + Promise.all([ + httpsServer.start(8445), + remove(@proxy._ca.certsFolder, '127.0.0.1.pem'), + remove(@proxy._ca.keysFolder, '127.0.0.1.key'), + remove(@proxy._ca.keysFolder, '127.0.0.1.public.key') + ]) + .then => + request({ + strictSSL: false + url: "https://127.0.0.1:8445/" + proxy: "http://localhost:3333" + }) + .then => + ## this should not stand up its own https server request({ strictSSL: false url: "https://localhost:8443/" proxy: "http://localhost:3333" }) .then => - expect(@proxy._generateMissingCertificates).to.be.calledOnce - expect(@proxy._getServerPortForIp).to.be.calledWith('1.1.1.1', sinon.match.any) + expect(@proxy._ipServers["127.0.0.1"]).to.be.an.instanceOf(https.Server) + expect(@proxy._getServerPortForIp).to.be.calledWith('127.0.0.1', sinon.match.any) context "closing", -> it "resets sslServers and can reopen", -> @@ -278,7 +289,7 @@ describe "Proxy", -> }) .then => throw new Error('should not succeed') - .catch { message: 'Error: socket hang up' }, => + .catch { message: "Error: socket hang up" }, => expect(createProxyConn).to.not.be.called expect(createSocket).to.be.calledWith({ port: @proxy._sniPort From 8cd3cce9c4f192d6ef8bdfd7d67b453bccf284dd Mon Sep 17 00:00:00 2001 From: Brian Mann Date: Wed, 11 Sep 2019 15:32:59 -0400 Subject: [PATCH 06/10] tighten up / cleanup code, consolidate + refactor MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - lazily fs.outputfile’s - move sslIpServers to be global - add remove all CA utility --- packages/https-proxy/lib/ca.coffee | 60 ++++++++--------- packages/https-proxy/lib/server.coffee | 65 ++++++++++--------- packages/https-proxy/package.json | 2 +- .../test/integration/proxy_spec.coffee | 8 +-- packages/server/lib/server.coffee | 1 - .../test/integration/server_spec.coffee | 38 ++++------- 6 files changed, 81 insertions(+), 93 deletions(-) diff --git a/packages/https-proxy/lib/ca.coffee b/packages/https-proxy/lib/ca.coffee index aa4dcf8f41b2..94fa2c13c67e 100644 --- a/packages/https-proxy/lib/ca.coffee +++ b/packages/https-proxy/lib/ca.coffee @@ -112,15 +112,27 @@ ServerExtensions = [{ }] class CA + constructor: (caFolder) -> + if not caFolder + caFolder = path.join(os.tmpdir(), 'cy-ca') + + @baseCAFolder = caFolder + @certsFolder = path.join(@baseCAFolder, "certs") + @keysFolder = path.join(@baseCAFolder, "keys") - randomSerialNumber: -> - ## generate random 16 bytes hex string - sn = "" + removeAll: -> + fs + .removeAsync(@baseCAFolder) + .catchReturn({ code: "ENOENT" }) - for i in [1..4] - sn += ("00000000" + Math.floor(Math.random()*Math.pow(256, 4)).toString(16)).slice(-8) + randomSerialNumber: -> + ## generate random 16 bytes hex string + sn = "" - sn + for i in [1..4] + sn += ("00000000" + Math.floor(Math.random()*Math.pow(256, 4)).toString(16)).slice(-8) + + sn generateCA: -> generateKeyPairAsync({bits: 512}) @@ -128,6 +140,7 @@ class CA cert = pki.createCertificate() cert.publicKey = keys.publicKey cert.serialNumber = @randomSerialNumber() + cert.validity.notBefore = new Date() cert.validity.notAfter = new Date() cert.validity.notAfter.setFullYear(cert.validity.notBefore.getFullYear() + 10) @@ -140,9 +153,9 @@ class CA @CAkeys = keys Promise.all([ - fs.writeFileAsync(path.join(@certsFolder, "ca.pem"), pki.certificateToPem(cert)) - fs.writeFileAsync(path.join(@keysFolder, "ca.private.key"), pki.privateKeyToPem(keys.privateKey)) - fs.writeFileAsync(path.join(@keysFolder, "ca.public.key"), pki.publicKeyToPem(keys.publicKey)) + fs.outputFileAsync(path.join(@certsFolder, "ca.pem"), pki.certificateToPem(cert)) + fs.outputFileAsync(path.join(@keysFolder, "ca.private.key"), pki.privateKeyToPem(keys.privateKey)) + fs.outputFileAsync(path.join(@keysFolder, "ca.public.key"), pki.publicKeyToPem(keys.publicKey)) ]) loadCA: -> @@ -198,9 +211,9 @@ class CA dest = mainHost.replace(asterisksRe, "_") Promise.all([ - fs.writeFileAsync(path.join(@certsFolder, dest + ".pem"), certPem) - fs.writeFileAsync(path.join(@keysFolder, dest + ".key"), keyPrivatePem) - fs.writeFileAsync(path.join(@keysFolder, dest + ".public.key"), keyPublicPem) + fs.outputFileAsync(path.join(@certsFolder, dest + ".pem"), certPem) + fs.outputFileAsync(path.join(@keysFolder, dest + ".key"), keyPrivatePem) + fs.outputFileAsync(path.join(@keysFolder, dest + ".public.key"), keyPublicPem) ]) .return([certPem, keyPrivatePem]) @@ -216,25 +229,12 @@ class CA path.join(@certsFolder, "ca.pem") @create = (caFolder) -> - ca = new CA + ca = new CA(caFolder) - if not caFolder - caFolder = path.join(os.tmpdir(), 'cy-ca') - - ca.baseCAFolder = caFolder - ca.certsFolder = path.join(ca.baseCAFolder, "certs") - ca.keysFolder = path.join(ca.baseCAFolder, "keys") - - Promise.all([ - fs.ensureDirAsync(ca.baseCAFolder) - fs.ensureDirAsync(ca.certsFolder) - fs.ensureDirAsync(ca.keysFolder) - ]) - .then -> - fs.statAsync(path.join(ca.certsFolder, "ca.pem")) - .bind(ca) - .then(ca.loadCA) - .catch(ca.generateCA) + fs.statAsync(path.join(ca.certsFolder, "ca.pem")) + .bind(ca) + .then(ca.loadCA) + .catch(ca.generateCA) .return(ca) module.exports = CA diff --git a/packages/https-proxy/lib/server.coffee b/packages/https-proxy/lib/server.coffee index b960ccc9d9ab..71cb8967cc15 100644 --- a/packages/https-proxy/lib/server.coffee +++ b/packages/https-proxy/lib/server.coffee @@ -13,6 +13,7 @@ url = require("url") fs = Promise.promisifyAll(fs) sslServers = {} +sslIpServers = {} sslSemaphores = {} ## https://en.wikipedia.org/wiki/Transport_Layer_Security#TLS_record @@ -24,7 +25,7 @@ SSL_RECORD_TYPES = [ class Server constructor: (@_ca, @_port, @_options) -> @_onError = null - @_ipServers = {} + @_ipServers = sslIpServers connect: (req, browserSocket, head, options = {}) -> ## don't buffer writes - thanks a lot, Nagle @@ -226,60 +227,62 @@ class Server @_sniServer.addContext(hostname, data) return @_sniPort - - ## browsers will not do SNI for an IP address, so we need to serve - ## 1 HTTPS server per IP - ## https://github.com/cypress-io/cypress/issues/771 - _getServerPortForIp: (ip, data) => - if server = @_ipServers[ip] - return server.address().port - - new Promise (resolve) => - @_ipServers[ip] = server = https.createServer(data) + + _listenHttpsServer: (data) -> + new Promise (resolve, reject) => + server = https.createServer(data) allowDestroy(server) + server.once "error", reject server.on "upgrade", @_onUpgrade.bind(@, @_options.onUpgrade) server.on "request", @_onRequest.bind(@, @_options.onRequest) - server.listen 0, '127.0.0.1', => + + server.listen 0, '127.0.0.1', -> port = server.address().port - debug("Created HTTPS Proxy for IP %s on port %s", ip, port) + server.removeListener("error", reject) + + resolve({ server, port }) - resolve(port) + ## browsers will not do SNI for an IP address + ## so we need to serve 1 HTTPS server per IP + ## https://github.com/cypress-io/cypress/issues/771 + _getServerPortForIp: (ip, data) => + if server = sslIpServers[ip] + return server.address().port - listen: -> - new Promise (resolve) => - @_onError = @_options.onError + @_listenHttpsServer(data) + .then ({ server, port }) -> + sslIpServers[ip] = server - @_sniServer = https.createServer({}) + debug("Created IP HTTPS Proxy Server", { port, ip }) - allowDestroy(@_sniServer) + return port - @_sniServer.on "upgrade", @_onUpgrade.bind(@, @_options.onUpgrade) - @_sniServer.on "request", @_onRequest.bind(@, @_options.onRequest) - @_sniServer.listen 0, '127.0.0.1', => - ## store the port of our current sniServer - @_sniPort = @_sniServer.address().port + listen: -> + @_onError = @_options.onError - debug("Created SNI HTTPS Proxy on port %s", @_sniPort) + @_listenHttpsServer({}) + .tap ({ server, port}) => + @_sniPort = port + @_sniServer = server - resolve() + debug("Created SNI HTTPS Proxy Server", { port }) close: -> close = => - servers = _.values(@_ipServers).concat([@_sniServer]) + servers = _.values(sslIpServers).concat(@_sniServer) Promise.map servers, (server) -> - Promise.fromCallback (cb) -> - server.destroy(cb) + Promise.fromCallback(server.destroy) close() - .finally -> - sslServers = {} + .finally(module.exports.reset) module.exports = { reset: -> sslServers = {} + sslIpServers = {} create: (ca, port, options = {}) -> srv = new Server(ca, port, options) diff --git a/packages/https-proxy/package.json b/packages/https-proxy/package.json index 4e4bfb4c1426..f860c2a7f614 100644 --- a/packages/https-proxy/package.json +++ b/packages/https-proxy/package.json @@ -21,7 +21,7 @@ "debug": "4.1.1", "fs-extra": "8.1.0", "lodash": "4.17.15", - "node-forge": "0.8.5", + "node-forge": "0.9.0", "proxy-from-env": "1.0.0", "semaphore": "1.1.0" }, diff --git a/packages/https-proxy/test/integration/proxy_spec.coffee b/packages/https-proxy/test/integration/proxy_spec.coffee index 036575ff864d..295f048fb147 100644 --- a/packages/https-proxy/test/integration/proxy_spec.coffee +++ b/packages/https-proxy/test/integration/proxy_spec.coffee @@ -132,6 +132,7 @@ describe "Proxy", -> url: "http://localhost:8080/" proxy: "http://localhost:3333" }) + .then (html) -> expect(html).to.include("http server") @@ -159,14 +160,9 @@ describe "Proxy", -> @sandbox.spy(@proxy, "_generateMissingCertificates") @sandbox.spy(@proxy, "_getServerPortForIp") - remove = (folder, file) => - fs.removeAsync(path.join(folder, file)) - Promise.all([ httpsServer.start(8445), - remove(@proxy._ca.certsFolder, '127.0.0.1.pem'), - remove(@proxy._ca.keysFolder, '127.0.0.1.key'), - remove(@proxy._ca.keysFolder, '127.0.0.1.public.key') + @proxy._ca.removeAll() ]) .then => request({ diff --git a/packages/server/lib/server.coffee b/packages/server/lib/server.coffee index e2d1548827a3..a06491932dfe 100644 --- a/packages/server/lib/server.coffee +++ b/packages/server/lib/server.coffee @@ -701,7 +701,6 @@ class Server @_fileServer?.close() @_httpsProxy?.close() ) - .catch { message: "Not running" }, _.noop .then => ## reset any middleware @_middleware = null diff --git a/packages/server/test/integration/server_spec.coffee b/packages/server/test/integration/server_spec.coffee index 54c96396e10b..92f96ced8427 100644 --- a/packages/server/test/integration/server_spec.coffee +++ b/packages/server/test/integration/server_spec.coffee @@ -65,36 +65,26 @@ describe "Server", -> } rp(options) - open = => - Promise.all([ - ## open our https server - httpsServer.start(8443), + return Promise.all([ + ## open our https server + httpsServer.start(8443), - ## and open our cypress server - @server = Server() + ## and open our cypress server + @server = Server() - @server.open(cfg) - .spread (port) => - if initialUrl - @server._onDomainSet(initialUrl) + @server.open(cfg) + .spread (port) => + if initialUrl + @server._onDomainSet(initialUrl) - @srv = @server.getHttpServer() + @srv = @server.getHttpServer() - # @session = new (Session({app: @srv})) + # @session = new (Session({app: @srv})) - @proxy = "http://localhost:" + port + @proxy = "http://localhost:" + port - @fileServer = @server._fileServer.address() - ]) - - if @server - Promise.join( - httpsServer.stop() - @server.close() - ) - .then(open) - else - open() + @fileServer = @server._fileServer.address() + ]) afterEach -> nock.cleanAll() From 473bfbc070dc5921f9f35ead07f35833e331c563 Mon Sep 17 00:00:00 2001 From: Zach Bloomquist Date: Thu, 12 Sep 2019 10:12:59 -0400 Subject: [PATCH 07/10] Improve proxy_spec test --- packages/https-proxy/test/integration/proxy_spec.coffee | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/https-proxy/test/integration/proxy_spec.coffee b/packages/https-proxy/test/integration/proxy_spec.coffee index 295f048fb147..b720a27b4a71 100644 --- a/packages/https-proxy/test/integration/proxy_spec.coffee +++ b/packages/https-proxy/test/integration/proxy_spec.coffee @@ -179,7 +179,8 @@ describe "Proxy", -> }) .then => expect(@proxy._ipServers["127.0.0.1"]).to.be.an.instanceOf(https.Server) - expect(@proxy._getServerPortForIp).to.be.calledWith('127.0.0.1', sinon.match.any) + expect(@proxy._getServerPortForIp).to.be.calledWith('127.0.0.1').and.be.calledOnce + expect(@proxy._generateMissingCertificates).to.be.calledTwice context "closing", -> it "resets sslServers and can reopen", -> From 2f5f838db8e1ab14ab63f1bedac9bde61b802a90 Mon Sep 17 00:00:00 2001 From: Zach Bloomquist Date: Thu, 12 Sep 2019 11:31:49 -0400 Subject: [PATCH 08/10] Don't crash on server error events --- packages/https-proxy/lib/server.coffee | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/packages/https-proxy/lib/server.coffee b/packages/https-proxy/lib/server.coffee index 71cb8967cc15..a0de73751f9e 100644 --- a/packages/https-proxy/lib/server.coffee +++ b/packages/https-proxy/lib/server.coffee @@ -99,6 +99,10 @@ class Server res.end() .pipe(res) + _onServerError: (err) -> + ## these need to be caught to avoid crashing but do not affect anything + debug('server error %o', { err }) + _getProxyForUrl: (urlStr) -> port = Number(_.get(url.parse(urlStr), 'port')) @@ -227,7 +231,7 @@ class Server @_sniServer.addContext(hostname, data) return @_sniPort - + _listenHttpsServer: (data) -> new Promise (resolve, reject) => server = https.createServer(data) @@ -237,12 +241,13 @@ class Server server.once "error", reject server.on "upgrade", @_onUpgrade.bind(@, @_options.onUpgrade) server.on "request", @_onRequest.bind(@, @_options.onRequest) - - server.listen 0, '127.0.0.1', -> + + server.listen 0, '127.0.0.1', => port = server.address().port server.removeListener("error", reject) - + server.on "error", @_onServerError + resolve({ server, port }) ## browsers will not do SNI for an IP address @@ -273,8 +278,9 @@ class Server close: -> close = => servers = _.values(sslIpServers).concat(@_sniServer) - Promise.map servers, (server) -> + Promise.map servers, (server) => Promise.fromCallback(server.destroy) + .catch @_onServerError close() .finally(module.exports.reset) From 7cd5a20a3a39862e2ec92848d1dc001f0d88326e Mon Sep 17 00:00:00 2001 From: Zach Bloomquist Date: Thu, 12 Sep 2019 12:41:50 -0400 Subject: [PATCH 09/10] feedback --- packages/https-proxy/lib/server.coffee | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/https-proxy/lib/server.coffee b/packages/https-proxy/lib/server.coffee index a0de73751f9e..ae54ac604944 100644 --- a/packages/https-proxy/lib/server.coffee +++ b/packages/https-proxy/lib/server.coffee @@ -22,6 +22,10 @@ SSL_RECORD_TYPES = [ 128, 0 ## TODO: what do these unknown types mean? ] +onError: (err) -> + ## these need to be caught to avoid crashing but do not affect anything + debug('server error %o', { err }) + class Server constructor: (@_ca, @_port, @_options) -> @_onError = null @@ -99,10 +103,6 @@ class Server res.end() .pipe(res) - _onServerError: (err) -> - ## these need to be caught to avoid crashing but do not affect anything - debug('server error %o', { err }) - _getProxyForUrl: (urlStr) -> port = Number(_.get(url.parse(urlStr), 'port')) @@ -246,7 +246,7 @@ class Server port = server.address().port server.removeListener("error", reject) - server.on "error", @_onServerError + server.on "error", onError resolve({ server, port }) @@ -280,7 +280,7 @@ class Server servers = _.values(sslIpServers).concat(@_sniServer) Promise.map servers, (server) => Promise.fromCallback(server.destroy) - .catch @_onServerError + .catch onError close() .finally(module.exports.reset) From 74fdc7c80a86f0ee211d3e6ae557ee3220846d55 Mon Sep 17 00:00:00 2001 From: Zach Bloomquist Date: Thu, 12 Sep 2019 12:57:39 -0400 Subject: [PATCH 10/10] derp --- packages/https-proxy/lib/server.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/https-proxy/lib/server.coffee b/packages/https-proxy/lib/server.coffee index ae54ac604944..db7358926397 100644 --- a/packages/https-proxy/lib/server.coffee +++ b/packages/https-proxy/lib/server.coffee @@ -22,7 +22,7 @@ SSL_RECORD_TYPES = [ 128, 0 ## TODO: what do these unknown types mean? ] -onError: (err) -> +onError = (err) -> ## these need to be caught to avoid crashing but do not affect anything debug('server error %o', { err })