Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix proxying HTTPS requests to IP addresses #4947

Merged
merged 12 commits into from
Sep 12, 2019
50 changes: 37 additions & 13 deletions packages/https-proxy/lib/server.coffee
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -23,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
Expand Down Expand Up @@ -217,25 +217,47 @@ 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) =>
flotwig marked this conversation as resolved.
Show resolved Hide resolved
if server = @_ipServers[ip]
return 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
Expand All @@ -246,8 +268,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 ->
Expand All @@ -258,9 +282,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)
}
5 changes: 2 additions & 3 deletions packages/https-proxy/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,9 @@
"debug": "4.1.1",
"fs-extra": "8.1.0",
"lodash": "4.17.15",
"node-forge": "0.6.49",
"node-forge": "0.8.5",
flotwig marked this conversation as resolved.
Show resolved Hide resolved
"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",
Expand Down
2 changes: 1 addition & 1 deletion packages/https-proxy/test/helpers/https_server.coffee
Original file line number Diff line number Diff line change
@@ -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) ->
Expand Down
5 changes: 4 additions & 1 deletion packages/https-proxy/test/helpers/proxy.coffee
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
{ allowDestroy } = require("@packages/network")
http = require("http")
path = require("path")
httpsProxy = require("../../lib/proxy")
Expand Down Expand Up @@ -28,6 +29,8 @@ module.exports = {
start: (port) ->
prx = http.createServer()

allowDestroy(prx)

dir = path.join(process.cwd(), "ca")

httpsProxy.create(dir, port, {
Expand Down Expand Up @@ -61,7 +64,7 @@ module.exports = {

stop: ->
new Promise (resolve) ->
prx.close(resolve)
prx.destroy(resolve)
.then ->
prx.proxy.close()
}
35 changes: 34 additions & 1 deletion packages/https-proxy/test/integration/proxy_spec.coffee
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down Expand Up @@ -152,6 +154,37 @@ 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")

remove = (folder, file) =>
fs.removeAsync(path.join(folder, file))
flotwig marked this conversation as resolved.
Show resolved Hide resolved

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._ipServers["127.0.0.1"]).to.be.an.instanceOf(https.Server)
expect(@proxy._getServerPortForIp).to.be.calledWith('127.0.0.1', sinon.match.any)
flotwig marked this conversation as resolved.
Show resolved Hide resolved

context "closing", ->
it "resets sslServers and can reopen", ->
request({
Expand Down Expand Up @@ -256,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
Expand Down
31 changes: 31 additions & 0 deletions packages/network/lib/allow-destroy.ts
Original file line number Diff line number Diff line change
@@ -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
}
9 changes: 6 additions & 3 deletions packages/network/lib/index.ts
Original file line number Diff line number Diff line change
@@ -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,
}
28 changes: 3 additions & 25 deletions packages/network/test/support/servers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,36 +4,14 @@ 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<void>
destroyAsync: () => Promise<void>
listenAsync: (port) => Promise<void>
}

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()

Expand Down Expand Up @@ -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, <http.RequestListener>app))
allowDestroy(https.createServer(this.https, <http.RequestListener>app))
) as https.Server & AsyncServer

this.wssServer = Io.server(this.httpsServer)
Expand Down
9 changes: 6 additions & 3 deletions packages/network/test/unit/agent_spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions packages/server/lib/server.coffee
Original file line number Diff line number Diff line change
Expand Up @@ -701,6 +701,7 @@ class Server
@_fileServer?.close()
@_httpsProxy?.close()
)
.catch { message: "Not running" }, _.noop
flotwig marked this conversation as resolved.
Show resolved Hide resolved
.then =>
## reset any middleware
@_middleware = null
Expand Down
11 changes: 2 additions & 9 deletions packages/server/lib/util/server_destroy.js
Original file line number Diff line number Diff line change
@@ -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)
Expand All @@ -16,4 +9,4 @@ module.exports = function (server) {
.catch(() => {})
}
}
//# dont catch any errors
// dont catch any errors
1 change: 0 additions & 1 deletion packages/server/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down