diff --git a/package.json b/package.json index cc8830d04..05f026c0a 100644 --- a/package.json +++ b/package.json @@ -57,6 +57,7 @@ "karma-phantomjs-launcher": "~0.1.4", "karma-tap": "~1.0.1", "rimraf": "~2.2.8", + "server-destroy": "~1.0.0", "tape": "~3.0.0", "taper": "~0.4.0" } diff --git a/tests/ssl/ca/gen-localhost.sh b/tests/ssl/ca/gen-localhost.sh new file mode 100755 index 000000000..21a1f367b --- /dev/null +++ b/tests/ssl/ca/gen-localhost.sh @@ -0,0 +1,20 @@ +#!/bin/sh + +# Adapted from: +# http://nodejs.org/api/tls.html +# https://github.com/joyent/node/blob/master/test/fixtures/keys/Makefile + +# Create a private key +openssl genrsa -out localhost.key 2048 + +# Create a certificate signing request +openssl req -new -sha256 -key localhost.key -out localhost.csr -config localhost.cnf + +# Use the CSR and the CA key (previously generated) to create a certificate +openssl x509 -req \ + -in localhost.csr \ + -CA ca.crt \ + -CAkey ca.key \ + -set_serial 0x`cat ca.srl` \ + -passin 'pass:password' \ + -out localhost.crt diff --git a/tests/ssl/ca/localhost.cnf b/tests/ssl/ca/localhost.cnf new file mode 100644 index 000000000..d8465085c --- /dev/null +++ b/tests/ssl/ca/localhost.cnf @@ -0,0 +1,20 @@ +[ req ] +default_bits = 1024 +days = 3650 +distinguished_name = req_distinguished_name +attributes = req_attributes +prompt = no +output_password = password + +[ req_distinguished_name ] +C = US +ST = CA +L = Oakland +O = request +OU = request@localhost +CN = localhost +emailAddress = do.not@email.me + +[ req_attributes ] +challengePassword = password challenge + diff --git a/tests/ssl/ca/localhost.crt b/tests/ssl/ca/localhost.crt new file mode 100644 index 000000000..7c8ad98c8 --- /dev/null +++ b/tests/ssl/ca/localhost.crt @@ -0,0 +1,20 @@ +-----BEGIN CERTIFICATE----- +MIIDLTCCApYCCQCt9iAWqkDJwzANBgkqhkiG9w0BAQsFADCBojELMAkGA1UEBhMC +VVMxCzAJBgNVBAgTAkNBMRAwDgYDVQQHEwdPYWtsYW5kMRAwDgYDVQQKEwdyZXF1 +ZXN0MSYwJAYDVQQLEx1yZXF1ZXN0IENlcnRpZmljYXRlIEF1dGhvcml0eTESMBAG +A1UEAxMJcmVxdWVzdENBMSYwJAYJKoZIhvcNAQkBFhdtaWtlYWxAbWlrZWFscm9n +ZXJzLmNvbTAeFw0xNTAxMjQwNDEzMzVaFw0xNTAyMjMwNDEzMzVaMIGOMQswCQYD +VQQGEwJVUzELMAkGA1UECBMCQ0ExEDAOBgNVBAcTB09ha2xhbmQxEDAOBgNVBAoT +B3JlcXVlc3QxGjAYBgNVBAsUEXJlcXVlc3RAbG9jYWxob3N0MRIwEAYDVQQDEwls +b2NhbGhvc3QxHjAcBgkqhkiG9w0BCQEWD2RvLm5vdEBlbWFpbC5tZTCCASIwDQYJ +KoZIhvcNAQEBBQADggEPADCCAQoCggEBAJ8rQcGbUWXLZZ0XAq0A5OSG/yunu0D5 +x5GcgArmiWo2EwgkdGGd3DrECmsXAqg05LDTP8LjN5wdvtdEXc4R+vf54VN/CD31 +AtFXILfGEQZioWtdni+T9K0jEcVukdklAwCC1jjplJ8MxTXyJ9pEVoyv/tX4EFMf ++ayUsDUCSrJQLW069iV4GXQglZr6UVfSG3ip4+1JDvP0MKUhitfWkrAYtb8m30AS +fRj2Le/9HhhBWwxLDK1G23TqC86Sqe0Mhk5a1V5DKZPanDld5jVNKlrXTUMU4OcL +b3mdidAy5kSFmRSJJdficeXnp6eBGK5kOFoRIyjeJ0Ut/ntw2c7WcLsCAwEAATAN +BgkqhkiG9w0BAQsFAAOBgQAgie0OE8U3w4w2cGgCa8qqraOezz961/i/6zNLamMn +XSjoIpB8syOgXzPTwk/pR1OPOIfv2C06usqTR31r/zAN63Ev+wqBW4RIQ6mD1J0O +WxmuY7pYyISD+5CXGMoxmM4Mh78GBQaUWTwhbsZr+vNSgEWwJfEvoh2BAVUgqjHh +ug== +-----END CERTIFICATE----- diff --git a/tests/ssl/ca/localhost.csr b/tests/ssl/ca/localhost.csr new file mode 100644 index 000000000..a74907d20 --- /dev/null +++ b/tests/ssl/ca/localhost.csr @@ -0,0 +1,18 @@ +-----BEGIN CERTIFICATE REQUEST----- +MIIC9zCCAd8CAQAwgY4xCzAJBgNVBAYTAlVTMQswCQYDVQQIEwJDQTEQMA4GA1UE +BxMHT2FrbGFuZDEQMA4GA1UEChMHcmVxdWVzdDEaMBgGA1UECxQRcmVxdWVzdEBs +b2NhbGhvc3QxEjAQBgNVBAMTCWxvY2FsaG9zdDEeMBwGCSqGSIb3DQEJARYPZG8u +bm90QGVtYWlsLm1lMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAnytB +wZtRZctlnRcCrQDk5Ib/K6e7QPnHkZyACuaJajYTCCR0YZ3cOsQKaxcCqDTksNM/ +wuM3nB2+10RdzhH69/nhU38IPfUC0Vcgt8YRBmKha12eL5P0rSMRxW6R2SUDAILW +OOmUnwzFNfIn2kRWjK/+1fgQUx/5rJSwNQJKslAtbTr2JXgZdCCVmvpRV9IbeKnj +7UkO8/QwpSGK19aSsBi1vybfQBJ9GPYt7/0eGEFbDEsMrUbbdOoLzpKp7QyGTlrV +XkMpk9qcOV3mNU0qWtdNQxTg5wtveZ2J0DLmRIWZFIkl1+Jx5eenp4EYrmQ4WhEj +KN4nRS3+e3DZztZwuwIDAQABoCMwIQYJKoZIhvcNAQkHMRQTEnBhc3N3b3JkIGNo +YWxsZW5nZTANBgkqhkiG9w0BAQsFAAOCAQEAQBSAV6pyGnm1+EsDku9sKWy1ZhM8 +75+nQ2rJvAtmcLE7mAzJ5QEB8MfGELfPbpKJEHi/TUHvONyrIyml9zy1+0+fkxRx +5gXZ6Ggw64t5OpNgEc2EtJta+dua+W7gNeGFWPJ36iAHlkRIgK4PxttM7YV4hEwQ +kJ5jWmNPj/e033kPShBAnWPGFdFTG92oq9Xb0+yF4a1ff4PpQLVivj5tDzs80B5M +Khm38sQOK7qPR4IdugoJHkRtBcXQKNmeSXhYPl+0FYIFpvPd+E8DKWEOfR6LjQ9J +WBLLMvr4B8BXnoJu4uHzJln6uVWFxizfa+u9LRIrL7CjxgAupKQ6kRprgQ== +-----END CERTIFICATE REQUEST----- diff --git a/tests/ssl/ca/localhost.js b/tests/ssl/ca/localhost.js new file mode 100644 index 000000000..515700c01 --- /dev/null +++ b/tests/ssl/ca/localhost.js @@ -0,0 +1,29 @@ +'use strict' + +var fs = require('fs') +var https = require('https') +var options = { key: fs.readFileSync('./localhost.key') + , cert: fs.readFileSync('./localhost.crt') } + +var server = https.createServer(options, function (req, res) { + res.writeHead(200) + res.end() + server.close() +}) +server.listen(1337) + +var ca = fs.readFileSync('./ca.crt') +var agent = new https.Agent({ host: 'localhost', port: 1337, ca: ca }) + +https.request({ host: 'localhost' + , method: 'HEAD' + , port: 1337 + , agent: agent + , ca: [ ca ] + , path: '/' }, function (res) { + if (res.client.authorized) { + console.log('node test: OK') + } else { + throw new Error(res.client.authorizationError) + } +}).end() diff --git a/tests/ssl/ca/localhost.key b/tests/ssl/ca/localhost.key new file mode 100644 index 000000000..2cfeaf458 --- /dev/null +++ b/tests/ssl/ca/localhost.key @@ -0,0 +1,27 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIEogIBAAKCAQEAnytBwZtRZctlnRcCrQDk5Ib/K6e7QPnHkZyACuaJajYTCCR0 +YZ3cOsQKaxcCqDTksNM/wuM3nB2+10RdzhH69/nhU38IPfUC0Vcgt8YRBmKha12e +L5P0rSMRxW6R2SUDAILWOOmUnwzFNfIn2kRWjK/+1fgQUx/5rJSwNQJKslAtbTr2 +JXgZdCCVmvpRV9IbeKnj7UkO8/QwpSGK19aSsBi1vybfQBJ9GPYt7/0eGEFbDEsM +rUbbdOoLzpKp7QyGTlrVXkMpk9qcOV3mNU0qWtdNQxTg5wtveZ2J0DLmRIWZFIkl +1+Jx5eenp4EYrmQ4WhEjKN4nRS3+e3DZztZwuwIDAQABAoIBAE3YJgy+HY0fcM7n +VhOugEOUEnATVG1uu7/nPmgWX9ZmI+Czk4e6YN8MydueIVqKo94nMuPppGTh11gI +w6fo+0kUGLNxSWKj1YD0j7fRUrpAupl768VxIxUaNbLNZN9CTrmNQ6AJ/PnckQbV +K9B/46Ri3steyv0cgkt5XMRQHqAd+OAMiqiSD03gxgcpnyPCskzgk48GIM1NhjwW +Q6ia0uIPUnak7KxW13F6yH9ddnNpS1CJdcStaZeFWlZgDGbTDef9Op2+f42CU54/ +bXlnb6pm8ZHv7NxkMS3ncObv1d1TD3qfFOQpLiWu8EdyqVrCKFbToTnwG0XdYKuG +1+GEe4ECgYEAzSnTI+INAxADuqu/M9KXSKtj30YdAo52s5z8Ui0SWbUS9fxpxzAV +Kx00RKD4I9CwV8sq4IETPFd+x+ietcMVeLH7jkwoY7A8ntLKctgQvpdkOCgsd1+Y +g2H2ukKjsc0RH0QUaq8pSlrIzku09CKwAeQK7tBDUZ3wMH4Xc5o6M+sCgYEAxpvb +xXF7UW5+xt8hwxin0FhiaqJuJoCo0E6/JjXI5B6QJNxVfechvig2H2lcZ7HcGdO6 +r+CmpgIcoEtWTLunFM6JnrZnmQixoQCSyC4CbTfpUpDxr8/2cKDU6982eo0sG2Tu +I0CCDrqWMQFMBkeQBdQECBXi9rQs2hc7Ji29EnECgYBLp5uzhL01nucxI/oq+wJM +it8WS32RHsXI8B/fkb1NlUc7rGu5RxLXRjqrAAzg8CjHByV1ikN0ofMfdrln31uA +mWlhDNZsBGYmTybWeLScA6myR6Y2Eutjr3FTOBWzECK7O9inipYYVCfuYt6ElHIB +EH2zmNrqMuqKh0TQnVPPJwKBgCmYrxjVQby2ZbsFNK8F1O/f8wzeZC+QNssaExLP +pPmSJSJzOzyZUgnfpiZCDOZy6+RE4g7AAGc4fgJchQChNMc40r34+g2lMn7D/foL +GNsDIMz4KoZmCflg1fdo0qIsOxaptu6PLi4jih1NZjzSdCmkVAvVeamt5s7umqbO +YZEhAoGAeICIxtu1kx0LQxvQ3nfBv5aJwvksvTcAZvC02XpFIpL8l8WE1pUAWHMC +R4K4O8uzBH3ILAmnihG096lhTtnt9RiEtPzOPkAB/83nipa/NCLgOIPOVqTgnS1Z +2Zmckn2mbYTNxB8g+nQmeLeH6pM9+KhxHioQJIzPPpubfUTriY8= +-----END RSA PRIVATE KEY----- diff --git a/tests/test-tunnel.js b/tests/test-tunnel.js index 85adbf943..1972f2481 100644 --- a/tests/test-tunnel.js +++ b/tests/test-tunnel.js @@ -1,91 +1,239 @@ 'use strict' -// test that we can tunnel a https request over an http proxy -// keeping all the CA and whatnot intact. -// -// Note: this requires that squid is installed. -// If the proxy fails to start, we'll just log a warning and assume success. - var server = require('./server') + , tape = require('tape') , request = require('../index') + , https = require('https') + , net = require('net') , fs = require('fs') , path = require('path') - , child_process = require('child_process') - , tape = require('tape') + , util = require('util') + , url = require('url') + , destroyable = require('server-destroy') -var sqConf = path.resolve(__dirname, 'squid.conf') - , sqArgs = ['-f', sqConf, '-N', '-d', '5'] - , proxy = 'http://localhost:3128' - , squid - , ready = false - , installed = true - , squidError = null +var events = [] + , caFile = path.resolve(__dirname, 'ssl/ca/ca.crt') + , ca = fs.readFileSync(caFile) + , sslOpts = { + key : path.resolve(__dirname, 'ssl/ca/localhost.key'), + cert : path.resolve(__dirname, 'ssl/ca/localhost.crt') + } -// This test doesn't fit into tape very well... +var httpsOpts = https.globalAgent.options +httpsOpts.ca = httpsOpts.ca || [] +httpsOpts.ca.push(ca) -tape('setup', function(t) { - squid = child_process.spawn('squid', sqArgs) +var s = server.createServer() + , ss = server.createSSLServer(null, sslOpts) - squid.stderr.on('data', function(c) { - console.error('SQUIDERR ' + c.toString().trim().split('\n').join('\nSQUIDERR ')) - ready = c.toString().match(/ready to serve requests|Accepting HTTP Socket connections/i) - }) +// XXX when tunneling https over https, connections get left open so the server +// doesn't want to close normally (and same issue with http server on v0.8.x) +destroyable(s) +destroyable(ss) + +function event() { + events.push(util.format.apply(null, arguments)) +} - squid.stdout.on('data', function(c) { - console.error('SQUIDOUT ' + c.toString().trim().split('\n').join('\nSQUIDOUT ')) +function setListeners(server, type) { + server.on('/', function(req, res) { + event('%s response', type) + res.end(type + ' ok') }) - squid.on('error', function(c) { - console.error('squid: error ' + c) - if (c && !ready) { - installed = false - } + server.on(s.url + '/', function(req, res) { + event('%s proxy to http', type) + request(s.url).pipe(res) }) - squid.on('exit', function(c) { - console.error('squid: exit ' + c) - if (c && !ready) { - installed = false - return - } + server.on(ss.url + '/', function(req, res) { + event('%s proxy to https', type) + request(ss.url).pipe(res) + }) - if (c) { - squidError = squidError || new Error('Squid exited with code ' + c) - } - if (squidError) { - throw squidError - } + server.on('connect', function(req, client, head) { + var u = url.parse(req.url) + var server = net.connect(u.host, u.port, function() { + event('%s connect to %s', type, req.url) + client.write('HTTP/1.1 200 Connection established\r\n\r\n') + client.pipe(server) + server.write(head) + server.pipe(client) + }) }) +} - t.end() -}) +setListeners(s, 'http') +setListeners(ss, 'https') -tape('tunnel', function(t) { - setTimeout(function F() { - if (!installed) { - console.error('squid must be installed to run this test.') - console.error('skipping this test. please install squid and run again if you need to test tunneling.') - t.skip() +tape('setup', function(t) { + s.listen(s.port, function() { + ss.listen(ss.port, function() { t.end() - return - } - if (!ready) { - setTimeout(F, 100) - return + }) + }) +}) + +// monkey-patch since you can't set a custom certificate authority for the +// proxy in tunnel-agent (this is necessary for "* over https" tests) +var customCaCount = 0 +var httpsRequestOld = https.request +https.request = function(options) { + if (customCaCount) { + options.ca = ca + customCaCount-- + } + return httpsRequestOld.apply(this, arguments) +} + +function runTest(name, opts, expected) { + tape(name, function(t) { + opts.ca = ca + if (opts.proxy === ss.url) { + customCaCount = (opts.url === ss.url ? 2 : 1) } - request({ - uri: 'https://registry.npmjs.org/', - proxy: 'http://localhost:3128', - strictSSL: true, - json: true - }, function(err, body) { - t.equal(err, null) + request(opts, function(err, res, body) { + event(err ? 'err ' + err.message : res.statusCode + ' ' + body) + t.deepEqual(events, expected) + events = [] t.end() }) - }, 100) -}) + }) +} + + +// HTTP OVER HTTP TESTS + +runTest('http over http, tunnel=true', { + url : s.url, + proxy : s.url, + tunnel : true +}, [ + 'http connect to localhost:' + s.port, + 'http response', + '200 http ok' +]) + +runTest('http over http, tunnel=false', { + url : s.url, + proxy : s.url, + tunnel : false +}, [ + 'http proxy to http', + 'http response', + '200 http ok' +]) + +runTest('http over http, tunnel=default', { + url : s.url, + proxy : s.url +}, [ + 'http proxy to http', + 'http response', + '200 http ok' +]) + + +// HTTP OVER HTTPS TESTS + +runTest('http over https, tunnel=true', { + url : s.url, + proxy : ss.url, + tunnel : true +}, [ + 'https connect to localhost:' + s.port, + 'http response', + '200 http ok' +]) + +runTest('http over https, tunnel=false', { + url : s.url, + proxy : ss.url, + tunnel : false +}, [ + 'https proxy to http', + 'http response', + '200 http ok' +]) + +runTest('http over https, tunnel=default', { + url : s.url, + proxy : ss.url +}, [ + 'https proxy to http', + 'http response', + '200 http ok' +]) + + +// HTTPS OVER HTTP TESTS + +runTest('https over http, tunnel=true', { + url : ss.url, + proxy : s.url, + tunnel : true +}, [ + 'http connect to localhost:' + ss.port, + 'https response', + '200 https ok' +]) + +runTest('https over http, tunnel=false', { + url : ss.url, + proxy : s.url, + tunnel : false // currently has no effect +}, [ + 'http connect to localhost:' + ss.port, + 'https response', + '200 https ok' +]) + +runTest('https over http, tunnel=default', { + url : ss.url, + proxy : s.url +}, [ + 'http connect to localhost:' + ss.port, + 'https response', + '200 https ok' +]) + + +// HTTPS OVER HTTPS TESTS + +runTest('https over https, tunnel=true', { + url : ss.url, + proxy : ss.url, + tunnel : true +}, [ + 'https connect to localhost:' + ss.port, + 'https response', + '200 https ok' +]) + +runTest('https over https, tunnel=false', { + url : ss.url, + proxy : ss.url, + tunnel : false // currently has no effect +}, [ + 'https connect to localhost:' + ss.port, + 'https response', + '200 https ok' +]) + +runTest('https over https, tunnel=default', { + url : ss.url, + proxy : ss.url +}, [ + 'https connect to localhost:' + ss.port, + 'https response', + '200 https ok' +]) + tape('cleanup', function(t) { - squid.kill('SIGKILL') - t.end() + s.destroy(function() { + ss.destroy(function() { + t.end() + }) + }) })