From 7fc541027f6cf86d2f9246cf47666834fe9fb66e Mon Sep 17 00:00:00 2001 From: Ricky Romero Date: Fri, 16 Mar 2018 13:26:12 -0700 Subject: [PATCH 1/7] Support SSL proxying --- cli.js | 2 +- index.js | 53 ++++++++++++++++++++++++++++++++++++++++++++++-- ssl/snakeoil.crt | 18 ++++++++++++++++ ssl/snakeoil.key | 28 +++++++++++++++++++++++++ 4 files changed, 98 insertions(+), 3 deletions(-) create mode 100644 ssl/snakeoil.crt create mode 100644 ssl/snakeoil.key diff --git a/cli.js b/cli.js index 15bb87d..468cbdd 100755 --- a/cli.js +++ b/cli.js @@ -71,4 +71,4 @@ serverReplay(har, { }); console.log("Listening at http://localhost:" + argv.port); -console.log("Try " + har.log.entries[0].request.url.replace(/^https/, "http")); +console.log("Try " + har.log.entries[0].request.url); diff --git a/index.js b/index.js index a758c69..6b18ebe 100755 --- a/index.js +++ b/index.js @@ -15,7 +15,9 @@ */ var _fs = require("fs"); +var net = require("net"); var http = require("http"); +var https = require("https"); var URL = require("url"); var PATH = require("path"); var mime = require("mime"); @@ -23,9 +25,56 @@ var heuristic = require("./heuristic"); exports = module.exports = serverReplay; function serverReplay(har, options) { - var server = http.createServer(makeRequestListener(har.log.entries, options)); + var fs = options.fs || _fs; + if (!options.ssl) { + options.ssl = { + key: "./ssl/snakeoil.key", + cert: "./ssl/snakeoil.crt" + }; + } + + options.ssl.key = fs.readFileSync(options.ssl.key); + options.ssl.cert = fs.readFileSync(options.ssl.cert); + + var rl = makeRequestListener(har.log.entries, options) + var internalProxy = net.createServer(chooseProtocol.bind(this, options)); + var httpServer = http.createServer(rl); + var httpsServer = https.createServer(options.ssl, rl); + + internalProxy.listen(options.port); + httpServer.listen(options.port + 1); + httpsServer.listen(options.port + 2); +} + +function chooseProtocol(options, connection) { + connection.once("data", function (buf) { + var intent = buf.toString().split("\r\n")[0]; + var destPort = options.port + 1; + + if (/^CONNECT .+?:443 HTTP\/\d(?:\.\d)?$/.test(intent)) { + destPort += 1; + connection.write( + "HTTP/1.1 200 Connection established\r\n" + + "Connection: keep-alive\r\n" + + "Via: HTTP/1.1 server-replay\r\n" + + "\r\n" + ); + + connection.once("data", function (buf2) { + bridgeConnection(buf2, connection, destPort) + }); + } else { + bridgeConnection(buf, connection, destPort) + } + }); + +} - server.listen(options.port); +function bridgeConnection(buf, connection, destPort) { + var proxy = net.createConnection(destPort, function () { + proxy.write(buf); + connection.pipe(proxy).pipe(connection); + }); } // Export for testing diff --git a/ssl/snakeoil.crt b/ssl/snakeoil.crt new file mode 100644 index 0000000..a49ebcc --- /dev/null +++ b/ssl/snakeoil.crt @@ -0,0 +1,18 @@ +-----BEGIN CERTIFICATE----- +MIIC5TCCAc2gAwIBAgIJAOA+uqpK8B/PMA0GCSqGSIb3DQEBCwUAMBQxEjAQBgNV +BAMMCWxvY2FsaG9zdDAeFw0xODAzMTUyMDQ0MjNaFw0xODA0MTQyMDQ0MjNaMBQx +EjAQBgNVBAMMCWxvY2FsaG9zdDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoC +ggEBAOYqNJAxQYMJIcJ69zyROZpZfmArNyd+UZxuYPOEJbqjwRHDVOhErz6tm55s +THVaXuGJ7qLsKFCTxW5X4gtYKRlLG5rcbXPQUUDhIg/2cwNe1t4S/Ri9QjaDb7iZ +H7Rcxb4WNWpek2VFRV1010GkLvPkDJhauIReZr2SOCTlaR6zSKV8PcebsXRgPSl0 +Myz4fSYuN3jXH/uYmRySN4O7BV0nLf8Wpvd0sH7X0g5/3OE/kBZUljJSQ7SVnGcW +o+J7toa6K9kFhMf7kZoLk9JDFyew9Bq/0jf/Mo8aiZ3ujdXL/tREbzpMKfBFfJj2 +VqLzYKEtmdD2SaMpCWwt/g5pOokCAwEAAaM6MDgwFAYDVR0RBA0wC4IJbG9jYWxo +b3N0MAsGA1UdDwQEAwIHgDATBgNVHSUEDDAKBggrBgEFBQcDATANBgkqhkiG9w0B +AQsFAAOCAQEAm9MA4VmVIh4RzbQ1Pr+YD/SqnVmSajtT1GjuMIfevSX5XYxFcMfi +or8a5EyZnOfM/NfiEkOVgwSkQwbNg0/yUK1BequbP0NNhQNWJzrBcpNSqBEqgdUU +97u/q06dSonNBMw8OIzZ8aWw+SlwoUSpAxvWgiHe5PH49WfR402MJxPKJqu0mOqU +EwyND9Y4iLvL8Pqu4oSpfNZVwq4RfUcJF1h9lYfVK+dB/+sgsgyLIeSs/DKZhYPz +ib8scXqVQiJGzlQlVHny2ezr8rtQs54DJIbOnEEXCPaN14aPkp7pJKwl/0cm/AGf +9CIvVzw45kVdVoG6zjumFG+kd2AyecZmfQ== +-----END CERTIFICATE----- diff --git a/ssl/snakeoil.key b/ssl/snakeoil.key new file mode 100644 index 0000000..3a453ec --- /dev/null +++ b/ssl/snakeoil.key @@ -0,0 +1,28 @@ +-----BEGIN PRIVATE KEY----- +MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQDmKjSQMUGDCSHC +evc8kTmaWX5gKzcnflGcbmDzhCW6o8ERw1ToRK8+rZuebEx1Wl7hie6i7ChQk8Vu +V+ILWCkZSxua3G1z0FFA4SIP9nMDXtbeEv0YvUI2g2+4mR+0XMW+FjVqXpNlRUVd +dNdBpC7z5AyYWriEXma9kjgk5Wkes0ilfD3Hm7F0YD0pdDMs+H0mLjd41x/7mJkc +kjeDuwVdJy3/Fqb3dLB+19IOf9zhP5AWVJYyUkO0lZxnFqPie7aGuivZBYTH+5Ga +C5PSQxcnsPQav9I3/zKPGomd7o3Vy/7URG86TCnwRXyY9lai82ChLZnQ9kmjKQls +Lf4OaTqJAgMBAAECggEAGOyvHoJG/uKpRj88sNFlNILGfbGQWnWCbvdBBn3j/A8p +pDvL4Q83DwmL1Z8StI6hwbjHH9uFDhzCf42CzAmzAasxhRajv6vqcKUwpBvjHpVR +nWDfCaPNHMwk+A+U8Fovi8Mp66fsPEZBGbrCaLhX4U9r0b/ZRXRXmeXQsKYrOQiq +mY5gfuQCBBVECDnktrYV8YV3ULhqiuZNmz9xrpeLf9H19QRwbbcHkRZLbigP7kaU +8k800rXJVEz0fsOQ+PTOkJSiD7W50BuxWeIStUYaZmzBKbNMvZKeUIzLd1rFnxbj +sT3JNoZPfKSXXwCsyl58apzGROxf+4TXUAQUOk/1kQKBgQD4uScU0Wj2P6xliLUi +MGheOMwSiIm50deZdZQL19JpQE9R8OGL7wwwXOdc2+Mp01xt4cV2qA25T8diQa9w +O20VqDynMt7vbDC46Y0aeOegYpCINCuS3rfqVY8/Yhg/2jArfaZipP1QU05Ah7sG +0gsheLb0PKwJ8iuVUErw5dvpXQKBgQDs5g6aJQl4AFEAg+2HWggbeoRGHii0bVJQ +EnLBp+WBHjKD+DCCJ+sRyRBczAr3k/ueQmexMz24vhvJ1E/feo2CaSRvLI+kHinz +XJOEvDNBgmN+UR4rVWflGBDTjYfZjH1jlXkPNIuO3+AvNFPiTRd43nlIxynumzWN +CiAl2nNHHQKBgQCTWNTvP1PoNjaCfCealoTt9MXo4Nx+qfMI5aAMGBJ96exTxdlI +lhhpelBSMa309FMYgZ0Cu3JN6xZafkFZwsrP/rfX8Yoi2rxOf4XpPeEyodGv7wA1 +ZR4dhAx15z4obbEFws1UORwcfw2nqwFAfCS98o6oSF0/EymArm2HIxVRvQKBgQCW +5oySn9kCOaFfZKofN7hGWKp9R6TCGYj/PGEg/mPw9V1UNvofTnIsaBkmI0sxHXCA +BOisNWmxjleBHt6qChSt52+v6YCuGBC81lGZkZBMwFPEGMPQ8pw1kDjXqSXJ6/XL +Q2FT0DK9ldnl970fP+AdvAkh1MvfE7ru1m5X7mjT+QKBgDbHJjzyA5IBf/+IDQM8 +UfV9z2zReicMuUIAIubhXKlK5Oj69Nu6ziLjpt2sMSFT1OomiRWCpHLLJr1EI49L +XZMsx/Szt7/kCYwQHja2N6RDOmxdZemR+alxAo0cIyG5I6gQLURca4dhSMaBBGMQ +TJaTwEX5zeRz4i04ay8dIowT +-----END PRIVATE KEY----- From 8beb184688c54ad7d45e945cf6507b4886e83df7 Mon Sep 17 00:00:00 2001 From: Ricky Romero Date: Fri, 16 Mar 2018 13:27:55 -0700 Subject: [PATCH 2/7] Match semicolon style --- index.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/index.js b/index.js index 6b18ebe..07de263 100755 --- a/index.js +++ b/index.js @@ -36,7 +36,7 @@ function serverReplay(har, options) { options.ssl.key = fs.readFileSync(options.ssl.key); options.ssl.cert = fs.readFileSync(options.ssl.cert); - var rl = makeRequestListener(har.log.entries, options) + var rl = makeRequestListener(har.log.entries, options); var internalProxy = net.createServer(chooseProtocol.bind(this, options)); var httpServer = http.createServer(rl); var httpsServer = https.createServer(options.ssl, rl); @@ -61,10 +61,10 @@ function chooseProtocol(options, connection) { ); connection.once("data", function (buf2) { - bridgeConnection(buf2, connection, destPort) + bridgeConnection(buf2, connection, destPort); }); } else { - bridgeConnection(buf, connection, destPort) + bridgeConnection(buf, connection, destPort); } }); From fd83242c52f6f12288eb9ab189f9278a5c070fe8 Mon Sep 17 00:00:00 2001 From: Ricky Romero Date: Fri, 16 Mar 2018 13:30:03 -0700 Subject: [PATCH 3/7] Readme comment no longer accurate --- README.md | 2 +- spec/parse-config-spec.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index d23453b..6ea5c4d 100644 --- a/README.md +++ b/README.md @@ -59,7 +59,7 @@ Full example: "match": {"var": "entry.request.parsedUrl.query.callback"}, "replace": {"var": "request.parsedUrl.query.callback"} }, - // Proxy only works over http + // Replace HTTPS with HTTP in page content {"match": "https", "replace": "http"} ] } diff --git a/spec/parse-config-spec.js b/spec/parse-config-spec.js index be838e1..b04270f 100644 --- a/spec/parse-config-spec.js +++ b/spec/parse-config-spec.js @@ -33,7 +33,7 @@ describe("readme", function () { "match": {"var": "entry.request.parsedUrl.query.callback"}, "replace": {"var": "request.parsedUrl.query.callback"} }, - // Proxy only works over http + // Replace HTTPS with HTTP in page content {"match": "https", "replace": "http"} ] })); From 4002ca43468bd90fbbe24f2c50a0dc4a58101a3c Mon Sep 17 00:00:00 2001 From: Ricky Romero Date: Fri, 16 Mar 2018 13:43:03 -0700 Subject: [PATCH 4/7] Make snakeoil SSL files relative to node module --- index.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/index.js b/index.js index 07de263..97b878a 100755 --- a/index.js +++ b/index.js @@ -28,8 +28,8 @@ function serverReplay(har, options) { var fs = options.fs || _fs; if (!options.ssl) { options.ssl = { - key: "./ssl/snakeoil.key", - cert: "./ssl/snakeoil.crt" + key: __dirname + "/ssl/snakeoil.key", + cert: __dirname + "/ssl/snakeoil.crt" }; } From c4d39500d14e223d7c125d727d9a0dcefc2693db Mon Sep 17 00:00:00 2001 From: Ricky Romero Date: Fri, 16 Mar 2018 14:00:10 -0700 Subject: [PATCH 5/7] Gracefully handle connection reset errors on the proxy --- index.js | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/index.js b/index.js index 97b878a..83c73eb 100755 --- a/index.js +++ b/index.js @@ -47,6 +47,7 @@ function serverReplay(har, options) { } function chooseProtocol(options, connection) { + connection.on("error", handleProxyError); connection.once("data", function (buf) { var intent = buf.toString().split("\r\n")[0]; var destPort = options.port + 1; @@ -67,7 +68,11 @@ function chooseProtocol(options, connection) { bridgeConnection(buf, connection, destPort); } }); +} +function handleProxyError(err) { + console.warn("An error occurred while proxying:"); + console.warn(err.stack); } function bridgeConnection(buf, connection, destPort) { From 03f1726b9e906b9466de10f3eaafae27bf8d6960 Mon Sep 17 00:00:00 2001 From: Ricky Romero Date: Mon, 19 Mar 2018 04:14:54 -0700 Subject: [PATCH 6/7] Better Windows compatibility, more descriptive variable naming --- index.js | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/index.js b/index.js index 83c73eb..88154ba 100755 --- a/index.js +++ b/index.js @@ -28,18 +28,18 @@ function serverReplay(har, options) { var fs = options.fs || _fs; if (!options.ssl) { options.ssl = { - key: __dirname + "/ssl/snakeoil.key", - cert: __dirname + "/ssl/snakeoil.crt" + key: PATH.join(__dirname, "ssl", "snakeoil.key"), + cert: PATH.join(__dirname, "ssl", "snakeoil.crt") }; } options.ssl.key = fs.readFileSync(options.ssl.key); options.ssl.cert = fs.readFileSync(options.ssl.cert); - var rl = makeRequestListener(har.log.entries, options); + var requestListener = makeRequestListener(har.log.entries, options); var internalProxy = net.createServer(chooseProtocol.bind(this, options)); - var httpServer = http.createServer(rl); - var httpsServer = https.createServer(options.ssl, rl); + var httpServer = http.createServer(requestListener); + var httpsServer = https.createServer(options.ssl, requestListener); internalProxy.listen(options.port); httpServer.listen(options.port + 1); From 16c27f68545896430ffa357c6ae103e36aba80df Mon Sep 17 00:00:00 2001 From: Ricky Romero Date: Fri, 6 Apr 2018 09:46:30 -0700 Subject: [PATCH 7/7] Randomly assign internal proxying ports if none specified --- index.js | 38 +++++++++++++++++++++++++++++++++----- 1 file changed, 33 insertions(+), 5 deletions(-) diff --git a/index.js b/index.js index 88154ba..3af03de 100755 --- a/index.js +++ b/index.js @@ -33,27 +33,55 @@ function serverReplay(har, options) { }; } + if (!options.httpPort) { + options.httpPort = 0; + } + + if (!options.httpsPort) { + options.httpsPort = 0; + } + options.ssl.key = fs.readFileSync(options.ssl.key); options.ssl.cert = fs.readFileSync(options.ssl.cert); var requestListener = makeRequestListener(har.log.entries, options); - var internalProxy = net.createServer(chooseProtocol.bind(this, options)); var httpServer = http.createServer(requestListener); var httpsServer = https.createServer(options.ssl, requestListener); + var proxyStarted = false; + + httpServer.listen(options.httpPort); + httpsServer.listen(options.httpsPort); + + httpServer.once("listening", function () { + options.httpPort = httpServer.address().port; + if (options.httpPort && options.httpsPort && !proxyStarted) { + startProxy(options); + proxyStarted = true; + } + }); + httpsServer.once("listening", function () { + options.httpsPort = httpsServer.address().port; + if (options.httpPort && options.httpsPort && !proxyStarted) { + startProxy(options); + proxyStarted = true; + } + }); +} + +function startProxy(options) { + var internalProxy = net.createServer(chooseProtocol.bind(this, options)); internalProxy.listen(options.port); - httpServer.listen(options.port + 1); - httpsServer.listen(options.port + 2); } function chooseProtocol(options, connection) { connection.on("error", handleProxyError); connection.once("data", function (buf) { var intent = buf.toString().split("\r\n")[0]; - var destPort = options.port + 1; + var destPort = options.httpPort; if (/^CONNECT .+?:443 HTTP\/\d(?:\.\d)?$/.test(intent)) { - destPort += 1; + destPort = options.httpsPort; connection.write( "HTTP/1.1 200 Connection established\r\n" + "Connection: keep-alive\r\n" +