diff --git a/README.md b/README.md index b7f5aee..fe96c6a 100644 --- a/README.md +++ b/README.md @@ -97,6 +97,9 @@ proxy requests. The following options are supported: * array of strings `originWhitelist` - If set, requests whose origin is not listed are blocked. If this list is empty, all origins are allowed. Example: `['https://good.example.com', 'http://good.example.com']` +* boolean `redirectSameOrigin` - If true, requests to URLs from the same origin will not be proxied but redirected. + The primary purpose for this option is to save server resources by delegating the request to the client + (since same-origin requests should always succeed, even without proxying). * array of strings `requireHeader` - If set, the request must include this header or the API will refuse to proxy. Recommended if you want to prevent users from using the proxy for normal browsing. Example: `['Origin', 'X-Requested-With']`. diff --git a/lib/cors-anywhere.js b/lib/cors-anywhere.js index 2e49e40..3b2ba88 100644 --- a/lib/cors-anywhere.js +++ b/lib/cors-anywhere.js @@ -213,6 +213,7 @@ function getHandler(options, proxy) { maxRedirects: 5, // Maximum number of redirects to be followed. originBlacklist: [], // Requests from these origins will be blocked. originWhitelist: [], // If non-empty, requests not from an origin in this list will be blocked. + redirectSameOrigin: false, // Redirect the client to the requested URL for same-origin requests. requireHeader: null, // Require a header to be set? removeHeaders: [], // Strip these request headers. setHeaders: {}, // Set these request headers. @@ -302,6 +303,17 @@ function getHandler(options, proxy) { return; } + if (corsAnywhere.redirectSameOrigin && origin && location.href[origin.length] === '/' && + location.href.lastIndexOf(origin, 0) === 0) { + // Send a permanent redirect to offload the server. Badly coded clients should not waste our resources. + cors_headers.vary = 'origin'; + cors_headers['cache-control'] = 'private'; + cors_headers.location = location.href; + res.writeHead(301, 'Please use a direct request', cors_headers); + res.end(); + return; + } + var isRequestedOverHttps = req.connection.encrypted || /^\s*https/.test(req.headers['x-forwarded-proto']); var proxyBaseUrl = (isRequestedOverHttps ? 'https://' : 'http://') + req.headers.host; diff --git a/server.js b/server.js index ad970ff..a87abf1 100644 --- a/server.js +++ b/server.js @@ -21,6 +21,7 @@ cors_proxy.createServer({ 'x-heroku-dynos-in-use', 'x-request-start', ], + redirectSameOrigin: true, httpProxyOptions: { // Do not add X-Forwarded-For, etc. headers, because Heroku already adds it. xfwd: false, diff --git a/test/setup.js b/test/setup.js index 5f86c35..83cd3bd 100644 --- a/test/setup.js +++ b/test/setup.js @@ -102,6 +102,24 @@ nock('https://example.com') .reply(200, 'Response from https://example.com') ; +nock('http://example.com.com') + .persist() + .get('/') + .reply(200, 'Response from example.com.com') +; + +nock('http://example.com:1234') + .persist() + .get('/') + .reply(200, 'Response from example.com:1234') +; + +nock('http://prefix.example.com') + .persist() + .get('/') + .reply(200, 'Response from prefix.example.com') +; + echoheaders('http://example.com'); echoheaders('http://example.com:1337'); echoheaders('https://example.com'); diff --git a/test/test.js b/test/test.js index bb2c8de..6704b71 100644 --- a/test/test.js +++ b/test/test.js @@ -505,6 +505,81 @@ describe('originWhitelist', function() { }); }); +describe('redirectSameOrigin', function() { + before(function() { + cors_anywhere = createServer({ + redirectSameOrigin: true, + }); + cors_anywhere_port = cors_anywhere.listen(0).address().port; + }); + after(stopServer); + + it('GET /example.com with Origin: http://example.com', function(done) { + request(cors_anywhere) + .get('/example.com/') + .set('Origin', 'http://example.com') + .expect('Access-Control-Allow-Origin', '*') + .expect('Cache-Control', 'private') + .expect('Vary', 'origin') + .expect('Location', 'http://example.com/') + .expect(301, done); + }); + + it('GET /example.com with Origin: https://example.com', function(done) { + // Not same-origin because of different schemes. + request(cors_anywhere) + .get('/example.com/') + .set('Origin', 'https://example.com') + .expect('Access-Control-Allow-Origin', '*') + .expect(200, 'Response from example.com', done); + }); + + it('GET /example.com with Origin: http://example.com:1234', function(done) { + // Not same-origin because of different ports. + request(cors_anywhere) + .get('/example.com/') + .set('Origin', 'http://example.com:1234') + .expect('Access-Control-Allow-Origin', '*') + .expect(200, 'Response from example.com', done); + }); + + it('GET /example.com:1234 with Origin: http://example.com', function(done) { + // Not same-origin because of different ports. + request(cors_anywhere) + .get('/example.com:1234/') + .set('Origin', 'http://example.com') + .expect('Access-Control-Allow-Origin', '*') + .expect(200, 'Response from example.com:1234', done); + }); + + it('GET /example.com with Origin: http://example.com.test', function(done) { + // Not same-origin because of different host names. + request(cors_anywhere) + .get('/example.com/') + .set('Origin', 'http://example.com.test') + .expect('Access-Control-Allow-Origin', '*') + .expect(200, 'Response from example.com', done); + }); + + it('GET /example.com.com with Origin: http://example.com', function(done) { + // Not same-origin because of different host names. + request(cors_anywhere) + .get('/example.com.com/') + .set('Origin', 'http://example.com') + .expect('Access-Control-Allow-Origin', '*') + .expect(200, 'Response from example.com.com', done); + }); + + it('GET /prefix.example.com with Origin: http://example.com', function(done) { + // Not same-origin because of different host names. + request(cors_anywhere) + .get('/prefix.example.com/') + .set('Origin', 'http://example.com') + .expect('Access-Control-Allow-Origin', '*') + .expect(200, 'Response from prefix.example.com', done); + }); +}); + describe('requireHeader', function() { before(function() { cors_anywhere = createServer({