decaffeinate: Convert ca.coffee and 11 other files to JS

This commit is contained in:
decaffeinate
2020-02-20 12:04:29 -05:00
committed by Zach Bloomquist
parent 84a2caa6e8
commit 34b52074c1
12 changed files with 1110 additions and 903 deletions
+132 -110
View File
@@ -1,20 +1,27 @@
_ = require("lodash")
fs = require("fs-extra")
os = require("os")
path = require("path")
Forge = require("node-forge")
Promise = require("bluebird")
/*
* decaffeinate suggestions:
* DS102: Remove unnecessary code created because of implicit returns
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
*/
const _ = require("lodash");
let fs = require("fs-extra");
const os = require("os");
const path = require("path");
const Forge = require("node-forge");
const Promise = require("bluebird");
fs = Promise.promisifyAll(fs)
fs = Promise.promisifyAll(fs);
pki = Forge.pki
const {
pki
} = Forge;
generateKeyPairAsync = Promise.promisify(pki.rsa.generateKeyPair)
const generateKeyPairAsync = Promise.promisify(pki.rsa.generateKeyPair);
ipAddressRe = /^[\d\.]+$/
asterisksRe = /\*/g
const ipAddressRe = /^[\d\.]+$/;
const asterisksRe = /\*/g;
CAattrs = [{
const CAattrs = [{
name: "commonName",
value: "CypressProxyCA"
}, {
@@ -32,9 +39,9 @@ CAattrs = [{
}, {
shortName: "OU",
value: "CA"
}]
}];
CAextensions = [{
const CAextensions = [{
name: "basicConstraints",
cA: true
}, {
@@ -62,9 +69,9 @@ CAextensions = [{
objCA: true
}, {
name: "subjectKeyIdentifier"
}]
}];
ServerAttrs = [{
const ServerAttrs = [{
name: "countryName",
value: "Internet"
}, {
@@ -79,9 +86,9 @@ ServerAttrs = [{
}, {
shortName: "OU",
value: "Cypress Proxy Server Certificate"
}]
}];
ServerExtensions = [{
const ServerExtensions = [{
name: "basicConstraints",
cA: false
}, {
@@ -109,132 +116,147 @@ ServerExtensions = [{
objCA: false
}, {
name: "subjectKeyIdentifier"
}]
}];
class CA
constructor: (caFolder) ->
if not caFolder
caFolder = path.join(os.tmpdir(), 'cy-ca')
class CA {
constructor(caFolder) {
if (!caFolder) {
caFolder = path.join(os.tmpdir(), 'cy-ca');
}
@baseCAFolder = caFolder
@certsFolder = path.join(@baseCAFolder, "certs")
@keysFolder = path.join(@baseCAFolder, "keys")
this.baseCAFolder = caFolder;
this.certsFolder = path.join(this.baseCAFolder, "certs");
this.keysFolder = path.join(this.baseCAFolder, "keys");
}
removeAll: ->
fs
.removeAsync(@baseCAFolder)
.catchReturn({ code: "ENOENT" })
removeAll() {
return fs
.removeAsync(this.baseCAFolder)
.catchReturn({ code: "ENOENT" });
}
randomSerialNumber: ->
## generate random 16 bytes hex string
sn = ""
randomSerialNumber() {
//# generate random 16 bytes hex string
let sn = "";
for i in [1..4]
sn += ("00000000" + Math.floor(Math.random()*Math.pow(256, 4)).toString(16)).slice(-8)
for (let i = 1; i <= 4; i++) {
sn += ("00000000" + Math.floor(Math.random()*Math.pow(256, 4)).toString(16)).slice(-8);
}
sn
return sn;
}
generateCA: ->
generateKeyPairAsync({bits: 512})
.then (keys) =>
cert = pki.createCertificate()
cert.publicKey = keys.publicKey
cert.serialNumber = @randomSerialNumber()
generateCA() {
return generateKeyPairAsync({bits: 512})
.then(keys => {
const cert = pki.createCertificate();
cert.publicKey = keys.publicKey;
cert.serialNumber = this.randomSerialNumber();
cert.validity.notBefore = new Date()
cert.validity.notAfter = new Date()
cert.validity.notAfter.setFullYear(cert.validity.notBefore.getFullYear() + 10)
cert.setSubject(CAattrs)
cert.setIssuer(CAattrs)
cert.setExtensions(CAextensions)
cert.sign(keys.privateKey, Forge.md.sha256.create())
cert.validity.notBefore = new Date();
cert.validity.notAfter = new Date();
cert.validity.notAfter.setFullYear(cert.validity.notBefore.getFullYear() + 10);
cert.setSubject(CAattrs);
cert.setIssuer(CAattrs);
cert.setExtensions(CAextensions);
cert.sign(keys.privateKey, Forge.md.sha256.create());
@CAcert = cert
@CAkeys = keys
this.CAcert = cert;
this.CAkeys = keys;
Promise.all([
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))
])
return Promise.all([
fs.outputFileAsync(path.join(this.certsFolder, "ca.pem"), pki.certificateToPem(cert)),
fs.outputFileAsync(path.join(this.keysFolder, "ca.private.key"), pki.privateKeyToPem(keys.privateKey)),
fs.outputFileAsync(path.join(this.keysFolder, "ca.public.key"), pki.publicKeyToPem(keys.publicKey))
]);
});
}
loadCA: ->
Promise.props({
certPEM: fs.readFileAsync(path.join(@certsFolder, "ca.pem"), "utf-8")
keyPrivatePEM: fs.readFileAsync(path.join(@keysFolder, "ca.private.key"), "utf-8")
keyPublicPEM: fs.readFileAsync(path.join(@keysFolder, "ca.public.key"), "utf-8")
loadCA() {
return Promise.props({
certPEM: fs.readFileAsync(path.join(this.certsFolder, "ca.pem"), "utf-8"),
keyPrivatePEM: fs.readFileAsync(path.join(this.keysFolder, "ca.private.key"), "utf-8"),
keyPublicPEM: fs.readFileAsync(path.join(this.keysFolder, "ca.public.key"), "utf-8")
})
.then (results) =>
@CAcert = pki.certificateFromPem(results.certPEM)
@CAkeys = {
privateKey: pki.privateKeyFromPem(results.keyPrivatePEM)
.then(results => {
this.CAcert = pki.certificateFromPem(results.certPEM);
return this.CAkeys = {
privateKey: pki.privateKeyFromPem(results.keyPrivatePEM),
publicKey: pki.publicKeyFromPem(results.keyPublicPEM)
}
.return(undefined)
};
})
.return(undefined);
}
generateServerCertificateKeys: (hosts) ->
hosts = [].concat(hosts)
generateServerCertificateKeys(hosts) {
hosts = [].concat(hosts);
mainHost = hosts[0]
keysServer = pki.rsa.generateKeyPair(1024)
certServer = pki.createCertificate()
const mainHost = hosts[0];
const keysServer = pki.rsa.generateKeyPair(1024);
const certServer = pki.createCertificate();
certServer.publicKey = keysServer.publicKey
certServer.serialNumber = @randomSerialNumber()
certServer.validity.notBefore = new Date
certServer.validity.notAfter = new Date
certServer.validity.notAfter.setFullYear(certServer.validity.notBefore.getFullYear() + 2)
certServer.publicKey = keysServer.publicKey;
certServer.serialNumber = this.randomSerialNumber();
certServer.validity.notBefore = new Date;
certServer.validity.notAfter = new Date;
certServer.validity.notAfter.setFullYear(certServer.validity.notBefore.getFullYear() + 2);
attrsServer = _.clone(ServerAttrs)
const attrsServer = _.clone(ServerAttrs);
attrsServer.unshift({
name: "commonName",
value: mainHost
})
});
certServer.setSubject(attrsServer)
certServer.setIssuer(@CAcert.issuer.attributes)
certServer.setSubject(attrsServer);
certServer.setIssuer(this.CAcert.issuer.attributes);
certServer.setExtensions(ServerExtensions.concat([{
name: "subjectAltName",
altNames: hosts.map (host) ->
if host.match(ipAddressRe)
{type: 7, ip: host}
else
{type: 2, value: host}
}]))
altNames: hosts.map(function(host) {
if (host.match(ipAddressRe)) {
return {type: 7, ip: host};
} else {
return {type: 2, value: host};
}})
}]));
certServer.sign(@CAkeys.privateKey, Forge.md.sha256.create())
certServer.sign(this.CAkeys.privateKey, Forge.md.sha256.create());
certPem = pki.certificateToPem(certServer)
keyPrivatePem = pki.privateKeyToPem(keysServer.privateKey)
keyPublicPem = pki.publicKeyToPem(keysServer.publicKey)
const certPem = pki.certificateToPem(certServer);
const keyPrivatePem = pki.privateKeyToPem(keysServer.privateKey);
const keyPublicPem = pki.publicKeyToPem(keysServer.publicKey);
dest = mainHost.replace(asterisksRe, "_")
const dest = mainHost.replace(asterisksRe, "_");
Promise.all([
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 Promise.all([
fs.outputFileAsync(path.join(this.certsFolder, dest + ".pem"), certPem),
fs.outputFileAsync(path.join(this.keysFolder, dest + ".key"), keyPrivatePem),
fs.outputFileAsync(path.join(this.keysFolder, dest + ".public.key"), keyPublicPem)
])
.return([certPem, keyPrivatePem])
.return([certPem, keyPrivatePem]);
}
getCertificateKeysForHostname: (hostname) ->
dest = hostname.replace(asterisksRe, "_")
getCertificateKeysForHostname(hostname) {
const dest = hostname.replace(asterisksRe, "_");
Promise.all([
fs.readFileAsync(path.join(@certsFolder, dest + ".pem"))
fs.readFileAsync(path.join(@keysFolder, dest + ".key"))
])
return Promise.all([
fs.readFileAsync(path.join(this.certsFolder, dest + ".pem")),
fs.readFileAsync(path.join(this.keysFolder, dest + ".key"))
]);
}
getCACertPath: ->
path.join(@certsFolder, "ca.pem")
getCACertPath() {
return path.join(this.certsFolder, "ca.pem");
}
@create = (caFolder) ->
ca = new CA(caFolder)
static create(caFolder) {
const ca = new CA(caFolder);
fs.statAsync(path.join(ca.certsFolder, "ca.pem"))
return fs.statAsync(path.join(ca.certsFolder, "ca.pem"))
.bind(ca)
.then(ca.loadCA)
.catch(ca.generateCA)
.return(ca)
.return(ca);
}
}
module.exports = CA
module.exports = CA;
+18 -11
View File
@@ -1,16 +1,23 @@
CA = require("./ca")
Server = require("./server")
/*
* decaffeinate suggestions:
* DS102: Remove unnecessary code created because of implicit returns
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
*/
const CA = require("./ca");
const Server = require("./server");
module.exports = {
create: (dir, port, options) ->
CA.create(dir)
.then (ca) ->
Server.create(ca, port, options)
create(dir, port, options) {
return CA.create(dir)
.then(ca => Server.create(ca, port, options));
},
reset: ->
Server.reset()
reset() {
return Server.reset();
},
httpsServer: (onRequest) ->
require("../test/helpers/https_server").create(onRequest)
httpsServer(onRequest) {
return require("../test/helpers/https_server").create(onRequest);
}
}
};
+322 -258
View File
@@ -1,296 +1,360 @@
_ = require("lodash")
{ agent, allowDestroy, connect } = require("@packages/network")
debug = require("debug")("cypress:https-proxy")
fs = require("fs-extra")
getProxyForUrl = require("proxy-from-env").getProxyForUrl
https = require("https")
net = require("net")
parse = require("./util/parse")
Promise = require("bluebird")
semaphore = require("semaphore")
url = require("url")
/*
* decaffeinate suggestions:
* DS102: Remove unnecessary code created because of implicit returns
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
*/
const _ = require("lodash");
const { agent, allowDestroy, connect } = require("@packages/network");
const debug = require("debug")("cypress:https-proxy");
let fs = require("fs-extra");
const {
getProxyForUrl
} = require("proxy-from-env");
const https = require("https");
const net = require("net");
const parse = require("./util/parse");
const Promise = require("bluebird");
const semaphore = require("semaphore");
const url = require("url");
fs = Promise.promisifyAll(fs)
fs = Promise.promisifyAll(fs);
sslServers = {}
sslIpServers = {}
sslSemaphores = {}
let sslServers = {};
let sslIpServers = {};
const sslSemaphores = {};
## https://en.wikipedia.org/wiki/Transport_Layer_Security#TLS_record
SSL_RECORD_TYPES = [
22 ## Handshake
128, 0 ## TODO: what do these unknown types mean?
]
//# https://en.wikipedia.org/wiki/Transport_Layer_Security#TLS_record
const SSL_RECORD_TYPES = [
22, //# Handshake
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 })
let 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
@_ipServers = sslIpServers
class Server {
constructor(_ca, _port, _options) {
this._getServerPortForIp = this._getServerPortForIp.bind(this);
this._ca = _ca;
this._port = _port;
this._options = _options;
this._onError = null;
this._ipServers = sslIpServers;
}
connect: (req, browserSocket, head, options = {}) ->
## don't buffer writes - thanks a lot, Nagle
## https://github.com/cypress-io/cypress/issues/3192
browserSocket.setNoDelay(true)
connect(req, browserSocket, head, options = {}) {
//# don't buffer writes - thanks a lot, Nagle
//# https://github.com/cypress-io/cypress/issues/3192
browserSocket.setNoDelay(true);
debug("Writing browserSocket connection headers %o", { url: req.url, headLength: _.get(head, 'length'), headers: req.headers })
debug("Writing browserSocket connection headers %o", { url: req.url, headLength: _.get(head, 'length'), headers: req.headers });
browserSocket.on "error", (err) =>
## TODO: shouldn't we destroy the upstream socket here?
## and also vise versa if the upstream socket throws?
## we may get this "for free" though because piping will
## automatically forward the TCP errors...?
browserSocket.on("error", err => {
//# TODO: shouldn't we destroy the upstream socket here?
//# and also vise versa if the upstream socket throws?
//# we may get this "for free" though because piping will
//# automatically forward the TCP errors...?
## nothing to do except catch here, the browser has d/c'd
debug("received error on client browserSocket %o", {
//# nothing to do except catch here, the browser has d/c'd
return debug("received error on client browserSocket %o", {
err, url: req.url
})
});
});
browserSocket.write "HTTP/1.1 200 OK\r\n"
browserSocket.write("HTTP/1.1 200 OK\r\n");
if req.headers["proxy-connection"] is "keep-alive"
browserSocket.write("Proxy-Connection: keep-alive\r\n")
browserSocket.write("Connection: keep-alive\r\n")
browserSocket.write("\r\n")
## if we somehow already have the head here
if _.get(head, "length")
## then immediately make up the connection
return @_onFirstHeadBytes(req, browserSocket, head, options)
## else once we get it make the connection later
browserSocket.once "data", (data) =>
@_onFirstHeadBytes(req, browserSocket, data, options)
_onFirstHeadBytes: (req, browserSocket, head, options) ->
debug("Got first head bytes %o", { url: req.url, head: _.chain(head).invoke('toString').slice(0, 64).join('').value() })
browserSocket.pause()
if odc = options.onDirectConnection
## if onDirectConnection return true
## then dont proxy, just pass this through
if odc.call(@, req, browserSocket, head) is true
return @_makeDirectConnection(req, browserSocket, head)
else
debug("Not making direct connection %o", { url: req.url })
@_onServerConnectData(req, browserSocket, head)
_onUpgrade: (fn, req, browserSocket, head) ->
if fn
fn.call(@, req, browserSocket, head)
_onRequest: (fn, req, res) ->
hostPort = parse.hostAndPort(req.url, req.headers, 443)
req.url = url.format({
protocol: "https:"
hostname: hostPort.host
port: hostPort.port
}) + req.url
if fn
return fn.call(@, req, res)
_getProxyForUrl: (urlStr) ->
port = Number(_.get(url.parse(urlStr), 'port'))
debug('getting proxy URL %o', { port, serverPort: @_port, sniPort: @_sniPort, url: urlStr })
if [@_sniPort, @_port].includes(port)
## https://github.com/cypress-io/cypress/issues/4257
## this is a tunnel to the SNI server or to the main server,
## it should never go through a proxy
return undefined
getProxyForUrl(urlStr)
_makeDirectConnection: (req, browserSocket, head) ->
{ port, hostname } = url.parse("https://#{req.url}")
debug("Making connection to #{hostname}:#{port}")
@_makeConnection(browserSocket, head, port, hostname)
_makeConnection: (browserSocket, head, port, hostname) ->
onSocket = (err, upstreamSocket) =>
debug('received upstreamSocket callback for request %o', { port, hostname, err })
onError = (err) =>
browserSocket.destroy(err)
if @_onError
@_onError(err, browserSocket, head, port)
if err
return onError(err)
upstreamSocket.setNoDelay(true)
upstreamSocket.on "error", onError
browserSocket.emit 'upstream-connected', upstreamSocket
browserSocket.pipe(upstreamSocket)
upstreamSocket.pipe(browserSocket)
upstreamSocket.write(head)
browserSocket.resume()
port or= "443"
if upstreamProxy = @_getProxyForUrl("https://#{hostname}:#{port}")
# todo: as soon as all requests are intercepted, this can go away since this is just for pass-through
debug("making proxied connection %o", {
host: "#{hostname}:#{port}",
proxy: upstreamProxy,
})
return agent.httpsAgent.createUpstreamProxyConnection {
proxy: upstreamProxy
href: "https://#{hostname}:#{port}"
uri: {
port
hostname
}
shouldRetry: true
}, onSocket
return connect.createRetryingSocket({ port, host: hostname }, onSocket)
_onServerConnectData: (req, browserSocket, head) ->
firstBytes = head[0]
makeConnection = (port) =>
debug("Making intercepted connection to %s", port)
@_makeConnection(browserSocket, head, port, "localhost")
if firstBytes not in SSL_RECORD_TYPES
## if this isn't an SSL request then go
## ahead and make the connection now
return makeConnection(@_port)
## else spin up the SNI server
{ hostname } = url.parse("https://#{req.url}")
if sslServer = sslServers[hostname]
return makeConnection(sslServer.port)
## only be creating one SSL server per hostname at once
if not sem = sslSemaphores[hostname]
sem = sslSemaphores[hostname] = semaphore(1)
sem.take =>
leave = ->
process.nextTick ->
sem.leave()
if sslServer = sslServers[hostname]
leave()
return makeConnection(sslServer.port)
@_getPortFor(hostname)
.then (port) ->
sslServers[hostname] = { port: port }
leave()
makeConnection(port)
_normalizeKeyAndCert: (certPem, privateKeyPem) ->
return {
key: privateKeyPem
cert: certPem
if (req.headers["proxy-connection"] === "keep-alive") {
browserSocket.write("Proxy-Connection: keep-alive\r\n");
browserSocket.write("Connection: keep-alive\r\n");
}
_getCertificatePathsFor: (hostname) ->
@_ca.getCertificateKeysForHostname(hostname)
.spread(@_normalizeKeyAndCert)
browserSocket.write("\r\n");
_generateMissingCertificates: (hostname) ->
@_ca.generateServerCertificateKeys(hostname)
.spread(@_normalizeKeyAndCert)
//# if we somehow already have the head here
if (_.get(head, "length")) {
//# then immediately make up the connection
return this._onFirstHeadBytes(req, browserSocket, head, options);
}
_getPortFor: (hostname) ->
@_getCertificatePathsFor(hostname)
.catch (err) =>
@_generateMissingCertificates(hostname)
.then (data = {}) =>
if net.isIP(hostname)
return @_getServerPortForIp(hostname, data)
//# else once we get it make the connection later
return browserSocket.once("data", data => {
return this._onFirstHeadBytes(req, browserSocket, data, options);
});
}
@_sniServer.addContext(hostname, data)
_onFirstHeadBytes(req, browserSocket, head, options) {
let odc;
debug("Got first head bytes %o", { url: req.url, head: _.chain(head).invoke('toString').slice(0, 64).join('').value() });
return @_sniPort
browserSocket.pause();
_listenHttpsServer: (data) ->
new Promise (resolve, reject) =>
server = https.createServer(data)
if (odc = options.onDirectConnection) {
//# if onDirectConnection return true
//# then dont proxy, just pass this through
if (odc.call(this, req, browserSocket, head) === true) {
return this._makeDirectConnection(req, browserSocket, head);
} else {
debug("Not making direct connection %o", { url: req.url });
}
}
allowDestroy(server)
return this._onServerConnectData(req, browserSocket, head);
}
server.once "error", reject
server.on "upgrade", @_onUpgrade.bind(@, @_options.onUpgrade)
server.on "request", @_onRequest.bind(@, @_options.onRequest)
_onUpgrade(fn, req, browserSocket, head) {
if (fn) {
return fn.call(this, req, browserSocket, head);
}
}
server.listen 0, '127.0.0.1', =>
port = server.address().port
_onRequest(fn, req, res) {
const hostPort = parse.hostAndPort(req.url, req.headers, 443);
server.removeListener("error", reject)
server.on "error", onError
req.url = url.format({
protocol: "https:",
hostname: hostPort.host,
port: hostPort.port
}) + req.url;
resolve({ server, port })
if (fn) {
return fn.call(this, req, res);
}
}
## 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
@_listenHttpsServer(data)
.then ({ server, port }) ->
sslIpServers[ip] = server
_getProxyForUrl(urlStr) {
const port = Number(_.get(url.parse(urlStr), 'port'));
debug("Created IP HTTPS Proxy Server", { port, ip })
debug('getting proxy URL %o', { port, serverPort: this._port, sniPort: this._sniPort, url: urlStr });
return port
if ([this._sniPort, this._port].includes(port)) {
//# https://github.com/cypress-io/cypress/issues/4257
//# this is a tunnel to the SNI server or to the main server,
//# it should never go through a proxy
return undefined;
}
listen: ->
@_onError = @_options.onError
return getProxyForUrl(urlStr);
}
@_listenHttpsServer({})
.tap ({ server, port}) =>
@_sniPort = port
@_sniServer = server
_makeDirectConnection(req, browserSocket, head) {
const { port, hostname } = url.parse(`https://${req.url}`);
debug("Created SNI HTTPS Proxy Server", { port })
debug(`Making connection to ${hostname}:${port}`);
return this._makeConnection(browserSocket, head, port, hostname);
}
close: ->
close = =>
servers = _.values(sslIpServers).concat(@_sniServer)
Promise.map servers, (server) =>
Promise.fromCallback(server.destroy)
.catch onError
_makeConnection(browserSocket, head, port, hostname) {
let upstreamProxy;
const onSocket = (err, upstreamSocket) => {
debug('received upstreamSocket callback for request %o', { port, hostname, err });
close()
.finally(module.exports.reset)
onError = err => {
browserSocket.destroy(err);
if (this._onError) {
return this._onError(err, browserSocket, head, port);
}
};
if (err) {
return onError(err);
}
upstreamSocket.setNoDelay(true);
upstreamSocket.on("error", onError);
browserSocket.emit('upstream-connected', upstreamSocket);
browserSocket.pipe(upstreamSocket);
upstreamSocket.pipe(browserSocket);
upstreamSocket.write(head);
return browserSocket.resume();
};
if (!port) { port = "443"; }
if (upstreamProxy = this._getProxyForUrl(`https://${hostname}:${port}`)) {
// todo: as soon as all requests are intercepted, this can go away since this is just for pass-through
debug("making proxied connection %o", {
host: `${hostname}:${port}`,
proxy: upstreamProxy,
});
return agent.httpsAgent.createUpstreamProxyConnection({
proxy: upstreamProxy,
href: `https://${hostname}:${port}`,
uri: {
port,
hostname
},
shouldRetry: true
}, onSocket);
}
return connect.createRetryingSocket({ port, host: hostname }, onSocket);
}
_onServerConnectData(req, browserSocket, head) {
let sem, sslServer;
const firstBytes = head[0];
const makeConnection = port => {
debug("Making intercepted connection to %s", port);
return this._makeConnection(browserSocket, head, port, "localhost");
};
if (!SSL_RECORD_TYPES.includes(firstBytes)) {
//# if this isn't an SSL request then go
//# ahead and make the connection now
return makeConnection(this._port);
}
//# else spin up the SNI server
const { hostname } = url.parse(`https://${req.url}`);
if (sslServer = sslServers[hostname]) {
return makeConnection(sslServer.port);
}
//# only be creating one SSL server per hostname at once
if (!(sem = sslSemaphores[hostname])) {
sem = (sslSemaphores[hostname] = semaphore(1));
}
return sem.take(() => {
const leave = () => process.nextTick(() => sem.leave());
if (sslServer = sslServers[hostname]) {
leave();
return makeConnection(sslServer.port);
}
return this._getPortFor(hostname)
.then(function(port) {
sslServers[hostname] = { port };
leave();
return makeConnection(port);
});
});
}
_normalizeKeyAndCert(certPem, privateKeyPem) {
return {
key: privateKeyPem,
cert: certPem
};
}
_getCertificatePathsFor(hostname) {
return this._ca.getCertificateKeysForHostname(hostname)
.spread(this._normalizeKeyAndCert);
}
_generateMissingCertificates(hostname) {
return this._ca.generateServerCertificateKeys(hostname)
.spread(this._normalizeKeyAndCert);
}
_getPortFor(hostname) {
return this._getCertificatePathsFor(hostname)
.catch(err => {
return this._generateMissingCertificates(hostname);
}).then((data = {}) => {
if (net.isIP(hostname)) {
return this._getServerPortForIp(hostname, data);
}
this._sniServer.addContext(hostname, data);
return this._sniPort;
});
}
_listenHttpsServer(data) {
return new Promise((resolve, reject) => {
const server = https.createServer(data);
allowDestroy(server);
server.once("error", reject);
server.on("upgrade", this._onUpgrade.bind(this, this._options.onUpgrade));
server.on("request", this._onRequest.bind(this, this._options.onRequest));
return server.listen(0, '127.0.0.1', () => {
const {
port
} = server.address();
server.removeListener("error", reject);
server.on("error", onError);
return resolve({ server, 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) {
let server;
if (server = sslIpServers[ip]) {
return server.address().port;
}
return this._listenHttpsServer(data)
.then(function({ server, port }) {
sslIpServers[ip] = server;
debug("Created IP HTTPS Proxy Server", { port, ip });
return port;
});
}
listen() {
this._onError = this._options.onError;
return this._listenHttpsServer({})
.tap(({ server, port}) => {
this._sniPort = port;
this._sniServer = server;
return debug("Created SNI HTTPS Proxy Server", { port });
});
}
close() {
const close = () => {
const servers = _.values(sslIpServers).concat(this._sniServer);
return Promise.map(servers, server => {
return Promise.fromCallback(server.destroy)
.catch(onError);
});
};
return close()
.finally(module.exports.reset);
}
}
module.exports = {
reset: ->
sslServers = {}
sslIpServers = {}
reset() {
sslServers = {};
return sslIpServers = {};
},
create: (ca, port, options = {}) ->
srv = new Server(ca, port, options)
create(ca, port, options = {}) {
const srv = new Server(ca, port, options);
srv
return srv
.listen()
.return(srv)
}
.return(srv);
}
};
+38 -25
View File
@@ -1,35 +1,48 @@
url = require("url")
/*
* decaffeinate suggestions:
* DS102: Remove unnecessary code created because of implicit returns
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
*/
const url = require("url");
module.exports = {
parseHost: (hostString, defaultPort) ->
if m = hostString.match(/^http:\/\/(.*)/)
parsedUrl = url.parse(hostString)
parseHost(hostString, defaultPort) {
let m;
if (m = hostString.match(/^http:\/\/(.*)/)) {
const parsedUrl = url.parse(hostString);
return {
host: parsedUrl.hostname
host: parsedUrl.hostname,
port: parsedUrl.port
}
hostPort = hostString.split(':')
host = hostPort[0]
port = if hostPort.length is 2 then +hostPort[1] else defaultPort
return {
host: host
port: port
};
}
hostAndPort: (reqUrl, headers, defaultPort) ->
host = headers.host
const hostPort = hostString.split(':');
const host = hostPort[0];
const port = hostPort.length === 2 ? +hostPort[1] : defaultPort;
hostPort = @parseHost(host, defaultPort)
return {
host,
port
};
},
## this handles paths which include the full url. This could happen if it's a proxy
if m = reqUrl.match(/^http:\/\/([^\/]*)\/?(.*)$/)
parsedUrl = url.parse(reqUrl)
hostPort.host = parsedUrl.hostname
hostPort.port = parsedUrl.port
reqUrl = parsedUrl.path
hostAndPort(reqUrl, headers, defaultPort) {
let m;
const {
host
} = headers;
hostPort
}
const hostPort = this.parseHost(host, defaultPort);
//# this handles paths which include the full url. This could happen if it's a proxy
if (m = reqUrl.match(/^http:\/\/([^\/]*)\/?(.*)$/)) {
const parsedUrl = url.parse(reqUrl);
hostPort.host = parsedUrl.hostname;
hostPort.port = parsedUrl.port;
reqUrl = parsedUrl.path;
}
return hostPort;
}
};
+8 -8
View File
@@ -1,14 +1,14 @@
fs = require("fs")
path = require("path")
sslRootCas = require('ssl-root-cas')
const fs = require("fs");
const path = require("path");
const sslRootCas = require('ssl-root-cas');
sslRootCas
.inject()
.addFile(path.join(__dirname, "certs", "server", "my-root-ca.crt.pem"))
.addFile(path.join(__dirname, "certs", "server", "my-root-ca.crt.pem"));
options = {
key: fs.readFileSync(path.join(__dirname, "certs", "server", "my-server.key.pem"))
const options = {
key: fs.readFileSync(path.join(__dirname, "certs", "server", "my-server.key.pem")),
cert: fs.readFileSync(path.join(__dirname, "certs", "server", "my-server.crt.pem"))
}
};
module.exports = options
module.exports = options;
@@ -1,24 +1,31 @@
http = require("http")
Promise = require("bluebird")
/*
* decaffeinate suggestions:
* DS102: Remove unnecessary code created because of implicit returns
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
*/
const http = require("http");
const Promise = require("bluebird");
srv = http.createServer (req, res) ->
console.log "HTTP SERVER REQUEST URL:", req.url
console.log "HTTP SERVER REQUEST HEADERS:", req.headers
const srv = http.createServer(function(req, res) {
console.log("HTTP SERVER REQUEST URL:", req.url);
console.log("HTTP SERVER REQUEST HEADERS:", req.headers);
res.setHeader("Content-Type", "text/html")
res.writeHead(200)
res.end("<html><body>http server</body></html>")
res.setHeader("Content-Type", "text/html");
res.writeHead(200);
return res.end("<html><body>http server</body></html>");
});
module.exports = {
srv: srv
srv,
start: ->
new Promise (resolve) ->
srv.listen 8080, ->
console.log "server listening on port: 8080"
resolve(srv)
start() {
return new Promise(resolve => srv.listen(8080, function() {
console.log("server listening on port: 8080");
return resolve(srv);
}));
},
stop: ->
new Promise (resolve) ->
srv.close(resolve)
}
stop() {
return new Promise(resolve => srv.close(resolve));
}
};
@@ -1,42 +1,49 @@
https = require("https")
Promise = require("bluebird")
{ allowDestroy } = require("@packages/network")
certs = require("./certs")
/*
* decaffeinate suggestions:
* DS102: Remove unnecessary code created because of implicit returns
* DS207: Consider shorter variations of null checks
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
*/
const https = require("https");
const Promise = require("bluebird");
const { allowDestroy } = require("@packages/network");
const certs = require("./certs");
defaultOnRequest = (req, res) ->
console.log "HTTPS SERVER REQUEST URL:", req.url
console.log "HTTPS SERVER REQUEST HEADERS:", req.headers
const defaultOnRequest = function(req, res) {
console.log("HTTPS SERVER REQUEST URL:", req.url);
console.log("HTTPS SERVER REQUEST HEADERS:", req.headers);
res.setHeader("Content-Type", "text/html")
res.writeHead(200)
res.end("<html><head></head><body>https server</body></html>")
res.setHeader("Content-Type", "text/html");
res.writeHead(200);
return res.end("<html><head></head><body>https server</body></html>");
};
servers = []
let servers = [];
create = (onRequest) ->
https.createServer(certs, onRequest ? defaultOnRequest)
const create = onRequest => https.createServer(certs, onRequest != null ? onRequest : defaultOnRequest);
module.exports = {
create
create,
start: (port, onRequest) ->
new Promise (resolve) ->
srv = create(onRequest)
start(port, onRequest) {
return new Promise(function(resolve) {
const srv = create(onRequest);
allowDestroy(srv)
allowDestroy(srv);
servers.push(srv)
servers.push(srv);
srv.listen port, ->
console.log "server listening on port: #{port}"
resolve(srv)
return srv.listen(port, function() {
console.log(`server listening on port: ${port}`);
return resolve(srv);
});
});
},
stop: ->
stop = (srv) ->
new Promise (resolve) ->
srv.destroy(resolve)
stop() {
const stop = srv => new Promise(resolve => srv.destroy(resolve));
Promise.map(servers, stop)
.then ->
servers = []
}
return Promise.map(servers, stop)
.then(() => servers = []);
}
};
+60 -52
View File
@@ -1,70 +1,78 @@
{ allowDestroy } = require("@packages/network")
http = require("http")
path = require("path")
httpsProxy = require("../../lib/proxy")
/*
* decaffeinate suggestions:
* DS102: Remove unnecessary code created because of implicit returns
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
*/
const { allowDestroy } = require("@packages/network");
const http = require("http");
const path = require("path");
const httpsProxy = require("../../lib/proxy");
prx = null
let prx = null;
pipe = (req, res) ->
req.pipe(request(req.url))
.on "error", ->
console.log "**ERROR**", req.url
req.statusCode = 500
res.end()
.pipe(res)
const pipe = (req, res) => req.pipe(request(req.url))
.on("error", function() {
console.log("**ERROR**", req.url);
req.statusCode = 500;
return res.end();
}).pipe(res);
onConnect = (req, socket, head, proxy) ->
proxy.connect(req, socket, head, {
onDirectConnection: (req, socket, head) ->
["localhost:8444", "localhost:12344"].includes(req.url)
})
const onConnect = (req, socket, head, proxy) => proxy.connect(req, socket, head, {
onDirectConnection(req, socket, head) {
return ["localhost:8444", "localhost:12344"].includes(req.url);
}
});
onRequest = (req, res) ->
pipe(req, res)
const onRequest = (req, res) => pipe(req, res);
module.exports = {
reset: ->
httpsProxy.reset()
reset() {
return httpsProxy.reset();
},
start: (port) ->
prx = http.createServer()
start(port) {
prx = http.createServer();
allowDestroy(prx)
allowDestroy(prx);
dir = path.join(process.cwd(), "ca")
const dir = path.join(process.cwd(), "ca");
httpsProxy.create(dir, port, {
onUpgrade: (req, socket, head) ->
return httpsProxy.create(dir, port, {
onUpgrade(req, socket, head) {},
onRequest: (req, res) ->
console.log "ON REQUEST FROM OUTER PROXY", req.url, req.headers, req.method
onRequest(req, res) {
console.log("ON REQUEST FROM OUTER PROXY", req.url, req.headers, req.method);
if req.url.includes("replace")
write = res.write
res.write = (chunk) ->
chunk = Buffer.from(chunk.toString().replace("https server", "replaced content"))
if (req.url.includes("replace")) {
const {
write
} = res;
res.write = function(chunk) {
chunk = Buffer.from(chunk.toString().replace("https server", "replaced content"));
write.call(@, chunk)
return write.call(this, chunk);
};
pipe(req, res)
else
pipe(req, res)
return pipe(req, res);
} else {
return pipe(req, res);
}
}
})
.then (proxy) =>
prx.on "request", onRequest
.then(proxy => {
prx.on("request", onRequest);
prx.on "connect", (req, socket, head) ->
onConnect(req, socket, head, proxy)
prx.on("connect", (req, socket, head) => onConnect(req, socket, head, proxy));
new Promise (resolve) ->
prx.listen port, ->
prx.proxy = proxy
console.log "server listening on port: #{port}"
resolve(proxy)
return new Promise(resolve => prx.listen(port, function() {
prx.proxy = proxy;
console.log(`server listening on port: ${port}`);
return resolve(proxy);
}));
});
},
stop: ->
new Promise (resolve) ->
prx.destroy(resolve)
.then ->
prx.proxy.close()
}
stop() {
return new Promise(resolve => prx.destroy(resolve)).then(() => prx.proxy.close());
}
};
@@ -1,300 +1,321 @@
require("../spec_helper")
/*
* decaffeinate suggestions:
* DS102: Remove unnecessary code created because of implicit returns
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
*/
require("../spec_helper");
process.env.NODE_TLS_REJECT_UNAUTHORIZED = "0"
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")
Promise = require("bluebird")
proxy = require("../helpers/proxy")
httpServer = require("../helpers/http_server")
httpsServer = require("../helpers/https_server")
const _ = require("lodash");
const DebugProxy = require("@cypress/debugging-proxy");
const fs = require("fs-extra");
const https = require("https");
const net = require("net");
const network = require("@packages/network");
const path = require("path");
const Promise = require("bluebird");
const proxy = require("../helpers/proxy");
const httpServer = require("../helpers/http_server");
const httpsServer = require("../helpers/https_server");
describe "Proxy", ->
beforeEach ->
Promise.join(
httpServer.start()
describe("Proxy", function() {
beforeEach(function() {
return Promise.join(
httpServer.start(),
httpsServer.start(8443)
httpsServer.start(8443),
httpsServer.start(8444)
httpsServer.start(8444),
proxy.start(3333)
.then (@proxy) =>
)
.then(proxy1 => {
this.proxy = proxy1;
})
);
});
afterEach ->
Promise.join(
httpServer.stop()
httpsServer.stop()
proxy.stop()
)
afterEach(() => Promise.join(
httpServer.stop(),
httpsServer.stop(),
proxy.stop()
));
it "can request the googles", ->
## give some padding to external
## network request
@timeout(10000)
it("can request the googles", function() {
//# give some padding to external
//# network request
this.timeout(10000);
Promise.all([
return Promise.all([
request({
strictSSL: false
proxy: "http://localhost:3333"
strictSSL: false,
proxy: "http://localhost:3333",
url: "https://www.google.com"
})
}),
request({
strictSSL: false
proxy: "http://localhost:3333"
strictSSL: false,
proxy: "http://localhost:3333",
url: "https://mail.google.com"
})
}),
request({
strictSSL: false
proxy: "http://localhost:3333"
strictSSL: false,
proxy: "http://localhost:3333",
url: "https://google.com"
})
])
]);
});
it "can call the httpsDirectly without a proxy", ->
request({
strictSSL: false
url: "https://localhost:8443"
})
it("can call the httpsDirectly without a proxy", () => request({
strictSSL: false,
url: "https://localhost:8443"
}));
it "can boot the httpsServer", ->
request({
strictSSL: false
url: "https://localhost:8443/"
it("can boot the httpsServer", () => request({
strictSSL: false,
url: "https://localhost:8443/",
proxy: "http://localhost:3333"
})
.then(html => expect(html).to.include("https server")));
it("yields the onRequest callback", () => request({
strictSSL: false,
url: "https://localhost:8443/replace",
proxy: "http://localhost:3333"
})
.then(html => expect(html).to.include("replaced content")));
it("can pass directly through", () => //# this will fail due to dynamic cert
//# generation when strict ssl is true
request({
strictSSL: false,
url: "https://localhost:8444/replace",
proxy: "http://localhost:3333"
})
.then(html => expect(html).to.include("https server")));
it("retries 5 times", function() {
this.sandbox.spy(net, 'connect');
return request({
strictSSL: false,
url: "https://localhost:12344",
proxy: "http://localhost:3333"
})
.then (html) ->
expect(html).to.include("https server")
.then(function() {
throw new Error("should not reach");}).catch(() => expect(net.connect).to.have.callCount(5));
});
it "yields the onRequest callback", ->
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');
it "can pass directly through", ->
## this will fail due to dynamic cert
## generation when strict ssl is true
request({
strictSSL: false
url: "https://localhost:8444/replace"
proxy: "http://localhost:3333"
})
.then (html) ->
expect(html).to.include("https server")
it "retries 5 times", ->
@sandbox.spy(net, 'connect')
request({
strictSSL: false
url: "https://localhost:12344"
proxy: "http://localhost:3333"
})
.then ->
throw new Error("should not reach")
.catch ->
expect(net.connect).to.have.callCount(5)
it "closes outgoing connections when client disconnects", ->
@sandbox.spy(net, 'connect')
request({
strictSSL: false
url: "https://localhost:8444/replace"
proxy: "http://localhost:3333"
return request({
strictSSL: false,
url: "https://localhost:8444/replace",
proxy: "http://localhost:3333",
resolveWithFullResponse: true
})
.then (res) =>
## ensure client has disconnected
expect(res.socket.destroyed).to.be.true
## ensure the outgoing socket created for this connection was destroyed
socket = net.connect.getCalls()
.find (call) =>
call.args[0].port == "8444" && call.args[0].host == "localhost"
.returnValue
expect(socket.destroyed).to.be.true
.then(res => {
//# ensure client has disconnected
expect(res.socket.destroyed).to.be.true;
//# ensure the outgoing socket created for this connection was destroyed
const socket = net.connect.getCalls()
.find(call => {
return (call.args[0].port === "8444") && (call.args[0].host === "localhost");
}).returnValue;
return expect(socket.destroyed).to.be.true;
});
});
it "can boot the httpServer", ->
request({
strictSSL: false
url: "http://localhost:8080/"
it("can boot the httpServer", () => request({
strictSSL: false,
url: "http://localhost:8080/",
proxy: "http://localhost:3333"
})
.then(html => expect(html).to.include("http server")));
context("generating certificates", function() {
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"
});
});
});
//# https://github.com/cypress-io/cypress/issues/771
return it("generates certs and can proxy requests for HTTPS requests to IPs", function() {
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;
return expect(this.proxy._generateMissingCertificates).to.be.calledTwice;
});
});
});
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"));
.then (html) ->
expect(html).to.include("http server")
context "generating certificates", ->
it "reuses existing certificates", ->
request({
strictSSL: false
url: "https://localhost:8443/"
return request({
strictSSL: false,
url: "https://localhost:8443/",
proxy: "http://localhost:3333"
})
.then =>
proxy.reset()
});
});
}));
## force this to reject if its called
@sandbox.stub(@proxy, "_generateMissingCertificates").rejects(new Error("should not call"))
return context("with an upstream proxy", function() {
beforeEach(function() {
process.env.NO_PROXY = "";
process.env.HTTP_PROXY = (process.env.HTTPS_PROXY = "http://localhost:9001");
request({
strictSSL: false
url: "https://localhost:8443/"
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")
Promise.all([
httpsServer.start(8445),
@proxy._ca.removeAll()
])
.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').and.be.calledOnce
expect(@proxy._generateMissingCertificates).to.be.calledTwice
context "closing", ->
it "resets sslServers and can reopen", ->
request({
strictSSL: false
url: "https://localhost:8443/"
proxy: "http://localhost:3333"
})
.then =>
proxy.stop()
.then =>
proxy.start(3333)
.then =>
## force this to reject if its called
@sandbox.stub(@proxy, "_generateMissingCertificates").rejects(new Error("should not call"))
request({
strictSSL: false
url: "https://localhost:8443/"
proxy: "http://localhost:3333"
})
context "with an upstream proxy", ->
beforeEach ->
process.env.NO_PROXY = ""
process.env.HTTP_PROXY = process.env.HTTPS_PROXY = "http://localhost:9001"
@upstream = new DebugProxy({
this.upstream = new DebugProxy({
keepRequests: true
})
});
@upstream.start(9001)
return this.upstream.start(9001);
});
it "passes a request to an https server through the upstream", ->
@upstream._onConnect = (domain, port) ->
expect(domain).to.eq('localhost')
expect(port).to.eq('8444')
return true
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;
};
request({
strictSSL: false
url: "https://localhost:8444/"
return request({
strictSSL: false,
url: "https://localhost:8444/",
proxy: "http://localhost:3333"
}).then (res) =>
expect(res).to.contain("https server")
}).then(res => {
return expect(res).to.contain("https server");
});
});
it "uses HTTP basic auth when provided", ->
@upstream.setAuth({
username: 'foo'
it("uses HTTP basic auth when provided", function() {
this.upstream.setAuth({
username: 'foo',
password: 'bar'
})
});
@upstream._onConnect = (domain, port) ->
expect(domain).to.eq('localhost')
expect(port).to.eq('8444')
return true
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"
process.env.HTTP_PROXY = (process.env.HTTPS_PROXY = "http://foo:bar@localhost:9001");
request({
strictSSL: false
url: "https://localhost:8444/"
return request({
strictSSL: false,
url: "https://localhost:8444/",
proxy: "http://localhost:3333"
}).then (res) =>
expect(res).to.contain("https server")
}).then(res => {
return expect(res).to.contain("https server");
});
});
it "closes outgoing connections when client disconnects", ->
@sandbox.spy(net, 'connect')
it("closes outgoing connections when client disconnects", function() {
this.sandbox.spy(net, 'connect');
request({
strictSSL: false
url: "https://localhost:8444/replace"
proxy: "http://localhost:3333"
resolveWithFullResponse: true
return request({
strictSSL: false,
url: "https://localhost:8444/replace",
proxy: "http://localhost:3333",
resolveWithFullResponse: true,
forever: false
})
.then (res) =>
## ensure client has disconnected
expect(res.socket.destroyed).to.be.true
.then(res => {
//# ensure client has disconnected
expect(res.socket.destroyed).to.be.true;
## ensure the outgoing socket created for this connection was destroyed
socket = net.connect.getCalls()
.find (call) =>
call.args[0].port == 9001 && call.args[0].host == "localhost"
.returnValue
//# ensure the outgoing socket created for this connection was destroyed
const socket = net.connect.getCalls()
.find(call => {
return (call.args[0].port === 9001) && (call.args[0].host === "localhost");
}).returnValue;
new Promise (resolve) ->
socket.on 'close', =>
expect(socket.destroyed).to.be.true
resolve()
return new Promise(resolve => socket.on('close', () => {
expect(socket.destroyed).to.be.true;
return resolve();
}));
});
});
## https://github.com/cypress-io/cypress/issues/4257
it "passes through to SNI when it is intercepted and not through proxy", ->
createSocket = @sandbox.stub(network.connect, 'createRetryingSocket').callsArgWith(1, new Error('stub'))
createProxyConn = @sandbox.spy(network.agent.httpsAgent, 'createUpstreamProxyConnection')
//# 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');
request({
strictSSL: false
url: "https://localhost:8443"
proxy: "http://localhost:3333"
resolveWithFullResponse: true
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: @proxy._sniPort
.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;
return expect(createSocket).to.be.calledWith({
port: this.proxy._sniPort,
host: 'localhost'
})
});
});
});
afterEach ->
@upstream.stop()
delete process.env.HTTP_PROXY
delete process.env.HTTPS_PROXY
delete process.env.NO_PROXY
return afterEach(function() {
this.upstream.stop();
delete process.env.HTTP_PROXY;
delete process.env.HTTPS_PROXY;
return delete process.env.NO_PROXY;
});
});
});
+21 -14
View File
@@ -1,19 +1,26 @@
chai = require("chai")
sinon = require("sinon")
Promise = require("bluebird")
sinonChai = require("sinon-chai")
sinonPromise = require("sinon-as-promised")(Promise)
/*
* decaffeinate suggestions:
* DS102: Remove unnecessary code created because of implicit returns
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
*/
const chai = require("chai");
const sinon = require("sinon");
const Promise = require("bluebird");
const sinonChai = require("sinon-chai");
const sinonPromise = require("sinon-as-promised")(Promise);
global.request = require("request-promise")
global.sinon = sinon
global.supertest = require("supertest")
global.request = require("request-promise");
global.sinon = sinon;
global.supertest = require("supertest");
chai.use(sinonChai)
chai.use(sinonChai);
global.expect = chai.expect
global.expect = chai.expect;
beforeEach ->
@sandbox = sinon.sandbox.create()
beforeEach(function() {
return this.sandbox = sinon.sandbox.create();
});
afterEach ->
@sandbox.restore()
afterEach(function() {
return this.sandbox.restore();
});
+85 -57
View File
@@ -1,76 +1,104 @@
require("../spec_helper")
/*
* decaffeinate suggestions:
* DS102: Remove unnecessary code created because of implicit returns
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
*/
require("../spec_helper");
fs = require("fs-extra")
path = require("path")
Promise = require("bluebird")
CA = require("../../lib/ca")
let fs = require("fs-extra");
const path = require("path");
const Promise = require("bluebird");
const CA = require("../../lib/ca");
fs = Promise.promisifyAll(fs)
fs = Promise.promisifyAll(fs);
describe "lib/ca", ->
beforeEach ->
@timeout(5000)
describe("lib/ca", function() {
beforeEach(function() {
this.timeout(5000);
@dir = path.join(process.cwd(), "tmp")
this.dir = path.join(process.cwd(), "tmp");
fs.ensureDirAsync(@dir)
.then =>
CA.create(@dir)
.then (@ca) =>
return fs.ensureDirAsync(this.dir)
.then(() => {
return CA.create(this.dir);
}).then(ca => {
this.ca = ca;
});
});
afterEach ->
fs.removeAsync(@dir)
afterEach(function() {
return fs.removeAsync(this.dir);
});
context "#generateServerCertificateKeys", ->
it "generates certs for each host", ->
@ca.generateServerCertificateKeys("www.cypress.io")
.spread (certPem, keyPrivatePem) ->
expect(certPem).to.include("-----BEGIN CERTIFICATE-----")
expect(keyPrivatePem).to.include("-----BEGIN RSA PRIVATE KEY-----")
context("#generateServerCertificateKeys", () => it("generates certs for each host", function() {
return this.ca.generateServerCertificateKeys("www.cypress.io")
.spread(function(certPem, keyPrivatePem) {
expect(certPem).to.include("-----BEGIN CERTIFICATE-----");
return expect(keyPrivatePem).to.include("-----BEGIN RSA PRIVATE KEY-----");
});
}));
context ".create", ->
it "returns a new CA instance", ->
expect(@ca).to.be.an.instanceof(CA)
return context(".create", function() {
it("returns a new CA instance", function() {
return expect(this.ca).to.be.an.instanceof(CA);
});
it "creates certs + keys dir", ->
Promise.join(
fs.statAsync(path.join(@dir, "certs"))
fs.statAsync(path.join(@dir, "keys"))
)
it("creates certs + keys dir", function() {
return Promise.join(
fs.statAsync(path.join(this.dir, "certs")),
fs.statAsync(path.join(this.dir, "keys"))
);
});
it "writes certs/ca.pem", ->
fs.statAsync(path.join(@dir, "certs", "ca.pem"))
it("writes certs/ca.pem", function() {
return fs.statAsync(path.join(this.dir, "certs", "ca.pem"));
});
it "writes keys/ca.private.key", ->
fs.statAsync(path.join(@dir, "keys", "ca.private.key"))
it("writes keys/ca.private.key", function() {
return fs.statAsync(path.join(this.dir, "keys", "ca.private.key"));
});
it "writes keys/ca.public.key", ->
fs.statAsync(path.join(@dir, "keys", "ca.public.key"))
it("writes keys/ca.public.key", function() {
return fs.statAsync(path.join(this.dir, "keys", "ca.public.key"));
});
it "sets ca.CAcert", ->
expect(@ca.CAcert).to.be.an("object")
it("sets ca.CAcert", function() {
return expect(this.ca.CAcert).to.be.an("object");
});
it "sets ca.CAkeys", ->
expect(@ca.CAkeys).to.be.an("object")
expect(@ca.CAkeys).to.have.a.property("privateKey")
expect(@ca.CAkeys).to.have.a.property("publicKey")
it("sets ca.CAkeys", function() {
expect(this.ca.CAkeys).to.be.an("object");
expect(this.ca.CAkeys).to.have.a.property("privateKey");
return expect(this.ca.CAkeys).to.have.a.property("publicKey");
});
describe "existing CA folder", ->
beforeEach ->
@sandbox.spy(CA.prototype, "loadCA")
@sandbox.spy(CA.prototype, "generateCA")
return describe("existing CA folder", function() {
beforeEach(function() {
this.sandbox.spy(CA.prototype, "loadCA");
this.sandbox.spy(CA.prototype, "generateCA");
CA.create(@dir)
.then (@ca2) =>
return CA.create(this.dir)
.then(ca2 => {
this.ca2 = ca2;
});
});
it "calls loadCA and not generateCA", ->
expect(CA.prototype.loadCA).to.be.calledOnce
expect(CA.prototype.generateCA).not.to.be.called
it("calls loadCA and not generateCA", function() {
expect(CA.prototype.loadCA).to.be.calledOnce;
return expect(CA.prototype.generateCA).not.to.be.called;
});
it "sets ca.CAcert", ->
expect(@ca2.CAcert).to.be.an("object")
it("sets ca.CAcert", function() {
return expect(this.ca2.CAcert).to.be.an("object");
});
it "sets ca.CAkeys", ->
expect(@ca2.CAkeys).to.be.an("object")
expect(@ca2.CAkeys).to.have.a.property("privateKey")
expect(@ca2.CAkeys).to.have.a.property("publicKey")
return it("sets ca.CAkeys", function() {
expect(this.ca2.CAkeys).to.be.an("object");
expect(this.ca2.CAkeys).to.have.a.property("privateKey");
return expect(this.ca2.CAkeys).to.have.a.property("publicKey");
});
});
});
});
+100 -77
View File
@@ -1,110 +1,133 @@
require("../spec_helper")
/*
* decaffeinate suggestions:
* DS102: Remove unnecessary code created because of implicit returns
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
*/
require("../spec_helper");
EE = require("events")
Promise = require("bluebird")
proxy = require("../helpers/proxy")
Server = require("../../lib/server")
const EE = require("events");
const Promise = require("bluebird");
const proxy = require("../helpers/proxy");
const Server = require("../../lib/server");
describe "lib/server", ->
beforeEach ->
@setup = (options = {}) =>
@ca = {}
@port = 12345
describe("lib/server", function() {
beforeEach(function() {
return this.setup = (options = {}) => {
this.ca = {};
this.port = 12345;
Server.create(@ca, @port, options)
return Server.create(this.ca, this.port, options);
};
});
afterEach ->
delete process.env.HTTPS_PROXY
delete process.env.NO_PROXY
afterEach(function() {
delete process.env.HTTPS_PROXY;
return delete process.env.NO_PROXY;
});
context "#listen", ->
it "calls options.onUpgrade with req, socket head", ->
onUpgrade = @sandbox.stub()
return context("#listen", function() {
it("calls options.onUpgrade with req, socket head", function() {
const onUpgrade = this.sandbox.stub();
@setup({onUpgrade: onUpgrade})
.then (srv) ->
srv._sniServer.emit("upgrade", 1, 2, 3)
return this.setup({onUpgrade})
.then(function(srv) {
srv._sniServer.emit("upgrade", 1, 2, 3);
expect(onUpgrade).to.be.calledWith(1,2,3)
return expect(onUpgrade).to.be.calledWith(1,2,3);
});
});
it "calls options.onRequest with req, res", ->
onRequest = @sandbox.stub()
req = {url: "https://www.foobar.com", headers: {host: "www.foobar.com"}}
res = {}
it("calls options.onRequest with req, res", function() {
const onRequest = this.sandbox.stub();
const req = {url: "https://www.foobar.com", headers: {host: "www.foobar.com"}};
const res = {};
@setup({onRequest: onRequest})
.then (srv) ->
srv._sniServer.emit("request", req, res)
return this.setup({onRequest})
.then(function(srv) {
srv._sniServer.emit("request", req, res);
expect(onRequest).to.be.calledWith(req, res)
return expect(onRequest).to.be.calledWith(req, res);
});
});
it "calls options.onError with err and port and destroys the client socket", (done) ->
socket = new EE()
socket.destroy = @sandbox.stub()
head = {}
it("calls options.onError with err and port and destroys the client socket", function(done) {
const socket = new EE();
socket.destroy = this.sandbox.stub();
const head = {};
onError = (err, socket2, head2, port) ->
expect(err.message).to.eq("connect ECONNREFUSED 127.0.0.1:8444")
const onError = function(err, socket2, head2, port) {
expect(err.message).to.eq("connect ECONNREFUSED 127.0.0.1:8444");
expect(socket).to.eq(socket2)
expect(head).to.eq(head2)
expect(port).to.eq("8444")
expect(socket).to.eq(socket2);
expect(head).to.eq(head2);
expect(port).to.eq("8444");
expect(socket.destroy).to.be.calledOnce
expect(socket.destroy).to.be.calledOnce;
done()
return done();
};
@setup({ onError })
.then (srv) ->
conn = srv._makeDirectConnection({url: "localhost:8444"}, socket, head)
this.setup({ onError })
.then(function(srv) {
let conn;
return conn = srv._makeDirectConnection({url: "localhost:8444"}, socket, head);
});
return
});
## https://github.com/cypress-io/cypress/issues/3250
it "does not crash when an erroneous URL is provided, just destroys socket", (done) ->
socket = new EE()
socket.destroy = @sandbox.stub()
head = {}
//# https://github.com/cypress-io/cypress/issues/3250
it("does not crash when an erroneous URL is provided, just destroys socket", function(done) {
const socket = new EE();
socket.destroy = this.sandbox.stub();
const head = {};
onError = (err, socket2, head2, port) ->
expect(err.message).to.eq("connect ECONNREFUSED 127.0.0.1:443")
const onError = function(err, socket2, head2, port) {
expect(err.message).to.eq("connect ECONNREFUSED 127.0.0.1:443");
expect(socket).to.eq(socket2)
expect(head).to.eq(head2)
expect(port).to.eq("443")
expect(socket).to.eq(socket2);
expect(head).to.eq(head2);
expect(port).to.eq("443");
expect(socket.destroy).to.be.calledOnce
expect(socket.destroy).to.be.calledOnce;
done()
return done();
};
@setup({ onError })
.then (srv) ->
conn = srv._makeDirectConnection({url: "%7Balgolia_application_id%7D-dsn.algolia.net:443"}, socket, head)
this.setup({ onError })
.then(function(srv) {
let conn;
return conn = srv._makeDirectConnection({url: "%7Balgolia_application_id%7D-dsn.algolia.net:443"}, socket, head);
});
return
});
it "with proxied connection calls options.onError with err and port and destroys the client socket", (done) ->
socket = new EE()
socket.destroy = @sandbox.stub()
head = {}
return it("with proxied connection calls options.onError with err and port and destroys the client socket", function(done) {
const socket = new EE();
socket.destroy = this.sandbox.stub();
const head = {};
onError = (err, socket2, head2, port) ->
expect(err.message).to.eq("A connection to the upstream proxy could not be established: connect ECONNREFUSED 127.0.0.1:8444")
const onError = function(err, socket2, head2, port) {
expect(err.message).to.eq("A connection to the upstream proxy could not be established: connect ECONNREFUSED 127.0.0.1:8444");
expect(socket).to.eq(socket2)
expect(head).to.eq(head2)
expect(port).to.eq("11111")
expect(socket).to.eq(socket2);
expect(head).to.eq(head2);
expect(port).to.eq("11111");
expect(socket.destroy).to.be.calledOnce
expect(socket.destroy).to.be.calledOnce;
done()
return done();
};
process.env.HTTPS_PROXY = 'http://localhost:8444'
process.env.NO_PROXY = ''
process.env.HTTPS_PROXY = 'http://localhost:8444';
process.env.NO_PROXY = '';
@setup({ onError })
.then (srv) ->
conn = srv._makeDirectConnection({url: "should-not-reach.invalid:11111"}, socket, head)
this.setup({ onError })
.then(function(srv) {
let conn;
return conn = srv._makeDirectConnection({url: "should-not-reach.invalid:11111"}, socket, head);
});
return
});
});
});