diff --git a/lib/replay/index.coffee b/lib/replay/index.coffee index 6756039..608a7f4 100644 --- a/lib/replay/index.coffee +++ b/lib/replay/index.coffee @@ -1,5 +1,6 @@ DNS = require("dns") HTTP = require("http") +HTTPS = require("https") ProxyRequest = require("./proxy") Replay = require("./replay") @@ -20,6 +21,12 @@ HTTP.request = (options, callback)-> return request +# Route HTTPS requests +HTTPS.request = (options, callback)-> + options.protocol = "https:" + return HTTP.request(options, callback) + + # Redirect HTTP requests to 127.0.0.1 for all hosts defined as localhost original_lookup = DNS.lookup DNS.lookup = (domain, family, callback)-> diff --git a/lib/replay/pass_through.coffee b/lib/replay/pass_through.coffee index 848881f..e7c684b 100644 --- a/lib/replay/pass_through.coffee +++ b/lib/replay/pass_through.coffee @@ -1,7 +1,9 @@ HTTP = require("http") +HTTPS = require("https") # Capture original HTTP request. PassThrough proxy uses that. -httpRequest = HTTP.request +httpRequest = HTTP.request +httpsRequest = HTTPS.request passThrough = (allow)-> if arguments.length == 0 @@ -14,12 +16,17 @@ passThrough = (allow)-> return (request, callback)-> if allow(request) options = + protocol: request.url.protocol hostname: request.url.hostname port: request.url.port path: request.url.path method: request.method headers: request.headers - http = httpRequest(options) + + if request.url.protocol == "https:" + http = httpsRequest(options) + else + http = httpRequest(options) http.on "error", (error)-> callback error http.on "response", (response)-> diff --git a/lib/replay/proxy.coffee b/lib/replay/proxy.coffee index b3ba836..0946684 100644 --- a/lib/replay/proxy.coffee +++ b/lib/replay/proxy.coffee @@ -34,7 +34,7 @@ class ProxyRequest extends HTTP.ClientRequest constructor: (options = {}, @proxy)-> @method = (options.method || "GET").toUpperCase() [host, port] = (options.host || options.hostname).split(":") - @url = URL.parse("http://#{host || "localhost"}:#{options.port || port || 80}#{options.path || "/"}") + @url = URL.parse("#{options.protocol || "http:"}//#{host || "localhost"}:#{options.port || port || 80}#{options.path || "/"}") @headers = {} if options.headers for n,v of options.headers diff --git a/package.json b/package.json index 0e3dd16..0a357ce 100644 --- a/package.json +++ b/package.json @@ -19,7 +19,8 @@ }, "devDependencies": { "express": "~2.5.9", - "mocha": "~1.0.2" + "mocha": "~1.0.2", + "async": "~0.1.18" }, "repository": { "type": "git", diff --git a/test/fixtures/example.com:3443/minimal b/test/fixtures/example.com:3443/minimal new file mode 100644 index 0000000..72333bc --- /dev/null +++ b/test/fixtures/example.com:3443/minimal @@ -0,0 +1 @@ +GET /minimal diff --git a/test/helpers.coffee b/test/helpers.coffee index 9acfa97..4838935 100644 --- a/test/helpers.coffee +++ b/test/helpers.coffee @@ -2,11 +2,14 @@ # All requests using a hostname are routed to 127.0.0.1 # Port 3001 has a live server, see below for paths and responses # Port 3002 has no server, connections will be refused +# Port 3443 has a live https server -DNS = require("dns") +DNS = require("dns") Express = require("express") -Replay = require("../lib/replay") +Replay = require("../lib/replay") +File = require("fs") +Async = require("async") # Directory to load fixtures from. @@ -21,6 +24,7 @@ DNS.lookup = (domain, callback)-> else original_lookup domain, callback + # Serve pages from localhost. server = Express.createServer() server.use Express.bodyParser() @@ -35,9 +39,32 @@ server.get "/500", (req, res)-> res.send 500, "Boom!" +# SSL Server +ssl_server = Express.createServer( + key: File.readFileSync("#{__dirname}/ssl/privatekey.pem") + cert: File.readFileSync("#{__dirname}/ssl/certificate.pem") +) +ssl_server.use Express.bodyParser() +# Success page. +ssl_server.get "/", (req, res)-> + res.send "Success!" +# Not found +ssl_server.get "/404", (req, res)-> + res.send 404, "Not found" +# Internal error +ssl_server.get "/500", (req, res)-> + res.send 500, "Boom!" + + # Setup environment for running tests. setup = (callback)-> - server.listen 3001, callback + Async.parallel [ + (done)-> + server.listen 3001, done + (done)-> + ssl_server.listen 3443, done + ], callback + return if server._connected @@ -53,4 +80,5 @@ setup = (callback)-> exports.assert = require("assert") exports.setup = setup exports.HTTP = require("http") +exports.HTTPS = require("https") exports.Replay = Replay diff --git a/test/pass_through_test.coffee b/test/pass_through_test.coffee index 02baa6e..fe0d760 100644 --- a/test/pass_through_test.coffee +++ b/test/pass_through_test.coffee @@ -1,4 +1,4 @@ -{ assert, setup, HTTP, Replay } = require("./helpers") +{ assert, setup, HTTP, HTTPS, Replay } = require("./helpers") # First batch is testing requests that pass through to the server, no recording/replay. @@ -63,6 +63,33 @@ describe "Pass through", -> assert.deepEqual response.body, "Success!" + describe "ssl", -> + before -> + Replay.mode = "bloody" + + response = null + + before (done)-> + request = HTTPS.get(hostname: "pass-through", port: 3443, (_)-> + response = _ + response.body = "" + response.on "data", (chunk)-> + response.body += chunk + response.on "end", done + ) + request.on "error", done + + it "should return HTTP version", -> + assert.equal response.httpVersion, "1.1" + it "should return status code", -> + assert.equal response.statusCode, 200 + it "should return response headers", -> + assert.equal response.headers["content-type"], "text/html; charset=utf-8" + it "should return response trailers", -> + assert.deepEqual response.trailers, { } + it "should return response body", -> + assert.deepEqual response.body, "Success!" + # Send request to the live server on port 3001, but this time network connection disabled. describe "replay", -> before -> diff --git a/test/replay_test.coffee b/test/replay_test.coffee index 58f57a9..4574e0f 100644 --- a/test/replay_test.coffee +++ b/test/replay_test.coffee @@ -1,4 +1,4 @@ -{ assert, HTTP, Replay } = require("./helpers") +{ assert, HTTP, HTTPS, Replay } = require("./helpers") # Test replaying results from fixtures in spec/fixtures. @@ -50,6 +50,24 @@ describe "Replay", -> assert.deepEqual response.trailers, { } + describe "matching an https url", -> + response = null + + before (done)-> + Replay.mode = "replay" + + request = HTTPS.get(hostname: "example.com", port: 3443, path: "/minimal") + request.on "response", (_)-> + response = _ + done() + request.on "error", done + + it "should return HTTP version", -> + assert.equal response.httpVersion, "1.1" + it "should return status code", -> + assert.equal response.statusCode, 200 + + describe "matching a regexp", -> body = null diff --git a/test/ssl/certificate.pem b/test/ssl/certificate.pem new file mode 100644 index 0000000..76634cb --- /dev/null +++ b/test/ssl/certificate.pem @@ -0,0 +1,13 @@ +-----BEGIN CERTIFICATE----- +MIICATCCAWoCCQDwtkmH/OpeXzANBgkqhkiG9w0BAQUFADBFMQswCQYDVQQGEwJB +VTETMBEGA1UECBMKU29tZS1TdGF0ZTEhMB8GA1UEChMYSW50ZXJuZXQgV2lkZ2l0 +cyBQdHkgTHRkMB4XDTEyMDUwMjE2MzA0N1oXDTEyMDYwMTE2MzA0N1owRTELMAkG +A1UEBhMCQVUxEzARBgNVBAgTClNvbWUtU3RhdGUxITAfBgNVBAoTGEludGVybmV0 +IFdpZGdpdHMgUHR5IEx0ZDCBnzANBgkqhkiG9w0BAQEFAAOBjQAwgYkCgYEAzrQa ++3uX+75buOq2zXl8vgOn8DYtZCK1jm6kC17Qak3iHIkzUXSfYRlEWAzJsECjDWaL +6KCh5/B5XsdatIfpVb7SoeAK1Cs5ONTWNSvMTnDToJEJ3nY/cdcg3Nlrld2LkAt6 +W7Jh2NZC29e7+t1ro6w5Ok88dkYZul5Qn9Lc6isCAwEAATANBgkqhkiG9w0BAQUF +AAOBgQC5cmtHh6M7rByX3Gm5cMrtM+Gbfg25VP9BOgCCj+DjwY4gStcuhtU4sxZX +7IsoLMi22uiqP9/NSOReVB1gOkWMA5mkqACLerQqFmedXCjiCwgZ5F4G2UVpkXn7 +6QYThFtX1umpWcZwtb2n6NatmCcHMfPVAZFxmI/V5kl7lT6sNw== +-----END CERTIFICATE----- diff --git a/test/ssl/privatekey.pem b/test/ssl/privatekey.pem new file mode 100644 index 0000000..b4236bc --- /dev/null +++ b/test/ssl/privatekey.pem @@ -0,0 +1,15 @@ +-----BEGIN RSA PRIVATE KEY----- +MIICXQIBAAKBgQDOtBr7e5f7vlu46rbNeXy+A6fwNi1kIrWObqQLXtBqTeIciTNR +dJ9hGURYDMmwQKMNZovooKHn8Hlex1q0h+lVvtKh4ArUKzk41NY1K8xOcNOgkQne +dj9x1yDc2WuV3YuQC3pbsmHY1kLb17v63WujrDk6Tzx2Rhm6XlCf0tzqKwIDAQAB +AoGBAKFclkfF9yKOOxpDGMuU0F2hiwOJt6uZMPRsyOEbdkXWYPJ35Ljs+tKZH/JA +oV5XRzJZ4FSMuXfQEV987wFJrEtcQH4GaH+ZUGmoeCmYs1Gal3Pumexvbn5ffduu +HiFkoGhFUQuVGuBE1W7yOk/EmJQZlOBqQCuSW5rxzJGvwu55AkEA+pUgJsdSDMwk +KVNWb2UvDOYwJMa1PP7q32+P8o2S092zGm9HxPhYUhBdx8OqGfKbOUQTPlMOK9f4 +6ECD1stgLwJBANMsIIpHwah/B5DJaJEOI2iqc1RwntdpsxYTFsptMDkzi576q7y+ +JQXtc3DkfEMc/9oJSG+Tb+LHWdhhWADy+sUCQBU7Z4MBpokhDvtVbWB48VildHTZ +RWgKrXoLKOZDaqp7AX7+6NTeuhUR//A6OwKB1PcwNnU0cmHypcuAE+uyRc8CQCD6 +pD5URIdHB2xyN/Vnato+vHI0gGoN5N0OsCF++egFB8oVRdrdKzUIx12bIVjt33sy +tfBO60tUbNChKzhCui0CQQC+8dx4GlJ/AR/51b5zLIQ+XDcfj0SbHAEn/cdrNJb2 +x1LSckVayPvaVslesPzicC8cHfFqD3i8GSt6wRs7/fI8 +-----END RSA PRIVATE KEY-----