diff --git a/package.json b/package.json index a919e20..c2b3109 100644 --- a/package.json +++ b/package.json @@ -24,6 +24,12 @@ "http-proxy": "1.3.0" }, "devDependencies": { + "mocha": "~2.2.4", + "nock": "~1.9.0", + "supertest": "~0.15.0" + }, + "scripts": { + "test": "./node_modules/.bin/mocha ./test/test.js --reporter spec" }, "engines": { "node": ">=0.6.6", diff --git a/test/dummy.txt b/test/dummy.txt new file mode 100644 index 0000000..eaf5f75 --- /dev/null +++ b/test/dummy.txt @@ -0,0 +1 @@ +dummy content diff --git a/test/setup.js b/test/setup.js new file mode 100644 index 0000000..748a734 --- /dev/null +++ b/test/setup.js @@ -0,0 +1,95 @@ +var nock = require('nock'); + +nock.enableNetConnect('127.0.0.1'); + +function echoheaders(origin) { + nock(origin) + .persist() + .get('/echoheaders') + .reply(function(uri) { + var headers = this.req.headers; + var excluded_headers = [ + 'accept-encoding', + 'user-agent', + 'connection', + // Remove this header since its value is platform-specific. + 'x-forwarded-for', + 'test-include-xfwd', + ]; + if (!('test-include-xfwd' in headers)) { + excluded_headers.push('x-forwarded-port'); + excluded_headers.push('x-forwarded-proto'); + } + var response = {}; + Object.keys(headers).forEach(function(name) { + if (excluded_headers.indexOf(name) === -1) { + response[name] = headers[name]; + } + }); + return response; + }); +} + +nock('http://example.com') + .persist() + .get('/') + .reply(200, 'Response from example.com') + + .post('/echopost') + .reply(200, function(uri, requestBody) { + return requestBody; + }) + + .get('/setcookie') + .reply(200, '', { + 'Set-Cookie': 'x', + 'Set-Cookie2': 'y', + 'Set-Cookie3': 'z', // This is not a special cookie setting header. + }) + + .get('/redirecttarget') + .reply(200, 'redirect target', { + 'Some header': 'value' + }) + + .head('/redirect') + .reply(302, '', { + 'Location': '/redirecttarget' + }) + + .get('/redirect') + .reply(302, 'redirecting...', { + 'Location': '/redirecttarget' + }) + + .get('/redirectposttarget') + .reply(200, 'post target') + + .post('/redirectposttarget') + .reply(200, 'post target (POST)') + + .post('/redirectpost') + .reply(302, 'redirecting...', { + 'Location': '/redirectposttarget' + }) + + .post('/redirect307') + .reply(307, 'redirecting...', { + 'Location': '/redirectposttarget' + }) + + .get('/redirect2redirect') + .reply(302, 'redirecting to redirect...', { + 'Location': '/redirect' + }) + + .get('/redirectloop') + .reply(302, 'redirecting ad infinitum...', { + 'Location': '/redirectloop' + }) +; + +echoheaders('http://example.com'); +echoheaders('http://example.com:1337'); +echoheaders('https://example.com'); +echoheaders('https://example.com:1337'); diff --git a/test/test.js b/test/test.js new file mode 100644 index 0000000..3eb59d9 --- /dev/null +++ b/test/test.js @@ -0,0 +1,367 @@ +require('./setup'); + +var createServer = require('../').createServer; +var request = require('supertest'); +var path = require('path'); +var fs = require('fs'); + +var helpTextPath = path.join(__dirname, '../lib/help.txt'); +var helpText = fs.readFileSync(helpTextPath, { encoding: 'utf8' }); + +request.Test.prototype.expectJSON = function(json, done) { + return this.expect(200, JSON.stringify(json), done); +}; + +var cors_anywhere; +function stopServer(done) { + cors_anywhere.close(function() { + done(); + }); + cors_anywhere = null; +} + +describe('Basic functionality', function() { + before(function() { + // Mostly the default settings from server.js + cors_anywhere = createServer(); + }); + after(stopServer); + + it('GET /', function(done) { + request(cors_anywhere) + .get('/') + .type('text/plain') + .expect('Access-Control-Allow-Origin', '*') + .expect(200, helpText, done); + }); + + it('GET /iscorsneeded', function(done) { + request(cors_anywhere) + .get('/iscorsneeded') + .expect(function(res) { + if ('access-control-allow-origin' in res.headers) { + return 'access-control-allow-origin header should not be set'; + } + }) + .end(done); + }); + + it('GET /example.com:65536', function(done) { + request(cors_anywhere) + .get('/example.com:65536') + .expect('Access-Control-Allow-Origin', '*') + .expect(400, 'Port number too large: 65536', done); + }); + + it('GET /favicon.ico', function(done) { + request(cors_anywhere) + .get('/favicon.ico') + .expect('Access-Control-Allow-Origin', '*') + .expect(404, 'Invalid host: favicon.ico', done); + }); + + it('GET /robots.txt', function(done) { + request(cors_anywhere) + .get('/robots.txt') + .expect('Access-Control-Allow-Origin', '*') + .expect(404, 'Invalid host: robots.txt', done); + }); + + it('GET /example.com', function(done) { + request(cors_anywhere) + .get('/example.com') + .expect('Access-Control-Allow-Origin', '*') + .expect('x-request-url', 'http://example.com/') + .expect(200, 'Response from example.com', done); + }); + + it('GET //example.com', function(done) { + // '/example.com' is an invalid URL. + request(cors_anywhere) + .get('//example.com') + .expect('Access-Control-Allow-Origin', '*') + .expect(200, helpText, done); + }); + + it('GET ///example.com', function(done) { + // API base URL (with trailing slash) + '//example.com' + request(cors_anywhere) + .get('///example.com') + .expect('Access-Control-Allow-Origin', '*') + .expect('x-request-url', 'http://example.com/') + .expect(200, 'Response from example.com', done); + }); + + it('GET /http://example.com', function(done) { + request(cors_anywhere) + .get('/http://example.com') + .expect('Access-Control-Allow-Origin', '*') + .expect('x-request-url', 'http://example.com/') + .expect(200, 'Response from example.com', done); + }); + + it('POST plain text', function(done) { + request(cors_anywhere) + .post('/example.com/echopost') + .send('{"this is a request body & should not be mangled":1.00}') + .expect('Access-Control-Allow-Origin', '*') + .expect('{"this is a request body & should not be mangled":1.00}', done); + }); + + it('POST file', function(done) { + request(cors_anywhere) + .post('/example.com/echopost') + .attach('file', path.join(__dirname, 'dummy.txt')) + .expect('Access-Control-Allow-Origin', '*') + .expect(/\r\nContent-Disposition: form-data; name="file"; filename="dummy.txt"\r\nContent-Type: text\/plain\r\n\r\ndummy content\n\r\n/, done); + }); + + it('HEAD with redirect should be followed', function(done) { + // Redirects are automatically followed, because redirects are to be + // followed automatically per specification regardless of the HTTP verb. + request(cors_anywhere) + .head('/example.com/redirect') + .redirects(0) + .expect('Access-Control-Allow-Origin', '*') + .expect('some header', 'value') + .expect('x-request-url', 'http://example.com/redirect') + .expect('x-cors-redirect-1', '302 http://example.com/redirecttarget') + .expect('x-final-url', 'http://example.com/redirecttarget') + .expect('access-control-expose-headers', /some header,x-final-url/) + .expect(200, '', done); + }); + + it('GET with redirect should be followed', function(done) { + request(cors_anywhere) + .get('/example.com/redirect') + .redirects(0) + .expect('Access-Control-Allow-Origin', '*') + .expect('some header', 'value') + .expect('x-request-url', 'http://example.com/redirect') + .expect('x-cors-redirect-1', '302 http://example.com/redirecttarget') + .expect('x-final-url', 'http://example.com/redirecttarget') + .expect('access-control-expose-headers', /some header,x-final-url/) + .expect(200, 'redirect target', done); + }); + + it('GET with redirect loop should interrupt', function(done) { + request(cors_anywhere) + .get('/example.com/redirectloop') + .redirects(0) + .expect('Access-Control-Allow-Origin', '*') + .expect('x-request-url', 'http://example.com/redirectloop') + .expect('x-cors-redirect-1', '302 http://example.com/redirectloop') + .expect('x-cors-redirect-2', '302 http://example.com/redirectloop') + .expect('x-cors-redirect-3', '302 http://example.com/redirectloop') + .expect('x-cors-redirect-4', '302 http://example.com/redirectloop') + .expect('x-cors-redirect-5', '302 http://example.com/redirectloop') + .expect('Location', /^http:\/\/127.0.0.1:\d+\/http:\/\/example.com\/redirectloop$/) + .expect(302, 'redirecting ad infinitum...', done); + }); + + it('POST with 302 redirect should be followed', function(done) { + request(cors_anywhere) + .post('/example.com/redirectpost') + .redirects(0) + .expect('Access-Control-Allow-Origin', '*') + .expect('x-request-url', 'http://example.com/redirectpost') + .expect('x-cors-redirect-1', '302 http://example.com/redirectposttarget') + .expect('x-final-url', 'http://example.com/redirectposttarget') + .expect('access-control-expose-headers', /x-final-url/) + .expect(200, 'post target', done); + }); + + it('POST with 307 redirect should not be handled', function(done) { + // Because of implementation difficulties (having to keep the request body + // in memory), handling HTTP 307/308 redirects is deferred to the requestor. + request(cors_anywhere) + .post('/example.com/redirect307') + .redirects(0) + .expect('Access-Control-Allow-Origin', '*') + .expect('x-request-url', 'http://example.com/redirect307') + .expect('Location', /^http:\/\/127.0.0.1:\d+\/http:\/\/example.com\/redirectposttarget$/) + .expect('x-final-url', 'http://example.com/redirect307') + .expect('access-control-expose-headers', /x-final-url/) + .expect(307, 'redirecting...', done); + }); + + it('OPTIONS /', function(done) { + request(cors_anywhere) + .options('/') + .expect('Access-Control-Allow-Origin', '*') + .expect(200, '', done); + }); + + it('OPTIONS / with Access-Control-Request-Method / -Headers', function(done) { + request(cors_anywhere) + .options('/') + .set('Access-Control-Request-Method', 'DELETE') + .set('Access-Control-Request-Headers', 'X-Tralala') + .expect('Access-Control-Allow-Origin', '*') + .expect('Access-Control-Allow-Methods', 'DELETE') + .expect('Access-Control-Allow-Headers', 'X-Tralala') + .expect(200, '', done); + }); + + it('OPTIONS //bogus', function(done) { + // The preflight request always succeeds, regardless of whether the request + // is valid. + request(cors_anywhere) + .options('//bogus') + .expect('Access-Control-Allow-Origin', '*') + .expect(200, '', done); + }); + + it('X-Forwarded-* headers', function(done) { + request(cors_anywhere) + .get('/example.com/echoheaders') + .set('test-include-xfwd', '') + .expect('Access-Control-Allow-Origin', '*') + .expectJSON({ + host: 'example.com', + 'x-forwarded-port': '80', + 'x-forwarded-proto': 'http', + }, done); + }); + + it('X-Forwarded-* headers (non-standard port)', function(done) { + request(cors_anywhere) + .get('/example.com:1337/echoheaders') + .set('test-include-xfwd', '') + .expect('Access-Control-Allow-Origin', '*') + .expectJSON({ + host: 'example.com:1337', + 'x-forwarded-port': '1337', + 'x-forwarded-proto': 'http', + }, done); + }); + + // Skipped because x-forwarded-proto == http and port == 80 + it.skip('X-Forwarded-* headers (https)', function(done) { + request(cors_anywhere) + .get('/https://example.com/echoheaders') + .set('test-include-xfwd', '') + .expect('Access-Control-Allow-Origin', '*') + .expectJSON({ + host: 'example.com', + 'x-forwarded-port': '443', + 'x-forwarded-proto': 'https', + }, done); + }); + + // Skipped because x-forwarded-proto == http + it.skip('X-Forwarded-* headers (https, non-standard port)', function(done) { + request(cors_anywhere) + .get('/https://example.com:1337/echoheaders') + .set('test-include-xfwd', '') + .expect('Access-Control-Allow-Origin', '*') + .expectJSON({ + host: 'example.com:1337', + 'x-forwarded-port': '1337', + 'x-forwarded-proto': 'https', + }, done); + }); + + it('Ignore cookies', function(done) { + request(cors_anywhere) + .get('/example.com/setcookie') + .expect('Access-Control-Allow-Origin', '*') + .expect('Set-Cookie3', 'z') + .expect(function(res) { + if (res.headers['set-cookie']) { + return 'set-cookie header was set'; + } + if (res.headers['set-cookie2']) { + return 'set-cookie2 header was set'; + } + }) + .end(done); + }); + +}); + +describe('requireHeader', function() { + before(function() { + cors_anywhere = createServer({ + requireHeader: ['origin', 'x-requested-with'], + }); + }); + after(stopServer); + + it('GET /example.com without header', function(done) { + request(cors_anywhere) + .get('/example.com/') + .expect('Access-Control-Allow-Origin', '*') + .expect(400, 'Missing required request header. Must specify one of: origin,x-requested-with', done); + }); + + it('GET /example.com with X-Requested-With header', function(done) { + request(cors_anywhere) + .get('/example.com/') + .set('X-Requested-With', '') + .expect('Access-Control-Allow-Origin', '*') + .expect(200, done); + }); + + it('GET /example.com with Origin header', function(done) { + request(cors_anywhere) + .get('/example.com/') + .set('Origin', 'null') + .expect('Access-Control-Allow-Origin', '*') + .expect(200, done); + }); +}); + +describe('removeHeaders', function() { + before(function() { + cors_anywhere = createServer({ + removeHeaders: ['cookie', 'cookie2'], + }); + }); + after(stopServer); + + it('GET /example.com with request cookie', function(done) { + request(cors_anywhere) + .get('/example.com/echoheaders') + .set('cookie', 'a') + .set('cookie2', 'b') + .expect('Access-Control-Allow-Origin', '*') + .expectJSON({ + host: 'example.com', + }, done); + }); + + it('GET /example.com with unknown header', function(done) { + request(cors_anywhere) + .get('/example.com/echoheaders') + .set('cookie', 'a') + .set('cookie2', 'b') + .set('cookie3', 'c') + .expect('Access-Control-Allow-Origin', '*') + .expectJSON({ + host: 'example.com', + cookie3: 'c', + }, done); + }); +}); + +describe('httpProxyOptions.xfwd=false', function() { + before(function() { + cors_anywhere = createServer({ + httpProxyOptions: { + xfwd: false + } + }); + }); + after(stopServer); + + it('X-Forwarded-* headers should not be set', function(done) { + request(cors_anywhere) + .get('/example.com/echoheaders') + .set('test-include-xfwd', '') + .expect('Access-Control-Allow-Origin', '*') + .expectJSON({ + host: 'example.com', + }, done); + }); +});