├── .eslintignore ├── Procfile ├── test ├── dummy.txt ├── customHelp.txt ├── customHelp.html ├── cert.pem ├── key.pem ├── child.js ├── setup.js ├── test-memory.js ├── test-examples.js ├── test-ratelimit.js └── test.js ├── .gitignore ├── .travis.yml ├── .eslintrc ├── package.json ├── lib ├── help.txt ├── rate-limit.js ├── regexp-top-level-domain.js └── cors-anywhere.js ├── server.js ├── demo.html └── README.md /.eslintignore: -------------------------------------------------------------------------------- 1 | coverage/ 2 | -------------------------------------------------------------------------------- /Procfile: -------------------------------------------------------------------------------- 1 | web: node server.js 2 | -------------------------------------------------------------------------------- /test/dummy.txt: -------------------------------------------------------------------------------- 1 | dummy content 2 | -------------------------------------------------------------------------------- /test/customHelp.txt: -------------------------------------------------------------------------------- 1 | Server is OK!! 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.swp 2 | *.tmp 3 | *.log 4 | 5 | coverage/ 6 | node_modules/ 7 | -------------------------------------------------------------------------------- /test/customHelp.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 |

Custom HTML help!!

6 | 7 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - 0.10 4 | - 4 5 | - 6 6 | - 7 7 | - 8 8 | - 9 9 | - 11 10 | - 12 11 | - 13 12 | - 13 13 | - 14 14 | script: 15 | - npm run lint 16 | - npm run test 17 | - npm run test-coverage && cat coverage/lcov.info | ./node_modules/.bin/coveralls && rm -rf coverage 18 | -------------------------------------------------------------------------------- /test/cert.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIBsTCCARoCCQDp0DuED0RAJzANBgkqhkiG9w0BAQsFADAdMRswGQYDVQQDDBJj 3 | b3JzLWFueXdoZXJlIHRlc3QwHhcNMTUwNTA2MDcyOTM1WhcNMTUwNjA1MDcyOTM1 4 | WjAdMRswGQYDVQQDDBJjb3JzLWFueXdoZXJlIHRlc3QwgZ8wDQYJKoZIhvcNAQEB 5 | BQADgY0AMIGJAoGBALzTF5ClJKvkB6h9h7kLORV+mMV3ySDs+oGZn0NgXM+yb9Zh 6 | 69r5e95zZJl/V432LFdy0hkEcVteUkC2REWG8D4COGfiwWsXyZdaP1qqLpDpPAMm 7 | v6xFHjW6rVuxzfr4GUjE0Zh9Fg2R2SbtCOcHS/LZoDVOqOvn6+urP6XFY4aFAgMB 8 | AAEwDQYJKoZIhvcNAQELBQADgYEAYXMhS8ouff/c8lSUUs/CLh010cj5RPk/ivS7 9 | aN2PArzQ6pZvhpgJKf7XAQksBtLYYZMzIpG6W8zhPSbqzly7lELAdE+sxcbbfu8A 10 | FMjNVFQ2Fm1c8ImX8qpE3nhVrPAiwfPjGBqKHTl730gvbh1XH9TC4O4dZcbEomX3 11 | 5MsxQfc= 12 | -----END CERTIFICATE----- 13 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "node": true 4 | }, 5 | "rules": { 6 | "array-bracket-spacing": [2, "never"], 7 | "block-scoped-var": 2, 8 | "brace-style": [2, "1tbs", {"allowSingleLine": true}], 9 | "comma-dangle": [2, "always-multiline"], 10 | "computed-property-spacing": [2, "never"], 11 | "curly": 2, 12 | "eol-last": 2, 13 | "eqeqeq": [2, "smart"], 14 | "max-len": [1, 125], 15 | "new-cap": 1, 16 | "no-extend-native": 2, 17 | "no-mixed-spaces-and-tabs": 2, 18 | "no-trailing-spaces": 2, 19 | "no-undef": 2, 20 | "no-unused-vars": 1, 21 | "no-use-before-define": [2, "nofunc"], 22 | "object-curly-spacing": [2, "never"], 23 | "quotes": [2, "single", "avoid-escape"], 24 | "semi": [2, "always"], 25 | "keyword-spacing": 2, 26 | "space-unary-ops": 2 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /test/key.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN RSA PRIVATE KEY----- 2 | MIICXQIBAAKBgQC80xeQpSSr5AeofYe5CzkVfpjFd8kg7PqBmZ9DYFzPsm/WYeva 3 | +Xvec2SZf1eN9ixXctIZBHFbXlJAtkRFhvA+Ajhn4sFrF8mXWj9aqi6Q6TwDJr+s 4 | RR41uq1bsc36+BlIxNGYfRYNkdkm7QjnB0vy2aA1Tqjr5+vrqz+lxWOGhQIDAQAB 5 | AoGBAISy8OelN01Zlowxk/VWTsqtSl3UHdP21uHHfWaTTQZlxzTpYiBknkmp3LQH 6 | CxfoPidCuSX9ulBUzAdQUFBwUVp8wyPIRjpNyRiD58dLNxG0G+OACqnLxNWqIf6F 7 | vS3UqrRGIA5u+GSz+0g3DAeVA5JmsAyHQGkJsh3pcuD8/7wNAkEA7MScGfySy9td 8 | dDBekVU5/GaVg4DA4ELtDNfa99ARB89XP0ps/XrOPEL9yxTjWIHH+qxuhpfG6zGN 9 | ouxZlvBT9wJBAMwpig4A4JE8M8pBDwMY4213gud8B1grQTbhz5bv51aTaIEQFcxw 10 | sGfEmAfVToI+kVTrdFggy42YCSMSvwuF4mMCQQDZHkqPwf/TlSwT2i8+UstD28aL 11 | uswkWvsKZf9UdKbJZKd7UIK1x6HLvRsC2frJNOnvw6PvJMuy7dQWbWqScXxtAkBv 12 | /5msdO68vbnriiUiHdUliBpXwsKEq7Xq1ZV7x7+wzszVgG106ZzcUAzWvz2CVbCE 13 | VWZNsi/4TR82DmKff6LhAkBA/xceWaZjxh5dkWkIrMFWd2GFhGlpfwYw7oELwRL8 14 | RYXzc1Mr2fDdZDgwgjg67JQqIhOQ3E4RGKPgZ+E7Pk3/ 15 | -----END RSA PRIVATE KEY----- 16 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "cors-anywhere", 3 | "version": "0.4.3", 4 | "description": "CORS Anywhere is a reverse proxy which adds CORS headers to the proxied request. Request URL is taken from the path", 5 | "license": "MIT", 6 | "author": "Rob Wu ", 7 | "repository": { 8 | "type": "git", 9 | "url": "https://github.com/Rob--W/cors-anywhere.git" 10 | }, 11 | "bugs": { 12 | "url": "https://github.com/Rob--W/cors-anywhere/issues/", 13 | "email": "rob@robwu.nl" 14 | }, 15 | "keywords": [ 16 | "cors", 17 | "cross-domain", 18 | "http-proxy", 19 | "proxy", 20 | "heroku" 21 | ], 22 | "main": "./lib/cors-anywhere.js", 23 | "files": [ 24 | "lib/", 25 | "test/", 26 | "Procfile", 27 | "demo.html", 28 | "server.js" 29 | ], 30 | "dependencies": { 31 | "http-proxy": "1.11.1", 32 | "proxy-from-env": "0.0.1" 33 | }, 34 | "devDependencies": { 35 | "coveralls": "^2.11.6", 36 | "eslint": "^2.2.0", 37 | "istanbul": "^0.4.2", 38 | "lolex": "^1.5.0", 39 | "mocha": "^3.4.2", 40 | "nock": "^8.2.1", 41 | "supertest": "^2.0.1" 42 | }, 43 | "scripts": { 44 | "lint": "eslint .", 45 | "test": "mocha ./test/test*.js --reporter spec", 46 | "test-coverage": "istanbul cover ./node_modules/.bin/_mocha -- test/test.js test/test-ratelimit.js --reporter spec" 47 | }, 48 | "engines": { 49 | "node": ">=0.10.0" 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /lib/help.txt: -------------------------------------------------------------------------------- 1 | This API enables cross-origin requests to anywhere. 2 | 3 | Usage: 4 | 5 | / Shows help 6 | /iscorsneeded This is the only resource on this host which is served without CORS headers. 7 | / Create a request to , and includes CORS headers in the response. 8 | 9 | If the protocol is omitted, it defaults to http (https if port 443 is specified). 10 | 11 | Cookies are disabled and stripped from requests. 12 | 13 | Redirects are automatically followed. For debugging purposes, each followed redirect results 14 | in the addition of a X-CORS-Redirect-n header, where n starts at 1. These headers are not 15 | accessible by the XMLHttpRequest API. 16 | After 5 redirects, redirects are not followed any more. The redirect response is sent back 17 | to the browser, which can choose to follow the redirect (handled automatically by the browser). 18 | 19 | The requested URL is available in the X-Request-URL response header. 20 | The final URL, after following all redirects, is available in the X-Final-URL response header. 21 | 22 | 23 | To prevent the use of the proxy for casual browsing, the API requires either the Origin 24 | or the X-Requested-With header to be set. To avoid unnecessary preflight (OPTIONS) requests, 25 | it's recommended to not manually set these headers in your code. 26 | 27 | 28 | Demo : https://robwu.nl/cors-anywhere.html 29 | Source code : https://github.com/Rob--W/cors-anywhere/ 30 | Documentation : https://github.com/Rob--W/cors-anywhere/#documentation 31 | -------------------------------------------------------------------------------- /server.js: -------------------------------------------------------------------------------- 1 | // Listen on a specific host via the HOST environment variable 2 | var host = process.env.HOST || '0.0.0.0'; 3 | // Listen on a specific port via the PORT environment variable 4 | var port = process.env.PORT || 8080; 5 | 6 | // Grab the blacklist from the command-line so that we can update the blacklist without deploying 7 | // again. CORS Anywhere is open by design, and this blacklist is not used, except for countering 8 | // immediate abuse (e.g. denial of service). If you want to block all origins except for some, 9 | // use originWhitelist instead. 10 | var originBlacklist = parseEnvList(process.env.CORSANYWHERE_BLACKLIST); 11 | var originWhitelist = parseEnvList(process.env.CORSANYWHERE_WHITELIST); 12 | function parseEnvList(env) { 13 | if (!env) { 14 | return []; 15 | } 16 | return env.split(','); 17 | } 18 | 19 | // Set up rate-limiting to avoid abuse of the public CORS Anywhere server. 20 | var checkRateLimit = require('./lib/rate-limit')(process.env.CORSANYWHERE_RATELIMIT); 21 | 22 | var cors_proxy = require('./lib/cors-anywhere'); 23 | cors_proxy.createServer({ 24 | originBlacklist: originBlacklist, 25 | originWhitelist: originWhitelist, 26 | requireHeader: ['origin', 'x-requested-with'], 27 | checkRateLimit: checkRateLimit, 28 | removeHeaders: [ 29 | 'cookie', 30 | 'cookie2', 31 | // Strip Heroku-specific headers 32 | 'x-heroku-queue-wait-time', 33 | 'x-heroku-queue-depth', 34 | 'x-heroku-dynos-in-use', 35 | 'x-request-start', 36 | ], 37 | redirectSameOrigin: true, 38 | httpProxyOptions: { 39 | // Do not add X-Forwarded-For, etc. headers, because Heroku already adds it. 40 | xfwd: false, 41 | }, 42 | }).listen(port, host, function() { 43 | console.log('Running CORS Anywhere on ' + host + ':' + port); 44 | }); 45 | -------------------------------------------------------------------------------- /test/child.js: -------------------------------------------------------------------------------- 1 | // When this module is loaded, CORS Anywhere is started. 2 | // Then, a request is generated to warm up the server (just in case). 3 | // Then the base URL of CORS Anywhere is sent to the parent process. 4 | // ... 5 | // When the parent process is done, it sends an empty message to this child 6 | // process, which in turn records the change in used heap space. 7 | // The difference in heap space is finally sent back to the parent process. 8 | // ... 9 | // The parent process should then kill this child. 10 | 11 | process.on('uncaughtException', function(e) { 12 | console.error('Uncaught exception in child process: ' + e); 13 | console.error(e.stack); 14 | process.exit(-1); 15 | }); 16 | 17 | // Invoke memoryUsage() without using its result to make sure that any internal 18 | // datastructures that supports memoryUsage() is initialized and won't pollute 19 | // the memory usage measurement later on. 20 | process.memoryUsage(); 21 | 22 | var heapUsedStart = 0; 23 | function getMemoryUsage(callback) { 24 | // Note: Requires --expose-gc 25 | // 6 is the minimum amount of gc() calls before calling gc() again does not 26 | // reduce memory any more. 27 | for (var i = 0; i < 6; ++i) { 28 | global.gc(); 29 | } 30 | callback(process.memoryUsage().heapUsed); 31 | } 32 | 33 | var server; 34 | if (process.argv.indexOf('use-http-instead-of-cors-anywhere') >= 0) { 35 | server = require('http').createServer(function(req, res) { res.end(); }); 36 | } else { 37 | server = require('../').createServer(); 38 | } 39 | 40 | server.listen(0, function() { 41 | // Perform 1 request to warm up. 42 | require('http').get({ 43 | hostname: '127.0.0.1', 44 | port: server.address().port, 45 | path: '/http://invalid:99999', 46 | agent: false, 47 | }, function() { 48 | notifyParent(); 49 | }); 50 | 51 | function notifyParent() { 52 | getMemoryUsage(function(usage) { 53 | heapUsedStart = usage; 54 | process.send('http://127.0.0.1:' + server.address().port + '/'); 55 | }); 56 | } 57 | }); 58 | 59 | process.once('message', function() { 60 | getMemoryUsage(function(heapUsedEnd) { 61 | var delta = heapUsedEnd - heapUsedStart; 62 | process.send(delta); 63 | }); 64 | }); 65 | -------------------------------------------------------------------------------- /demo.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Demo of CORS Anywhere 6 | 7 | 46 | 47 | 48 |
49 | CORS Anywhere demo • GithubLive server. 50 | 54 | 58 | 61 |
62 |
63 | 64 |
65 | 66 | 106 | 107 | 108 | -------------------------------------------------------------------------------- /lib/rate-limit.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | module.exports = function createRateLimitChecker(CORSANYWHERE_RATELIMIT) { 3 | // Configure rate limit. The following format is accepted for CORSANYWHERE_RATELIMIT: 4 | // 5 | // where is a space-separated list of strings or regexes (/.../) that 6 | // matches the whole host (ports have to be listed explicitly if applicable). 7 | // cannot be zero. 8 | // 9 | // Examples: 10 | // - Allow any origin to make one request per 5 minutes: 11 | // 1 5 12 | // 13 | // - Allow example.com to make an unlimited number of requests, and the others 1 per 5 minutes. 14 | // 1 5 example.com 15 | // 16 | // - Allow example.com, or any subdomain to make any number of requests and block the rest: 17 | // 0 1 /(.*\.)?example\.com/ 18 | // 19 | // - Allow example.com and www.example.com, and block the rest: 20 | // 0 1 example.com www.example.com 21 | var rateLimitConfig = /^(\d+) (\d+)(?:\s*$|\s+(.+)$)/.exec(CORSANYWHERE_RATELIMIT); 22 | if (!rateLimitConfig) { 23 | // No rate limit by default. 24 | return function checkRateLimit() {}; 25 | } 26 | var maxRequestsPerPeriod = parseInt(rateLimitConfig[1]); 27 | var periodInMinutes = parseInt(rateLimitConfig[2]); 28 | var unlimitedPattern = rateLimitConfig[3]; // Will become a RegExp or void. 29 | if (unlimitedPattern) { 30 | var unlimitedPatternParts = []; 31 | unlimitedPattern.trim().split(/\s+/).forEach(function(unlimitedHost, i) { 32 | var startsWithSlash = unlimitedHost.charAt(0) === '/'; 33 | var endsWithSlash = unlimitedHost.slice(-1) === '/'; 34 | if (startsWithSlash || endsWithSlash) { 35 | if (unlimitedHost.length === 1 || !startsWithSlash || !endsWithSlash) { 36 | throw new Error('Invalid CORSANYWHERE_RATELIMIT. Regex at index ' + i + 37 | ' must start and end with a slash ("/").'); 38 | } 39 | unlimitedHost = unlimitedHost.slice(1, -1); 40 | // Throws if the pattern is invalid. 41 | new RegExp(unlimitedHost); 42 | } else { 43 | // Just escape RegExp characters even though they cannot appear in a host name. 44 | // The only actual important escape is the dot. 45 | unlimitedHost = unlimitedHost.replace(/[$()*+.?[\\\]^{|}]/g, '\\$&'); 46 | } 47 | unlimitedPatternParts.push(unlimitedHost); 48 | }); 49 | unlimitedPattern = new RegExp('^(?:' + unlimitedPatternParts.join('|') + ')$', 'i'); 50 | } 51 | 52 | var accessedHosts = Object.create(null); 53 | setInterval(function() { 54 | accessedHosts = Object.create(null); 55 | }, periodInMinutes * 60000); 56 | 57 | var rateLimitMessage = 'The number of requests is limited to ' + maxRequestsPerPeriod + 58 | (periodInMinutes === 1 ? ' per minute' : ' per ' + periodInMinutes + ' minutes') + '. ' + 59 | 'Please self-host CORS Anywhere if you need more quota. ' + 60 | 'See https://github.com/Rob--W/cors-anywhere#demo-server'; 61 | 62 | return function checkRateLimit(origin) { 63 | var host = origin.replace(/^[\w\-]+:\/\//i, ''); 64 | if (unlimitedPattern && unlimitedPattern.test(host)) { 65 | return; 66 | } 67 | var count = accessedHosts[host] || 0; 68 | ++count; 69 | if (count > maxRequestsPerPeriod) { 70 | return rateLimitMessage; 71 | } 72 | accessedHosts[host] = count; 73 | }; 74 | }; 75 | -------------------------------------------------------------------------------- /test/setup.js: -------------------------------------------------------------------------------- 1 | var nock = require('nock'); 2 | if (parseInt(process.versions.node, 10) >= 8) { 3 | // See DEP0066 at https://nodejs.org/api/deprecations.html. 4 | // _headers and _headerNames have been removed from Node v8, which causes 5 | // nock <= 9.0.13 to fail. The snippet below monkey-patches the library, see 6 | // https://github.com/node-nock/nock/pull/929/commits/f6369d0edd2a172024124f 7 | // for the equivalent logic without proxies. 8 | Object.defineProperty(require('http').ClientRequest.prototype, '_headers', { 9 | get: function() { 10 | var request = this; 11 | // eslint-disable-next-line no-undef 12 | return new Proxy(request.getHeaders(), { 13 | set: function(target, property, value) { 14 | request.setHeader(property, value); 15 | return true; 16 | }, 17 | }); 18 | }, 19 | set: function() { 20 | // Ignore. 21 | }, 22 | }); 23 | } 24 | 25 | nock.enableNetConnect('127.0.0.1'); 26 | 27 | function echoheaders(origin) { 28 | nock(origin) 29 | .persist() 30 | .get('/echoheaders') 31 | .reply(function() { 32 | var headers = this.req.headers; 33 | var excluded_headers = [ 34 | 'accept-encoding', 35 | 'user-agent', 36 | 'connection', 37 | // Remove this header since its value is platform-specific. 38 | 'x-forwarded-for', 39 | 'test-include-xfwd', 40 | ]; 41 | if (!('test-include-xfwd' in headers)) { 42 | excluded_headers.push('x-forwarded-port'); 43 | excluded_headers.push('x-forwarded-proto'); 44 | } 45 | var response = {}; 46 | Object.keys(headers).forEach(function(name) { 47 | if (excluded_headers.indexOf(name) === -1) { 48 | response[name] = headers[name]; 49 | } 50 | }); 51 | return response; 52 | }); 53 | } 54 | 55 | nock('http://example.com') 56 | .persist() 57 | .get('/') 58 | .reply(200, 'Response from example.com') 59 | 60 | .post('/echopost') 61 | .reply(200, function(uri, requestBody) { 62 | return requestBody; 63 | }) 64 | 65 | .get('/setcookie') 66 | .reply(200, '', { 67 | 'Set-Cookie': 'x', 68 | 'Set-Cookie2': 'y', 69 | 'Set-Cookie3': 'z', // This is not a special cookie setting header. 70 | }) 71 | 72 | .get('/redirecttarget') 73 | .reply(200, 'redirect target', { 74 | 'Some-header': 'value', 75 | }) 76 | 77 | .head('/redirect') 78 | .reply(302, '', { 79 | Location: '/redirecttarget', 80 | }) 81 | 82 | .get('/redirect') 83 | .reply(302, 'redirecting...', { 84 | 'header at redirect': 'should not be here', 85 | Location: '/redirecttarget', 86 | }) 87 | 88 | .get('/redirectposttarget') 89 | .reply(200, 'post target') 90 | 91 | .post('/redirectposttarget') 92 | .reply(200, 'post target (POST)') 93 | 94 | .post('/redirectpost') 95 | .reply(302, 'redirecting...', { 96 | Location: '/redirectposttarget', 97 | }) 98 | 99 | .post('/redirect307') 100 | .reply(307, 'redirecting...', { 101 | Location: '/redirectposttarget', 102 | }) 103 | 104 | .get('/redirect2redirect') 105 | .reply(302, 'redirecting to redirect...', { 106 | Location: '/redirect', 107 | }) 108 | 109 | .get('/redirectloop') 110 | .reply(302, 'redirecting ad infinitum...', { 111 | Location: '/redirectloop', 112 | }) 113 | 114 | .get('/redirectwithoutlocation') 115 | .reply(302, 'maybe found') 116 | 117 | .get('/redirectinvalidlocation') 118 | .reply(302, 'redirecting to junk...', { 119 | Location: 'http:///', 120 | }) 121 | 122 | .get('/proxyerror') 123 | .replyWithError('throw node') 124 | ; 125 | 126 | nock('https://example.com') 127 | .persist() 128 | .get('/') 129 | .reply(200, 'Response from https://example.com') 130 | ; 131 | 132 | nock('http://example.com.com') 133 | .persist() 134 | .get('/') 135 | .reply(200, 'Response from example.com.com') 136 | ; 137 | 138 | nock('http://example.com:1234') 139 | .persist() 140 | .get('/') 141 | .reply(200, 'Response from example.com:1234') 142 | ; 143 | 144 | nock('http://prefix.example.com') 145 | .persist() 146 | .get('/') 147 | .reply(200, 'Response from prefix.example.com') 148 | ; 149 | 150 | echoheaders('http://example.com'); 151 | echoheaders('http://example.com:1337'); 152 | echoheaders('https://example.com'); 153 | echoheaders('https://example.com:1337'); 154 | 155 | nock('http://robots.txt') 156 | .get('/') 157 | .reply(200, 'this is http://robots.txt'); 158 | -------------------------------------------------------------------------------- /test/test-memory.js: -------------------------------------------------------------------------------- 1 | /* eslint-env mocha */ 2 | // Run this specific test using: 3 | // npm test -- -f memory 4 | var http = require('http'); 5 | var path = require('path'); 6 | var url = require('url'); 7 | var fork = require('child_process').fork; 8 | 9 | describe('memory usage', function() { 10 | var cors_api_url; 11 | 12 | var server; 13 | var cors_anywhere_child; 14 | before(function(done) { 15 | server = http.createServer(function(req, res) { 16 | res.writeHead(200); 17 | res.end(); 18 | }).listen(0, function() { 19 | done(); 20 | }); 21 | }); 22 | 23 | after(function(done) { 24 | server.close(function() { 25 | done(); 26 | }); 27 | }); 28 | 29 | beforeEach(function(done) { 30 | var cors_module_path = path.join(__dirname, 'child'); 31 | var args = []; 32 | // Uncomment this if you want to compare the performance of CORS Anywhere 33 | // with the standard no-op http module. 34 | // args.push('use-http-instead-of-cors-anywhere'); 35 | var nodeOptionsArgs = ['--expose-gc']; 36 | var nodeMajorV = parseInt(process.versions.node, 10); 37 | // Node 11.3.0+, 10.14.0+, 8.14.0+, 6.15.0+ restrict header sizes 38 | // (CVE-2018-12121), and need to be passed the --max-http-header-size flag 39 | // to not reject large headers. 40 | if (nodeMajorV >= 11 || 41 | nodeMajorV === 10 || 42 | nodeMajorV === 8 || 43 | nodeMajorV === 6) { 44 | nodeOptionsArgs.push('--max-http-header-size=60000'); 45 | } 46 | cors_anywhere_child = fork(cors_module_path, args, { 47 | execArgv: nodeOptionsArgs, 48 | }); 49 | cors_anywhere_child.once('message', function(cors_url) { 50 | cors_api_url = cors_url; 51 | done(); 52 | }); 53 | }); 54 | 55 | afterEach(function() { 56 | cors_anywhere_child.kill(); 57 | }); 58 | 59 | /** 60 | * Perform N CORS Anywhere proxy requests to a simple test server. 61 | * 62 | * @param {number} n - number of repetitions. 63 | * @param {number} requestSize - Approximate size of request in kilobytes. 64 | * @param {number} memMax - Expected maximum memory usage in kilobytes. 65 | * @param {function} done - Upon success, called without arguments. 66 | * Upon failure, called with the error as parameter. 67 | */ 68 | function performNRequests(n, requestSize, memMax, done) { 69 | var remaining = n; 70 | var request = url.parse( 71 | cors_api_url + 'http://127.0.0.1:' + server.address().port); 72 | request.agent = false; // Force Connection: Close 73 | request.headers = { 74 | 'Long-header': new Array(requestSize * 1e3).join('x'), 75 | }; 76 | (function requestAgain() { 77 | if (remaining-- === 0) { 78 | cors_anywhere_child.once('message', function(memory_usage_delta) { 79 | console.log('Memory usage delta: ' + memory_usage_delta + 80 | ' (' + n + ' requests of ' + requestSize + ' kb each)'); 81 | if (memory_usage_delta > memMax * 1e3) { 82 | // Note: Even if this error is reached, always profile (e.g. using 83 | // node-inspector) whether it is a true leak, and not e.g. noise 84 | // caused by the implementation of V8/Node.js. 85 | // Uncomment args.push('use-http-instead-of-cors-anywhere') at the 86 | // fork() call to get a sense of what's normal. 87 | throw new Error('Possible memory leak: ' + memory_usage_delta + 88 | ' bytes was not released, which exceeds the ' + memMax + 89 | ' kb limit by ' + 90 | Math.round(memory_usage_delta / memMax / 10 - 100) + '%.'); 91 | } 92 | done(); 93 | }); 94 | cors_anywhere_child.send(null); 95 | return; 96 | } 97 | http.request(request, function() { 98 | requestAgain(); 99 | }).on('error', function(error) { 100 | done(error); 101 | }).end(); 102 | })(); 103 | } 104 | 105 | it('100 GET requests 50k', function(done) { 106 | // This test is just for comparison with the following tests. 107 | performNRequests(100, 50, 1200, done); 108 | }); 109 | 110 | // 100x 1k and 100x 50k for comparison. 111 | // Both should use about the same amount of memory if there is no leak. 112 | it('1000 GET requests 1k', function(done) { 113 | // Every request should complete within 10ms. 114 | this.timeout(1000 * 10); 115 | performNRequests(1000, 1, 2000, done); 116 | }); 117 | it('1000 GET requests 50k', function(done) { 118 | // Every request should complete within 10ms. 119 | this.timeout(1000 * 10); 120 | performNRequests(1000, 50, 2000, done); 121 | }); 122 | }); 123 | -------------------------------------------------------------------------------- /test/test-examples.js: -------------------------------------------------------------------------------- 1 | /** 2 | * CORS Anywhere is designed for use as a standalone server. Sometimes you want 3 | * to have extra functionality on top of the default CORS server. If it may be 4 | * useful to others, please open a feature request on the issue tracker at 5 | * https://github.com/Rob--W/cors-anywhere/issues. 6 | * 7 | * If it is only useful to your application, look below for some examples. 8 | * These examples are provided as-is without guarantees. Use at your own risk. 9 | */ 10 | 11 | /* eslint-env mocha */ 12 | require('./setup'); 13 | 14 | var createServer = require('../').createServer; 15 | var assert = require('assert'); 16 | var request = require('supertest'); 17 | 18 | var http = require('http'); 19 | 20 | describe('Examples', function() { 21 | // Note: In the examples below we don't listen on any port after calling 22 | // createServer() because it is not needed to start listening on a port if the 23 | // CORS Anywhere is only used internally. 24 | 25 | // And normally you have to listen on some port, like this: 26 | // 27 | // http_server.listen(port_number); 28 | // 29 | // But in these test, the call to request() automatically handles that part so 30 | // the examples don't have an explicit .listen() call. 31 | 32 | it('Rewrite proxy URL', function(done) { 33 | var cors_anywhere = createServer(); 34 | 35 | var http_server = http.createServer(function(req, res) { 36 | // For testing, check whether req.url is the same as what we input below. 37 | assert.strictEqual(req.url, '/dummy-for-testing'); 38 | 39 | // Basic example: Always proxy example.com. 40 | req.url = '/http://example.com'; 41 | 42 | cors_anywhere.emit('request', req, res); 43 | }); 44 | 45 | request(http_server) 46 | .get('/dummy-for-testing') 47 | .expect('Access-Control-Allow-Origin', '*') 48 | .expect('x-request-url', 'http://example.com/') 49 | .expect(200, 'Response from example.com', done); 50 | }); 51 | 52 | it('Transform response to uppercase (streaming)', function(done) { 53 | var cors_anywhere = createServer(); 54 | 55 | var http_server = http.createServer(function(req, res) { 56 | var originalWrite = res.write; 57 | 58 | res.write = function(data, encoding, callback) { 59 | if (Buffer.isBuffer(data)) { 60 | data = data.toString(); 61 | } 62 | 63 | assert.strictEqual(typeof data, 'string'); 64 | 65 | // This example shows how to transform the response to upper case. 66 | data = data.toUpperCase(); 67 | 68 | originalWrite.call(this, data, encoding, callback); 69 | }; 70 | 71 | cors_anywhere.emit('request', req, res); 72 | }); 73 | 74 | request(http_server) 75 | .get('/example.com') 76 | .expect('Access-Control-Allow-Origin', '*') 77 | .expect('x-request-url', 'http://example.com/') 78 | .expect(200, 'RESPONSE FROM EXAMPLE.COM', done); 79 | }); 80 | 81 | it('Transform response to uppercase (buffered)', function(done) { 82 | var cors_anywhere = createServer(); 83 | 84 | var http_server = http.createServer(function(req, res) { 85 | var originalWrite = res.write; 86 | var originalEnd = res.end; 87 | 88 | var buffers = []; 89 | 90 | res.write = function(data, encoding, callback) { 91 | assert.ok(Buffer.isBuffer(data) || typeof data === 'string'); 92 | 93 | buffers.push(data); 94 | if (callback) { 95 | process.nextTick(callback, null); 96 | } 97 | }; 98 | res.end = function(data, encoding, callback) { 99 | if (data) { 100 | this.write(data, encoding); 101 | } 102 | 103 | // After calling .end(), .write shouldn't be called any more. So let's 104 | // restore it so that the default error handling for writing to closed 105 | // streams would occur. 106 | this.write = originalWrite; 107 | 108 | // Combine all chunks. Note that we're assuming that all chunks are 109 | // utf8 strings or buffers whose content is utf8-encoded. If this 110 | // assumption is not true, then you have to update the .write method 111 | // above. 112 | data = buffers.join(''); 113 | 114 | // This example shows how to transform the response to upper case. 115 | data = data.toUpperCase(); 116 | 117 | // .end should be called once, so let's restore it so that any default 118 | // error handling occurs if it occurs again. 119 | this.end = originalEnd; 120 | this.end(data, 'utf8', callback); 121 | }; 122 | 123 | cors_anywhere.emit('request', req, res); 124 | }); 125 | 126 | request(http_server) 127 | .get('/example.com') 128 | .expect('Access-Control-Allow-Origin', '*') 129 | .expect('x-request-url', 'http://example.com/') 130 | .expect(200, 'RESPONSE FROM EXAMPLE.COM', done); 131 | }); 132 | }); 133 | 134 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Build Status](https://travis-ci.org/Rob--W/cors-anywhere.svg?branch=master)](https://travis-ci.org/Rob--W/cors-anywhere) 2 | [![Coverage Status](https://coveralls.io/repos/github/Rob--W/cors-anywhere/badge.svg?branch=master)](https://coveralls.io/github/Rob--W/cors-anywhere?branch=master) 3 | 4 | **CORS Anywhere** is a NodeJS proxy which adds CORS headers to the proxied request. 5 | 6 | The url to proxy is literally taken from the path, validated and proxied. The protocol 7 | part of the proxied URI is optional, and defaults to "http". If port 443 is specified, 8 | the protocol defaults to "https". 9 | 10 | This package does not put any restrictions on the http methods or headers, except for 11 | cookies. Requesting [user credentials](http://www.w3.org/TR/cors/#user-credentials) is disallowed. 12 | The app can be configured to require a header for proxying a request, for example to avoid 13 | a direct visit from the browser. 14 | 15 | ## Example 16 | 17 | ```javascript 18 | // Listen on a specific host via the HOST environment variable 19 | var host = process.env.HOST || '0.0.0.0'; 20 | // Listen on a specific port via the PORT environment variable 21 | var port = process.env.PORT || 8080; 22 | 23 | var cors_proxy = require('cors-anywhere'); 24 | cors_proxy.createServer({ 25 | originWhitelist: [], // Allow all origins 26 | requireHeader: ['origin', 'x-requested-with'], 27 | removeHeaders: ['cookie', 'cookie2'] 28 | }).listen(port, host, function() { 29 | console.log('Running CORS Anywhere on ' + host + ':' + port); 30 | }); 31 | 32 | ``` 33 | Request examples: 34 | 35 | * `http://localhost:8080/http://google.com/` - Google.com with CORS headers 36 | * `http://localhost:8080/google.com` - Same as previous. 37 | * `http://localhost:8080/google.com:443` - Proxies `https://google.com/` 38 | * `http://localhost:8080/` - Shows usage text, as defined in `libs/help.txt` 39 | * `http://localhost:8080/favicon.ico` - Replies 404 Not found 40 | 41 | Live examples: 42 | 43 | * https://cors-anywhere.herokuapp.com/ 44 | * https://robwu.nl/cors-anywhere.html - This demo shows how to use the API. 45 | 46 | ## Documentation 47 | 48 | ### Client 49 | 50 | To use the API, just prefix the URL with the API URL. Take a look at [demo.html](demo.html) for an example. 51 | A concise summary of the documentation is provided at [lib/help.txt](lib/help.txt). 52 | 53 | If you want to automatically enable cross-domain requests when needed, use the following snippet: 54 | 55 | ```javascript 56 | (function() { 57 | var cors_api_host = 'cors-anywhere.herokuapp.com'; 58 | var cors_api_url = 'https://' + cors_api_host + '/'; 59 | var slice = [].slice; 60 | var origin = window.location.protocol + '//' + window.location.host; 61 | var open = XMLHttpRequest.prototype.open; 62 | XMLHttpRequest.prototype.open = function() { 63 | var args = slice.call(arguments); 64 | var targetOrigin = /^https?:\/\/([^\/]+)/i.exec(args[1]); 65 | if (targetOrigin && targetOrigin[0].toLowerCase() !== origin && 66 | targetOrigin[1] !== cors_api_host) { 67 | args[1] = cors_api_url + args[1]; 68 | } 69 | return open.apply(this, args); 70 | }; 71 | })(); 72 | ``` 73 | 74 | If you're using jQuery, you can also use the following code **instead of** the previous one: 75 | 76 | ```javascript 77 | jQuery.ajaxPrefilter(function(options) { 78 | if (options.crossDomain && jQuery.support.cors) { 79 | options.url = 'https://cors-anywhere.herokuapp.com/' + options.url; 80 | } 81 | }); 82 | ``` 83 | 84 | ### Server 85 | 86 | The module exports `createServer(options)`, which creates a server that handles 87 | proxy requests. The following options are supported: 88 | 89 | * function `getProxyForUrl` - If set, specifies which intermediate proxy to use for a given URL. 90 | If the return value is void, a direct request is sent. The default implementation is 91 | [`proxy-from-env`](https://github.com/Rob--W/proxy-from-env), which respects the standard proxy 92 | environment variables (e.g. `https_proxy`, `no_proxy`, etc.). 93 | * array of strings `originBlacklist` - If set, requests whose origin is listed are blocked. 94 | Example: `['https://bad.example.com', 'http://bad.example.com']` 95 | * array of strings `originWhitelist` - If set, requests whose origin is not listed are blocked. 96 | If this list is empty, all origins are allowed. 97 | Example: `['https://good.example.com', 'http://good.example.com']` 98 | * function `checkRateLimit` - If set, it is called with the origin (string) of the request. If this 99 | function returns a non-empty string, the request is rejected and the string is send to the client. 100 | * boolean `redirectSameOrigin` - If true, requests to URLs from the same origin will not be proxied but redirected. 101 | The primary purpose for this option is to save server resources by delegating the request to the client 102 | (since same-origin requests should always succeed, even without proxying). 103 | * array of strings `requireHeader` - If set, the request must include this header or the API will refuse to proxy. 104 | Recommended if you want to prevent users from using the proxy for normal browsing. 105 | Example: `['Origin', 'X-Requested-With']`. 106 | * array of lowercase strings `removeHeaders` - Exclude certain headers from being included in the request. 107 | Example: `["cookie"]` 108 | * dictionary of lowercase strings `setHeaders` - Set headers for the request (overwrites existing ones). 109 | Example: `{"x-powered-by": "CORS Anywhere"}` 110 | * number `corsMaxAge` - If set, an Access-Control-Max-Age request header with this value (in seconds) will be added. 111 | Example: `600` - Allow CORS preflight request to be cached by the browser for 10 minutes. 112 | * string `helpFile` - Set the help file (shown at the homepage). 113 | Example: `"myCustomHelpText.txt"` 114 | 115 | For advanced users, the following options are also provided. 116 | 117 | * `httpProxyOptions` - Under the hood, [http-proxy](https://github.com/nodejitsu/node-http-proxy) 118 | is used to proxy requests. Use this option if you really need to pass options 119 | to http-proxy. The documentation for these options can be found [here](https://github.com/nodejitsu/node-http-proxy#options). 120 | * `httpsOptions` - If set, a `https.Server` will be created. The given options are passed to the 121 | [`https.createServer`](https://nodejs.org/api/https.html#https_https_createserver_options_requestlistener) method. 122 | 123 | For even more advanced usage (building upon CORS Anywhere), 124 | see the sample code in [test/test-examples.js](test/test-examples.js). 125 | 126 | ### Demo server 127 | 128 | A public demo of CORS Anywhere is available at https://cors-anywhere.herokuapp.com. This server is 129 | only provided so that you can easily and quickly try out CORS Anywhere. To ensure that the service 130 | stays available to everyone, the number of requests per period is limited, except for requests from 131 | some explicitly whitelisted origins. 132 | 133 | If you expect lots of traffic, please host your own instance of CORS Anywhere, and make sure that 134 | the CORS Anywhere server only whitelists your site to prevent others from using your instance of 135 | CORS Anywhere as an open proxy. 136 | 137 | For instance, to run a CORS Anywhere server that accepts any request from some example.com sites on 138 | port 8080, use: 139 | ``` 140 | export PORT=8080 141 | export CORSANYWHERE_WHITELIST=https://example.com,http://example.com,http://example.com:8080 142 | node server.js 143 | ``` 144 | 145 | This application can immediately be run on Heroku, see https://devcenter.heroku.com/articles/nodejs 146 | for instructions. Note that their [Acceptable Use Policy](https://www.heroku.com/policy/aup) forbids 147 | the use of Heroku for operating an open proxy, so make sure that you either enforce a whitelist as 148 | shown above, or severly rate-limit the number of requests. 149 | 150 | For example, to blacklist abuse.example.com and rate-limit everything to 50 requests per 3 minutes, 151 | except for my.example.com and my2.example.com (which may be unlimited), use: 152 | 153 | ``` 154 | export PORT=8080 155 | export CORSANYWHERE_BLACKLIST=https://abuse.example.com,http://abuse.example.com 156 | export CORSANYWHERE_RATELIMIT='50 3 my.example.com my2.example.com' 157 | node server.js 158 | ``` 159 | 160 | 161 | ## License 162 | 163 | Copyright (C) 2013 - 2016 Rob Wu 164 | 165 | Permission is hereby granted, free of charge, to any person obtaining a copy of 166 | this software and associated documentation files (the "Software"), to deal in 167 | the Software without restriction, including without limitation the rights to 168 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies 169 | of the Software, and to permit persons to whom the Software is furnished to do 170 | so, subject to the following conditions: 171 | 172 | The above copyright notice and this permission notice shall be included in all 173 | copies or substantial portions of the Software. 174 | 175 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 176 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 177 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 178 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 179 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 180 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 181 | SOFTWARE. 182 | -------------------------------------------------------------------------------- /lib/regexp-top-level-domain.js: -------------------------------------------------------------------------------- 1 | // Based on http://data.iana.org/TLD/tlds-alpha-by-domain.txt 2 | // '/\\.(?:' + document.body.firstChild.textContent.trim().split('\n').slice(1).join('|') + ')$/i'; 3 | 4 | /* eslint max-len:0 */ 5 | 6 | // # Version 2017111000, Last Updated Fri Nov 10 07:07:01 2017 UTC 7 | var regexp = /\.(?:AAA|AARP|ABARTH|ABB|ABBOTT|ABBVIE|ABC|ABLE|ABOGADO|ABUDHABI|AC|ACADEMY|ACCENTURE|ACCOUNTANT|ACCOUNTANTS|ACO|ACTIVE|ACTOR|AD|ADAC|ADS|ADULT|AE|AEG|AERO|AETNA|AF|AFAMILYCOMPANY|AFL|AFRICA|AG|AGAKHAN|AGENCY|AI|AIG|AIGO|AIRBUS|AIRFORCE|AIRTEL|AKDN|AL|ALFAROMEO|ALIBABA|ALIPAY|ALLFINANZ|ALLSTATE|ALLY|ALSACE|ALSTOM|AM|AMERICANEXPRESS|AMERICANFAMILY|AMEX|AMFAM|AMICA|AMSTERDAM|ANALYTICS|ANDROID|ANQUAN|ANZ|AO|AOL|APARTMENTS|APP|APPLE|AQ|AQUARELLE|AR|ARAB|ARAMCO|ARCHI|ARMY|ARPA|ART|ARTE|AS|ASDA|ASIA|ASSOCIATES|AT|ATHLETA|ATTORNEY|AU|AUCTION|AUDI|AUDIBLE|AUDIO|AUSPOST|AUTHOR|AUTO|AUTOS|AVIANCA|AW|AWS|AX|AXA|AZ|AZURE|BA|BABY|BAIDU|BANAMEX|BANANAREPUBLIC|BAND|BANK|BAR|BARCELONA|BARCLAYCARD|BARCLAYS|BAREFOOT|BARGAINS|BASEBALL|BASKETBALL|BAUHAUS|BAYERN|BB|BBC|BBT|BBVA|BCG|BCN|BD|BE|BEATS|BEAUTY|BEER|BENTLEY|BERLIN|BEST|BESTBUY|BET|BF|BG|BH|BHARTI|BI|BIBLE|BID|BIKE|BING|BINGO|BIO|BIZ|BJ|BLACK|BLACKFRIDAY|BLANCO|BLOCKBUSTER|BLOG|BLOOMBERG|BLUE|BM|BMS|BMW|BN|BNL|BNPPARIBAS|BO|BOATS|BOEHRINGER|BOFA|BOM|BOND|BOO|BOOK|BOOKING|BOOTS|BOSCH|BOSTIK|BOSTON|BOT|BOUTIQUE|BOX|BR|BRADESCO|BRIDGESTONE|BROADWAY|BROKER|BROTHER|BRUSSELS|BS|BT|BUDAPEST|BUGATTI|BUILD|BUILDERS|BUSINESS|BUY|BUZZ|BV|BW|BY|BZ|BZH|CA|CAB|CAFE|CAL|CALL|CALVINKLEIN|CAM|CAMERA|CAMP|CANCERRESEARCH|CANON|CAPETOWN|CAPITAL|CAPITALONE|CAR|CARAVAN|CARDS|CARE|CAREER|CAREERS|CARS|CARTIER|CASA|CASE|CASEIH|CASH|CASINO|CAT|CATERING|CATHOLIC|CBA|CBN|CBRE|CBS|CC|CD|CEB|CENTER|CEO|CERN|CF|CFA|CFD|CG|CH|CHANEL|CHANNEL|CHASE|CHAT|CHEAP|CHINTAI|CHRISTMAS|CHROME|CHRYSLER|CHURCH|CI|CIPRIANI|CIRCLE|CISCO|CITADEL|CITI|CITIC|CITY|CITYEATS|CK|CL|CLAIMS|CLEANING|CLICK|CLINIC|CLINIQUE|CLOTHING|CLOUD|CLUB|CLUBMED|CM|CN|CO|COACH|CODES|COFFEE|COLLEGE|COLOGNE|COM|COMCAST|COMMBANK|COMMUNITY|COMPANY|COMPARE|COMPUTER|COMSEC|CONDOS|CONSTRUCTION|CONSULTING|CONTACT|CONTRACTORS|COOKING|COOKINGCHANNEL|COOL|COOP|CORSICA|COUNTRY|COUPON|COUPONS|COURSES|CR|CREDIT|CREDITCARD|CREDITUNION|CRICKET|CROWN|CRS|CRUISE|CRUISES|CSC|CU|CUISINELLA|CV|CW|CX|CY|CYMRU|CYOU|CZ|DABUR|DAD|DANCE|DATA|DATE|DATING|DATSUN|DAY|DCLK|DDS|DE|DEAL|DEALER|DEALS|DEGREE|DELIVERY|DELL|DELOITTE|DELTA|DEMOCRAT|DENTAL|DENTIST|DESI|DESIGN|DEV|DHL|DIAMONDS|DIET|DIGITAL|DIRECT|DIRECTORY|DISCOUNT|DISCOVER|DISH|DIY|DJ|DK|DM|DNP|DO|DOCS|DOCTOR|DODGE|DOG|DOHA|DOMAINS|DOT|DOWNLOAD|DRIVE|DTV|DUBAI|DUCK|DUNLOP|DUNS|DUPONT|DURBAN|DVAG|DVR|DZ|EARTH|EAT|EC|ECO|EDEKA|EDU|EDUCATION|EE|EG|EMAIL|EMERCK|ENERGY|ENGINEER|ENGINEERING|ENTERPRISES|EPOST|EPSON|EQUIPMENT|ER|ERICSSON|ERNI|ES|ESQ|ESTATE|ESURANCE|ET|ETISALAT|EU|EUROVISION|EUS|EVENTS|EVERBANK|EXCHANGE|EXPERT|EXPOSED|EXPRESS|EXTRASPACE|FAGE|FAIL|FAIRWINDS|FAITH|FAMILY|FAN|FANS|FARM|FARMERS|FASHION|FAST|FEDEX|FEEDBACK|FERRARI|FERRERO|FI|FIAT|FIDELITY|FIDO|FILM|FINAL|FINANCE|FINANCIAL|FIRE|FIRESTONE|FIRMDALE|FISH|FISHING|FIT|FITNESS|FJ|FK|FLICKR|FLIGHTS|FLIR|FLORIST|FLOWERS|FLY|FM|FO|FOO|FOOD|FOODNETWORK|FOOTBALL|FORD|FOREX|FORSALE|FORUM|FOUNDATION|FOX|FR|FREE|FRESENIUS|FRL|FROGANS|FRONTDOOR|FRONTIER|FTR|FUJITSU|FUJIXEROX|FUN|FUND|FURNITURE|FUTBOL|FYI|GA|GAL|GALLERY|GALLO|GALLUP|GAME|GAMES|GAP|GARDEN|GB|GBIZ|GD|GDN|GE|GEA|GENT|GENTING|GEORGE|GF|GG|GGEE|GH|GI|GIFT|GIFTS|GIVES|GIVING|GL|GLADE|GLASS|GLE|GLOBAL|GLOBO|GM|GMAIL|GMBH|GMO|GMX|GN|GODADDY|GOLD|GOLDPOINT|GOLF|GOO|GOODHANDS|GOODYEAR|GOOG|GOOGLE|GOP|GOT|GOV|GP|GQ|GR|GRAINGER|GRAPHICS|GRATIS|GREEN|GRIPE|GROCERY|GROUP|GS|GT|GU|GUARDIAN|GUCCI|GUGE|GUIDE|GUITARS|GURU|GW|GY|HAIR|HAMBURG|HANGOUT|HAUS|HBO|HDFC|HDFCBANK|HEALTH|HEALTHCARE|HELP|HELSINKI|HERE|HERMES|HGTV|HIPHOP|HISAMITSU|HITACHI|HIV|HK|HKT|HM|HN|HOCKEY|HOLDINGS|HOLIDAY|HOMEDEPOT|HOMEGOODS|HOMES|HOMESENSE|HONDA|HONEYWELL|HORSE|HOSPITAL|HOST|HOSTING|HOT|HOTELES|HOTELS|HOTMAIL|HOUSE|HOW|HR|HSBC|HT|HU|HUGHES|HYATT|HYUNDAI|IBM|ICBC|ICE|ICU|ID|IE|IEEE|IFM|IKANO|IL|IM|IMAMAT|IMDB|IMMO|IMMOBILIEN|IN|INDUSTRIES|INFINITI|INFO|ING|INK|INSTITUTE|INSURANCE|INSURE|INT|INTEL|INTERNATIONAL|INTUIT|INVESTMENTS|IO|IPIRANGA|IQ|IR|IRISH|IS|ISELECT|ISMAILI|IST|ISTANBUL|IT|ITAU|ITV|IVECO|IWC|JAGUAR|JAVA|JCB|JCP|JE|JEEP|JETZT|JEWELRY|JIO|JLC|JLL|JM|JMP|JNJ|JO|JOBS|JOBURG|JOT|JOY|JP|JPMORGAN|JPRS|JUEGOS|JUNIPER|KAUFEN|KDDI|KE|KERRYHOTELS|KERRYLOGISTICS|KERRYPROPERTIES|KFH|KG|KH|KI|KIA|KIM|KINDER|KINDLE|KITCHEN|KIWI|KM|KN|KOELN|KOMATSU|KOSHER|KP|KPMG|KPN|KR|KRD|KRED|KUOKGROUP|KW|KY|KYOTO|KZ|LA|LACAIXA|LADBROKES|LAMBORGHINI|LAMER|LANCASTER|LANCIA|LANCOME|LAND|LANDROVER|LANXESS|LASALLE|LAT|LATINO|LATROBE|LAW|LAWYER|LB|LC|LDS|LEASE|LECLERC|LEFRAK|LEGAL|LEGO|LEXUS|LGBT|LI|LIAISON|LIDL|LIFE|LIFEINSURANCE|LIFESTYLE|LIGHTING|LIKE|LILLY|LIMITED|LIMO|LINCOLN|LINDE|LINK|LIPSY|LIVE|LIVING|LIXIL|LK|LOAN|LOANS|LOCKER|LOCUS|LOFT|LOL|LONDON|LOTTE|LOTTO|LOVE|LPL|LPLFINANCIAL|LR|LS|LT|LTD|LTDA|LU|LUNDBECK|LUPIN|LUXE|LUXURY|LV|LY|MA|MACYS|MADRID|MAIF|MAISON|MAKEUP|MAN|MANAGEMENT|MANGO|MAP|MARKET|MARKETING|MARKETS|MARRIOTT|MARSHALLS|MASERATI|MATTEL|MBA|MC|MCKINSEY|MD|ME|MED|MEDIA|MEET|MELBOURNE|MEME|MEMORIAL|MEN|MENU|MEO|MERCKMSD|METLIFE|MG|MH|MIAMI|MICROSOFT|MIL|MINI|MINT|MIT|MITSUBISHI|MK|ML|MLB|MLS|MM|MMA|MN|MO|MOBI|MOBILE|MOBILY|MODA|MOE|MOI|MOM|MONASH|MONEY|MONSTER|MOPAR|MORMON|MORTGAGE|MOSCOW|MOTO|MOTORCYCLES|MOV|MOVIE|MOVISTAR|MP|MQ|MR|MS|MSD|MT|MTN|MTR|MU|MUSEUM|MUTUAL|MV|MW|MX|MY|MZ|NA|NAB|NADEX|NAGOYA|NAME|NATIONWIDE|NATURA|NAVY|NBA|NC|NE|NEC|NET|NETBANK|NETFLIX|NETWORK|NEUSTAR|NEW|NEWHOLLAND|NEWS|NEXT|NEXTDIRECT|NEXUS|NF|NFL|NG|NGO|NHK|NI|NICO|NIKE|NIKON|NINJA|NISSAN|NISSAY|NL|NO|NOKIA|NORTHWESTERNMUTUAL|NORTON|NOW|NOWRUZ|NOWTV|NP|NR|NRA|NRW|NTT|NU|NYC|NZ|OBI|OBSERVER|OFF|OFFICE|OKINAWA|OLAYAN|OLAYANGROUP|OLDNAVY|OLLO|OM|OMEGA|ONE|ONG|ONL|ONLINE|ONYOURSIDE|OOO|OPEN|ORACLE|ORANGE|ORG|ORGANIC|ORIGINS|OSAKA|OTSUKA|OTT|OVH|PA|PAGE|PANASONIC|PANERAI|PARIS|PARS|PARTNERS|PARTS|PARTY|PASSAGENS|PAY|PCCW|PE|PET|PF|PFIZER|PG|PH|PHARMACY|PHD|PHILIPS|PHONE|PHOTO|PHOTOGRAPHY|PHOTOS|PHYSIO|PIAGET|PICS|PICTET|PICTURES|PID|PIN|PING|PINK|PIONEER|PIZZA|PK|PL|PLACE|PLAY|PLAYSTATION|PLUMBING|PLUS|PM|PN|PNC|POHL|POKER|POLITIE|PORN|POST|PR|PRAMERICA|PRAXI|PRESS|PRIME|PRO|PROD|PRODUCTIONS|PROF|PROGRESSIVE|PROMO|PROPERTIES|PROPERTY|PROTECTION|PRU|PRUDENTIAL|PS|PT|PUB|PW|PWC|PY|QA|QPON|QUEBEC|QUEST|QVC|RACING|RADIO|RAID|RE|READ|REALESTATE|REALTOR|REALTY|RECIPES|RED|REDSTONE|REDUMBRELLA|REHAB|REISE|REISEN|REIT|RELIANCE|REN|RENT|RENTALS|REPAIR|REPORT|REPUBLICAN|REST|RESTAURANT|REVIEW|REVIEWS|REXROTH|RICH|RICHARDLI|RICOH|RIGHTATHOME|RIL|RIO|RIP|RMIT|RO|ROCHER|ROCKS|RODEO|ROGERS|ROOM|RS|RSVP|RU|RUGBY|RUHR|RUN|RW|RWE|RYUKYU|SA|SAARLAND|SAFE|SAFETY|SAKURA|SALE|SALON|SAMSCLUB|SAMSUNG|SANDVIK|SANDVIKCOROMANT|SANOFI|SAP|SAPO|SARL|SAS|SAVE|SAXO|SB|SBI|SBS|SC|SCA|SCB|SCHAEFFLER|SCHMIDT|SCHOLARSHIPS|SCHOOL|SCHULE|SCHWARZ|SCIENCE|SCJOHNSON|SCOR|SCOT|SD|SE|SEARCH|SEAT|SECURE|SECURITY|SEEK|SELECT|SENER|SERVICES|SES|SEVEN|SEW|SEX|SEXY|SFR|SG|SH|SHANGRILA|SHARP|SHAW|SHELL|SHIA|SHIKSHA|SHOES|SHOP|SHOPPING|SHOUJI|SHOW|SHOWTIME|SHRIRAM|SI|SILK|SINA|SINGLES|SITE|SJ|SK|SKI|SKIN|SKY|SKYPE|SL|SLING|SM|SMART|SMILE|SN|SNCF|SO|SOCCER|SOCIAL|SOFTBANK|SOFTWARE|SOHU|SOLAR|SOLUTIONS|SONG|SONY|SOY|SPACE|SPIEGEL|SPOT|SPREADBETTING|SR|SRL|SRT|ST|STADA|STAPLES|STAR|STARHUB|STATEBANK|STATEFARM|STATOIL|STC|STCGROUP|STOCKHOLM|STORAGE|STORE|STREAM|STUDIO|STUDY|STYLE|SU|SUCKS|SUPPLIES|SUPPLY|SUPPORT|SURF|SURGERY|SUZUKI|SV|SWATCH|SWIFTCOVER|SWISS|SX|SY|SYDNEY|SYMANTEC|SYSTEMS|SZ|TAB|TAIPEI|TALK|TAOBAO|TARGET|TATAMOTORS|TATAR|TATTOO|TAX|TAXI|TC|TCI|TD|TDK|TEAM|TECH|TECHNOLOGY|TEL|TELECITY|TELEFONICA|TEMASEK|TENNIS|TEVA|TF|TG|TH|THD|THEATER|THEATRE|TIAA|TICKETS|TIENDA|TIFFANY|TIPS|TIRES|TIROL|TJ|TJMAXX|TJX|TK|TKMAXX|TL|TM|TMALL|TN|TO|TODAY|TOKYO|TOOLS|TOP|TORAY|TOSHIBA|TOTAL|TOURS|TOWN|TOYOTA|TOYS|TR|TRADE|TRADING|TRAINING|TRAVEL|TRAVELCHANNEL|TRAVELERS|TRAVELERSINSURANCE|TRUST|TRV|TT|TUBE|TUI|TUNES|TUSHU|TV|TVS|TW|TZ|UA|UBANK|UBS|UCONNECT|UG|UK|UNICOM|UNIVERSITY|UNO|UOL|UPS|US|UY|UZ|VA|VACATIONS|VANA|VANGUARD|VC|VE|VEGAS|VENTURES|VERISIGN|VERSICHERUNG|VET|VG|VI|VIAJES|VIDEO|VIG|VIKING|VILLAS|VIN|VIP|VIRGIN|VISA|VISION|VISTA|VISTAPRINT|VIVA|VIVO|VLAANDEREN|VN|VODKA|VOLKSWAGEN|VOLVO|VOTE|VOTING|VOTO|VOYAGE|VU|VUELOS|WALES|WALMART|WALTER|WANG|WANGGOU|WARMAN|WATCH|WATCHES|WEATHER|WEATHERCHANNEL|WEBCAM|WEBER|WEBSITE|WED|WEDDING|WEIBO|WEIR|WF|WHOSWHO|WIEN|WIKI|WILLIAMHILL|WIN|WINDOWS|WINE|WINNERS|WME|WOLTERSKLUWER|WOODSIDE|WORK|WORKS|WORLD|WOW|WS|WTC|WTF|XBOX|XEROX|XFINITY|XIHUAN|XIN|XN--11B4C3D|XN--1CK2E1B|XN--1QQW23A|XN--2SCRJ9C|XN--30RR7Y|XN--3BST00M|XN--3DS443G|XN--3E0B707E|XN--3HCRJ9C|XN--3OQ18VL8PN36A|XN--3PXU8K|XN--42C2D9A|XN--45BR5CYL|XN--45BRJ9C|XN--45Q11C|XN--4GBRIM|XN--54B7FTA0CC|XN--55QW42G|XN--55QX5D|XN--5SU34J936BGSG|XN--5TZM5G|XN--6FRZ82G|XN--6QQ986B3XL|XN--80ADXHKS|XN--80AO21A|XN--80AQECDR1A|XN--80ASEHDB|XN--80ASWG|XN--8Y0A063A|XN--90A3AC|XN--90AE|XN--90AIS|XN--9DBQ2A|XN--9ET52U|XN--9KRT00A|XN--B4W605FERD|XN--BCK1B9A5DRE4C|XN--C1AVG|XN--C2BR7G|XN--CCK2B3B|XN--CG4BKI|XN--CLCHC0EA0B2G2A9GCD|XN--CZR694B|XN--CZRS0T|XN--CZRU2D|XN--D1ACJ3B|XN--D1ALF|XN--E1A4C|XN--ECKVDTC9D|XN--EFVY88H|XN--ESTV75G|XN--FCT429K|XN--FHBEI|XN--FIQ228C5HS|XN--FIQ64B|XN--FIQS8S|XN--FIQZ9S|XN--FJQ720A|XN--FLW351E|XN--FPCRJ9C3D|XN--FZC2C9E2C|XN--FZYS8D69UVGM|XN--G2XX48C|XN--GCKR3F0F|XN--GECRJ9C|XN--GK3AT1E|XN--H2BREG3EVE|XN--H2BRJ9C|XN--H2BRJ9C8C|XN--HXT814E|XN--I1B6B1A6A2E|XN--IMR513N|XN--IO0A7I|XN--J1AEF|XN--J1AMH|XN--J6W193G|XN--JLQ61U9W7B|XN--JVR189M|XN--KCRX77D1X4A|XN--KPRW13D|XN--KPRY57D|XN--KPU716F|XN--KPUT3I|XN--L1ACC|XN--LGBBAT1AD8J|XN--MGB9AWBF|XN--MGBA3A3EJT|XN--MGBA3A4F16A|XN--MGBA7C0BBN0A|XN--MGBAAKC7DVF|XN--MGBAAM7A8H|XN--MGBAB2BD|XN--MGBAI9AZGQP6J|XN--MGBAYH7GPA|XN--MGBB9FBPOB|XN--MGBBH1A|XN--MGBBH1A71E|XN--MGBC0A9AZCG|XN--MGBCA7DZDO|XN--MGBERP4A5D4AR|XN--MGBGU82A|XN--MGBI4ECEXP|XN--MGBPL2FH|XN--MGBT3DHD|XN--MGBTX2B|XN--MGBX4CD0AB|XN--MIX891F|XN--MK1BU44C|XN--MXTQ1M|XN--NGBC5AZD|XN--NGBE9E0A|XN--NGBRX|XN--NODE|XN--NQV7F|XN--NQV7FS00EMA|XN--NYQY26A|XN--O3CW4H|XN--OGBPF8FL|XN--P1ACF|XN--P1AI|XN--PBT977C|XN--PGBS0DH|XN--PSSY2U|XN--Q9JYB4C|XN--QCKA1PMC|XN--QXAM|XN--RHQV96G|XN--ROVU88B|XN--RVC1E0AM3E|XN--S9BRJ9C|XN--SES554G|XN--T60B56A|XN--TCKWE|XN--TIQ49XQYJ|XN--UNUP4Y|XN--VERMGENSBERATER-CTB|XN--VERMGENSBERATUNG-PWB|XN--VHQUV|XN--VUQ861B|XN--W4R85EL8FHU5DNRA|XN--W4RS40L|XN--WGBH1C|XN--WGBL6A|XN--XHQ521B|XN--XKC2AL3HYE2A|XN--XKC2DL3A5EE0H|XN--Y9A3AQ|XN--YFRO4I67O|XN--YGBI2AMMX|XN--ZFR164B|XPERIA|XXX|XYZ|YACHTS|YAHOO|YAMAXUN|YANDEX|YE|YODOBASHI|YOGA|YOKOHAMA|YOU|YOUTUBE|YT|YUN|ZA|ZAPPOS|ZARA|ZERO|ZIP|ZIPPO|ZM|ZONE|ZUERICH|ZW)$/i; 8 | module.exports = regexp; 9 | -------------------------------------------------------------------------------- /test/test-ratelimit.js: -------------------------------------------------------------------------------- 1 | /* eslint-env mocha */ 2 | 3 | var createRateLimitChecker = require('../lib/rate-limit'); 4 | 5 | var lolex = require('lolex'); 6 | var assert = require('assert'); 7 | 8 | function assertNotLimited(rateLimitReturnValue) { 9 | if (rateLimitReturnValue) { 10 | assert.fail('Expected no limit, but got ' + rateLimitReturnValue); 11 | } 12 | } 13 | 14 | function assertLimited(rateLimitReturnValue, limit, period) { 15 | var msg; 16 | if (period === 1) { 17 | msg = 'The number of requests is limited to ' + limit + ' per minute. '; 18 | } else { 19 | msg = 'The number of requests is limited to ' + limit + ' per ' + period + ' minutes. '; 20 | } 21 | msg += 'Please self-host CORS Anywhere if you need more quota. ' + 22 | 'See https://github.com/Rob--W/cors-anywhere#demo-server'; 23 | assert.equal(rateLimitReturnValue, msg); 24 | } 25 | 26 | describe('Rate limit', function() { 27 | var clock; 28 | beforeEach(function() { 29 | clock = lolex.install(); 30 | }); 31 | afterEach(function() { 32 | clock.uninstall(); 33 | }); 34 | it('is unlimited by default', function() { 35 | var checkRateLimit = createRateLimitChecker(); 36 | assertNotLimited(checkRateLimit('http://example.com')); 37 | assertNotLimited(checkRateLimit('https://example.com')); 38 | assertNotLimited(checkRateLimit('https://example.com:1234')); 39 | 40 | checkRateLimit = createRateLimitChecker(''); 41 | assertNotLimited(checkRateLimit('http://example.com')); 42 | 43 | checkRateLimit = createRateLimitChecker(' '); 44 | assertNotLimited(checkRateLimit('http://example.com')); 45 | }); 46 | 47 | it('zero per minute / 5 minutes', function() { 48 | var checkRateLimit = createRateLimitChecker('0 1'); 49 | assertLimited(checkRateLimit('http://example.com'), 0, 1); 50 | assertLimited(checkRateLimit('https://example.com'), 0, 1); 51 | 52 | checkRateLimit = createRateLimitChecker('0 5'); 53 | assertLimited(checkRateLimit('http://example.com'), 0, 5); 54 | assertLimited(checkRateLimit('https://example.com'), 0, 5); 55 | }); 56 | 57 | it('one per minute', function() { 58 | var checkRateLimit = createRateLimitChecker('1 1'); 59 | assertNotLimited(checkRateLimit('http://example.com')); 60 | assertLimited(checkRateLimit('http://example.com'), 1, 1); 61 | assertNotLimited(checkRateLimit('http://example.com:1234')); 62 | assertLimited(checkRateLimit('http://example.com:1234'), 1, 1); 63 | 64 | clock.tick(59000); 65 | assertLimited(checkRateLimit('http://example.com'), 1, 1); 66 | 67 | clock.tick(1000); 68 | assertNotLimited(checkRateLimit('http://example.com')); 69 | assertLimited(checkRateLimit('http://example.com'), 1, 1); 70 | assertNotLimited(checkRateLimit('http://example.com:1234')); 71 | assertLimited(checkRateLimit('http://example.com:1234'), 1, 1); 72 | }); 73 | 74 | it('different domains, one per minute', function() { 75 | var checkRateLimit = createRateLimitChecker('1 1'); 76 | assertNotLimited(checkRateLimit('http://example.com')); 77 | assertNotLimited(checkRateLimit('http://example.net')); 78 | assertNotLimited(checkRateLimit('http://wexample.net')); 79 | assertNotLimited(checkRateLimit('http://xample.net')); 80 | assertNotLimited(checkRateLimit('http://www.example.net')); 81 | assertLimited(checkRateLimit('http://example.com'), 1, 1); 82 | assertLimited(checkRateLimit('http://example.net'), 1, 1); 83 | assertLimited(checkRateLimit('http://wexample.net'), 1, 1); 84 | assertLimited(checkRateLimit('http://xample.net'), 1, 1); 85 | assertLimited(checkRateLimit('http://www.example.net'), 1, 1); 86 | 87 | clock.tick(60000); // 1 minute 88 | assertNotLimited(checkRateLimit('http://example.com')); 89 | assertNotLimited(checkRateLimit('http://example.net')); 90 | assertNotLimited(checkRateLimit('http://wexample.net')); 91 | assertNotLimited(checkRateLimit('http://xample.net')); 92 | assertNotLimited(checkRateLimit('http://www.example.net')); 93 | }); 94 | 95 | it('unlimited domains, string', function() { 96 | var checkRateLimit = createRateLimitChecker('1 2 example.com'); 97 | assertNotLimited(checkRateLimit('http://example.com')); 98 | assertNotLimited(checkRateLimit('http://example.com')); 99 | 100 | assertNotLimited(checkRateLimit('http://wexample.com')); 101 | assertNotLimited(checkRateLimit('http://xample.com')); 102 | assertNotLimited(checkRateLimit('http://www.example.com')); 103 | assertLimited(checkRateLimit('http://wexample.com'), 1, 2); 104 | assertLimited(checkRateLimit('http://xample.com'), 1, 2); 105 | assertLimited(checkRateLimit('http://www.example.com'), 1, 2); 106 | }); 107 | 108 | it('unlimited domains, RegExp', function() { 109 | var checkRateLimit = createRateLimitChecker('1 2 /example\\.com/'); 110 | assertNotLimited(checkRateLimit('http://example.com')); 111 | assertNotLimited(checkRateLimit('http://example.com')); 112 | 113 | assertNotLimited(checkRateLimit('http://wexample.com')); 114 | assertNotLimited(checkRateLimit('http://xample.com')); 115 | assertNotLimited(checkRateLimit('http://www.example.com')); 116 | assertLimited(checkRateLimit('http://wexample.com'), 1, 2); 117 | assertLimited(checkRateLimit('http://xample.com'), 1, 2); 118 | assertLimited(checkRateLimit('http://www.example.com'), 1, 2); 119 | }); 120 | 121 | it('multiple domains, string', function() { 122 | var checkRateLimit = createRateLimitChecker('1 2 a b cc '); 123 | assertNotLimited(checkRateLimit('http://a')); 124 | assertNotLimited(checkRateLimit('http://a')); 125 | assertNotLimited(checkRateLimit('http://b')); 126 | assertNotLimited(checkRateLimit('http://b')); 127 | assertNotLimited(checkRateLimit('http://cc')); 128 | assertNotLimited(checkRateLimit('http://cc')); 129 | assertNotLimited(checkRateLimit('http://c')); 130 | assertLimited(checkRateLimit('http://c'), 1, 2); 131 | }); 132 | 133 | it('multiple domains, RegExp', function() { 134 | var checkRateLimit = createRateLimitChecker('1 2 /a/ /b/ /cc/ '); 135 | assertNotLimited(checkRateLimit('http://a')); 136 | assertNotLimited(checkRateLimit('http://a')); 137 | assertNotLimited(checkRateLimit('http://b')); 138 | assertNotLimited(checkRateLimit('http://b')); 139 | assertNotLimited(checkRateLimit('http://cc')); 140 | assertNotLimited(checkRateLimit('http://cc')); 141 | assertNotLimited(checkRateLimit('http://ccc')); 142 | assertLimited(checkRateLimit('http://ccc'), 1, 2); 143 | }); 144 | 145 | it('multiple domains, string and RegExp', function() { 146 | var checkRateLimit = createRateLimitChecker('1 2 a /b/'); 147 | assertNotLimited(checkRateLimit('http://a')); 148 | assertNotLimited(checkRateLimit('http://a')); 149 | assertNotLimited(checkRateLimit('http://b')); 150 | assertNotLimited(checkRateLimit('http://b')); 151 | assertNotLimited(checkRateLimit('http://ab')); 152 | assertLimited(checkRateLimit('http://ab'), 1, 2); 153 | }); 154 | 155 | it('multiple domains, RegExp and string', function() { 156 | var checkRateLimit = createRateLimitChecker('1 2 /a/ b'); 157 | assertNotLimited(checkRateLimit('http://a')); 158 | assertNotLimited(checkRateLimit('http://a')); 159 | assertNotLimited(checkRateLimit('http://b')); 160 | assertNotLimited(checkRateLimit('http://b')); 161 | assertNotLimited(checkRateLimit('http://ab')); 162 | assertLimited(checkRateLimit('http://ab'), 1, 2); 163 | }); 164 | 165 | it('wildcard subdomains', function() { 166 | var checkRateLimit = createRateLimitChecker('0 1 /(.*\\.)?example\\.com/'); 167 | assertNotLimited(checkRateLimit('http://example.com')); 168 | assertNotLimited(checkRateLimit('http://www.example.com')); 169 | assertLimited(checkRateLimit('http://xexample.com'), 0, 1); 170 | assertLimited(checkRateLimit('http://example.com.br'), 0, 1); 171 | }); 172 | 173 | it('wildcard ports', function() { 174 | var checkRateLimit = createRateLimitChecker('0 1 /example\\.com(:\\d{1,5})?/'); 175 | assertNotLimited(checkRateLimit('http://example.com')); 176 | assertNotLimited(checkRateLimit('http://example.com:1234')); 177 | }); 178 | 179 | it('empty host', function() { 180 | var checkRateLimit = createRateLimitChecker('0 1'); 181 | assertLimited(checkRateLimit(''), 0, 1); 182 | // Empty host actually means empty origin. But let's also test for 'http://'. 183 | assertLimited(checkRateLimit('http://'), 0, 1); 184 | 185 | checkRateLimit = createRateLimitChecker('0 1 '); 186 | assertLimited(checkRateLimit(''), 0, 1); 187 | assertLimited(checkRateLimit('http://'), 0, 1); 188 | 189 | checkRateLimit = createRateLimitChecker('0 1 //'); 190 | assertNotLimited(checkRateLimit('')); 191 | assertNotLimited(checkRateLimit('http://')); 192 | }); 193 | 194 | it('null origin', function() { 195 | var checkRateLimit = createRateLimitChecker('0 1'); 196 | assertLimited(checkRateLimit('null'), 0, 1); 197 | assertLimited(checkRateLimit('http://null'), 0, 1); 198 | 199 | checkRateLimit = createRateLimitChecker('0 1 null'); 200 | assertNotLimited(checkRateLimit('null')); 201 | assertNotLimited(checkRateLimit('http://null')); 202 | 203 | checkRateLimit = createRateLimitChecker('0 1 /null/'); 204 | assertNotLimited(checkRateLimit('null')); 205 | assertNotLimited(checkRateLimit('http://null')); 206 | }); 207 | 208 | it('case-insensitive', function() { 209 | var checkRateLimit = createRateLimitChecker('0 1 NULL'); 210 | assertNotLimited(checkRateLimit('null')); 211 | assertNotLimited(checkRateLimit('http://null')); 212 | 213 | checkRateLimit = createRateLimitChecker('0 1 /NULL/'); 214 | assertNotLimited(checkRateLimit('null')); 215 | assertNotLimited(checkRateLimit('http://null')); 216 | }); 217 | 218 | it('bad input', function() { 219 | assert.throws(function() { 220 | createRateLimitChecker('0 1 /'); 221 | }, /Invalid CORSANYWHERE_RATELIMIT\. Regex at index 0 must start and end with a slash \("\/"\)\./); 222 | 223 | assert.throws(function() { 224 | createRateLimitChecker('0 1 a /'); 225 | }, /Invalid CORSANYWHERE_RATELIMIT\. Regex at index 1 must start and end with a slash \("\/"\)\./); 226 | 227 | assert.throws(function() { 228 | createRateLimitChecker('0 1 /(/'); 229 | }, /Invalid regular expression/); 230 | }); 231 | }); 232 | -------------------------------------------------------------------------------- /lib/cors-anywhere.js: -------------------------------------------------------------------------------- 1 | // © 2013 - 2016 Rob Wu 2 | // Released under the MIT license 3 | 4 | 'use strict'; 5 | 6 | var httpProxy = require('http-proxy'); 7 | var net = require('net'); 8 | var url = require('url'); 9 | var regexp_tld = require('./regexp-top-level-domain'); 10 | var getProxyForUrl = require('proxy-from-env').getProxyForUrl; 11 | 12 | var help_text = {}; 13 | function showUsage(help_file, headers, response) { 14 | var isHtml = /\.html$/.test(help_file); 15 | headers['content-type'] = isHtml ? 'text/html' : 'text/plain'; 16 | if (help_text[help_file] != null) { 17 | response.writeHead(200, headers); 18 | response.end(help_text[help_file]); 19 | } else { 20 | require('fs').readFile(help_file, 'utf8', function(err, data) { 21 | if (err) { 22 | console.error(err); 23 | response.writeHead(500, headers); 24 | response.end(); 25 | } else { 26 | help_text[help_file] = data; 27 | showUsage(help_file, headers, response); // Recursive call, but since data is a string, the recursion will end 28 | } 29 | }); 30 | } 31 | } 32 | 33 | /** 34 | * Check whether the specified hostname is valid. 35 | * 36 | * @param hostname {string} Host name (excluding port) of requested resource. 37 | * @return {boolean} Whether the requested resource can be accessed. 38 | */ 39 | function isValidHostName(hostname) { 40 | return !!( 41 | regexp_tld.test(hostname) || 42 | net.isIPv4(hostname) || 43 | net.isIPv6(hostname) 44 | ); 45 | } 46 | 47 | /** 48 | * Adds CORS headers to the response headers. 49 | * 50 | * @param headers {object} Response headers 51 | * @param request {ServerRequest} 52 | */ 53 | function withCORS(headers, request) { 54 | headers['access-control-allow-origin'] = '*'; 55 | var corsMaxAge = request.corsAnywhereRequestState.corsMaxAge; 56 | if (corsMaxAge) { 57 | headers['access-control-max-age'] = corsMaxAge; 58 | } 59 | if (request.headers['access-control-request-method']) { 60 | headers['access-control-allow-methods'] = request.headers['access-control-request-method']; 61 | delete request.headers['access-control-request-method']; 62 | } 63 | if (request.headers['access-control-request-headers']) { 64 | headers['access-control-allow-headers'] = request.headers['access-control-request-headers']; 65 | delete request.headers['access-control-request-headers']; 66 | } 67 | 68 | headers['access-control-expose-headers'] = Object.keys(headers).join(','); 69 | 70 | return headers; 71 | } 72 | 73 | /** 74 | * Performs the actual proxy request. 75 | * 76 | * @param req {ServerRequest} Incoming http request 77 | * @param res {ServerResponse} Outgoing (proxied) http request 78 | * @param proxy {HttpProxy} 79 | */ 80 | function proxyRequest(req, res, proxy) { 81 | var location = req.corsAnywhereRequestState.location; 82 | req.url = location.path; 83 | 84 | var proxyOptions = { 85 | changeOrigin: false, 86 | prependPath: false, 87 | target: location, 88 | headers: { 89 | host: location.host, 90 | }, 91 | // HACK: Get hold of the proxyReq object, because we need it later. 92 | // https://github.com/nodejitsu/node-http-proxy/blob/v1.11.1/lib/http-proxy/passes/web-incoming.js#L144 93 | buffer: { 94 | pipe: function(proxyReq) { 95 | var proxyReqOn = proxyReq.on; 96 | // Intercepts the handler that connects proxyRes to res. 97 | // https://github.com/nodejitsu/node-http-proxy/blob/v1.11.1/lib/http-proxy/passes/web-incoming.js#L146-L158 98 | proxyReq.on = function(eventName, listener) { 99 | if (eventName !== 'response') { 100 | return proxyReqOn.call(this, eventName, listener); 101 | } 102 | return proxyReqOn.call(this, 'response', function(proxyRes) { 103 | if (onProxyResponse(proxy, proxyReq, proxyRes, req, res)) { 104 | try { 105 | listener(proxyRes); 106 | } catch (err) { 107 | // Wrap in try-catch because an error could occur: 108 | // "RangeError: Invalid status code: 0" 109 | // https://github.com/Rob--W/cors-anywhere/issues/95 110 | // https://github.com/nodejitsu/node-http-proxy/issues/1080 111 | 112 | // Forward error (will ultimately emit the 'error' event on our proxy object): 113 | // https://github.com/nodejitsu/node-http-proxy/blob/v1.11.1/lib/http-proxy/passes/web-incoming.js#L134 114 | proxyReq.emit('error', err); 115 | } 116 | } 117 | }); 118 | }; 119 | return req.pipe(proxyReq); 120 | }, 121 | }, 122 | }; 123 | 124 | var proxyThroughUrl = req.corsAnywhereRequestState.getProxyForUrl(location.href); 125 | if (proxyThroughUrl) { 126 | proxyOptions.target = proxyThroughUrl; 127 | proxyOptions.toProxy = true; 128 | // If a proxy URL was set, req.url must be an absolute URL. Then the request will not be sent 129 | // directly to the proxied URL, but through another proxy. 130 | req.url = location.href; 131 | } 132 | 133 | // Start proxying the request 134 | try { 135 | proxy.web(req, res, proxyOptions); 136 | } catch (err) { 137 | proxy.emit('error', err, req, res); 138 | } 139 | } 140 | 141 | /** 142 | * This method modifies the response headers of the proxied response. 143 | * If a redirect is detected, the response is not sent to the client, 144 | * and a new request is initiated. 145 | * 146 | * client (req) -> CORS Anywhere -> (proxyReq) -> other server 147 | * client (res) <- CORS Anywhere <- (proxyRes) <- other server 148 | * 149 | * @param proxy {HttpProxy} 150 | * @param proxyReq {ClientRequest} The outgoing request to the other server. 151 | * @param proxyRes {ServerResponse} The response from the other server. 152 | * @param req {IncomingMessage} Incoming HTTP request, augmented with property corsAnywhereRequestState 153 | * @param req.corsAnywhereRequestState {object} 154 | * @param req.corsAnywhereRequestState.location {object} See parseURL 155 | * @param req.corsAnywhereRequestState.getProxyForUrl {function} See proxyRequest 156 | * @param req.corsAnywhereRequestState.proxyBaseUrl {string} Base URL of the CORS API endpoint 157 | * @param req.corsAnywhereRequestState.maxRedirects {number} Maximum number of redirects 158 | * @param req.corsAnywhereRequestState.redirectCount_ {number} Internally used to count redirects 159 | * @param res {ServerResponse} Outgoing response to the client that wanted to proxy the HTTP request. 160 | * 161 | * @returns {boolean} true if http-proxy should continue to pipe proxyRes to res. 162 | */ 163 | function onProxyResponse(proxy, proxyReq, proxyRes, req, res) { 164 | var requestState = req.corsAnywhereRequestState; 165 | 166 | var statusCode = proxyRes.statusCode; 167 | 168 | if (!requestState.redirectCount_) { 169 | res.setHeader('x-request-url', requestState.location.href); 170 | } 171 | // Handle redirects 172 | if (statusCode === 301 || statusCode === 302 || statusCode === 303 || statusCode === 307 || statusCode === 308) { 173 | var locationHeader = proxyRes.headers.location; 174 | var parsedLocation; 175 | if (locationHeader) { 176 | locationHeader = url.resolve(requestState.location.href, locationHeader); 177 | parsedLocation = parseURL(locationHeader); 178 | } 179 | if (parsedLocation) { 180 | if (statusCode === 301 || statusCode === 302 || statusCode === 303) { 181 | // Exclude 307 & 308, because they are rare, and require preserving the method + request body 182 | requestState.redirectCount_ = requestState.redirectCount_ + 1 || 1; 183 | if (requestState.redirectCount_ <= requestState.maxRedirects) { 184 | // Handle redirects within the server, because some clients (e.g. Android Stock Browser) 185 | // cancel redirects. 186 | // Set header for debugging purposes. Do not try to parse it! 187 | res.setHeader('X-CORS-Redirect-' + requestState.redirectCount_, statusCode + ' ' + locationHeader); 188 | 189 | req.method = 'GET'; 190 | req.headers['content-length'] = '0'; 191 | delete req.headers['content-type']; 192 | requestState.location = parsedLocation; 193 | 194 | // Remove all listeners (=reset events to initial state) 195 | req.removeAllListeners(); 196 | 197 | // Remove the error listener so that the ECONNRESET "error" that 198 | // may occur after aborting a request does not propagate to res. 199 | // https://github.com/nodejitsu/node-http-proxy/blob/v1.11.1/lib/http-proxy/passes/web-incoming.js#L134 200 | proxyReq.removeAllListeners('error'); 201 | proxyReq.once('error', function catchAndIgnoreError() {}); 202 | proxyReq.abort(); 203 | 204 | // Initiate a new proxy request. 205 | proxyRequest(req, res, proxy); 206 | return false; 207 | } 208 | } 209 | proxyRes.headers.location = requestState.proxyBaseUrl + '/' + locationHeader; 210 | } 211 | } 212 | 213 | // Strip cookies 214 | delete proxyRes.headers['set-cookie']; 215 | delete proxyRes.headers['set-cookie2']; 216 | 217 | proxyRes.headers['x-final-url'] = requestState.location.href; 218 | withCORS(proxyRes.headers, req); 219 | return true; 220 | } 221 | 222 | 223 | /** 224 | * @param req_url {string} The requested URL (scheme is optional). 225 | * @return {object} URL parsed using url.parse 226 | */ 227 | function parseURL(req_url) { 228 | var match = req_url.match(/^(?:(https?:)?\/\/)?(([^\/?]+?)(?::(\d{0,5})(?=[\/?]|$))?)([\/?][\S\s]*|$)/i); 229 | // ^^^^^^^ ^^^^^^^^ ^^^^^^^ ^^^^^^^^^^^^ 230 | // 1:protocol 3:hostname 4:port 5:path + query string 231 | // ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 232 | // 2:host 233 | if (!match) { 234 | return null; 235 | } 236 | if (!match[1]) { 237 | if (/^https?:/i.test(req_url)) { 238 | // The pattern at top could mistakenly parse "http:///" as host="http:" and path=///. 239 | return null; 240 | } 241 | // Scheme is omitted. 242 | if (req_url.lastIndexOf('//', 0) === -1) { 243 | // "//" is omitted. 244 | req_url = '//' + req_url; 245 | } 246 | req_url = (match[4] === '443' ? 'https:' : 'http:') + req_url; 247 | } 248 | var parsed = url.parse(req_url); 249 | if (!parsed.hostname) { 250 | // "http://:1/" and "http:/notenoughslashes" could end up here. 251 | return null; 252 | } 253 | return parsed; 254 | } 255 | 256 | // Request handler factory 257 | function getHandler(options, proxy) { 258 | var corsAnywhere = { 259 | getProxyForUrl: getProxyForUrl, // Function that specifies the proxy to use 260 | maxRedirects: 5, // Maximum number of redirects to be followed. 261 | originBlacklist: [], // Requests from these origins will be blocked. 262 | originWhitelist: [], // If non-empty, requests not from an origin in this list will be blocked. 263 | checkRateLimit: null, // Function that may enforce a rate-limit by returning a non-empty string. 264 | redirectSameOrigin: false, // Redirect the client to the requested URL for same-origin requests. 265 | requireHeader: null, // Require a header to be set? 266 | removeHeaders: [], // Strip these request headers. 267 | setHeaders: {}, // Set these request headers. 268 | corsMaxAge: 0, // If set, an Access-Control-Max-Age header with this value (in seconds) will be added. 269 | helpFile: __dirname + '/help.txt', 270 | }; 271 | 272 | Object.keys(corsAnywhere).forEach(function(option) { 273 | if (Object.prototype.hasOwnProperty.call(options, option)) { 274 | corsAnywhere[option] = options[option]; 275 | } 276 | }); 277 | 278 | // Convert corsAnywhere.requireHeader to an array of lowercase header names, or null. 279 | if (corsAnywhere.requireHeader) { 280 | if (typeof corsAnywhere.requireHeader === 'string') { 281 | corsAnywhere.requireHeader = [corsAnywhere.requireHeader.toLowerCase()]; 282 | } else if (!Array.isArray(corsAnywhere.requireHeader) || corsAnywhere.requireHeader.length === 0) { 283 | corsAnywhere.requireHeader = null; 284 | } else { 285 | corsAnywhere.requireHeader = corsAnywhere.requireHeader.map(function(headerName) { 286 | return headerName.toLowerCase(); 287 | }); 288 | } 289 | } 290 | var hasRequiredHeaders = function(headers) { 291 | return !corsAnywhere.requireHeader || corsAnywhere.requireHeader.some(function(headerName) { 292 | return Object.hasOwnProperty.call(headers, headerName); 293 | }); 294 | }; 295 | 296 | return function(req, res) { 297 | req.corsAnywhereRequestState = { 298 | getProxyForUrl: corsAnywhere.getProxyForUrl, 299 | maxRedirects: corsAnywhere.maxRedirects, 300 | corsMaxAge: corsAnywhere.corsMaxAge, 301 | }; 302 | 303 | var cors_headers = withCORS({}, req); 304 | if (req.method === 'OPTIONS') { 305 | // Pre-flight request. Reply successfully: 306 | res.writeHead(200, cors_headers); 307 | res.end(); 308 | return; 309 | } 310 | 311 | var location = parseURL(req.url.slice(1)); 312 | 313 | if (!location) { 314 | // Invalid API call. Show how to correctly use the API 315 | showUsage(corsAnywhere.helpFile, cors_headers, res); 316 | return; 317 | } 318 | 319 | if (location.host === 'iscorsneeded') { 320 | // Is CORS needed? This path is provided so that API consumers can test whether it's necessary 321 | // to use CORS. The server's reply is always No, because if they can read it, then CORS headers 322 | // are not necessary. 323 | res.writeHead(200, {'Content-Type': 'text/plain'}); 324 | res.end('no'); 325 | return; 326 | } 327 | 328 | if (location.port > 65535) { 329 | // Port is higher than 65535 330 | res.writeHead(400, 'Invalid port', cors_headers); 331 | res.end('Port number too large: ' + location.port); 332 | return; 333 | } 334 | 335 | if (!/^\/https?:/.test(req.url) && !isValidHostName(location.hostname)) { 336 | // Don't even try to proxy invalid hosts (such as /favicon.ico, /robots.txt) 337 | res.writeHead(404, 'Invalid host', cors_headers); 338 | res.end('Invalid host: ' + location.hostname); 339 | return; 340 | } 341 | 342 | if (!hasRequiredHeaders(req.headers)) { 343 | res.writeHead(400, 'Header required', cors_headers); 344 | res.end('Missing required request header. Must specify one of: ' + corsAnywhere.requireHeader); 345 | return; 346 | } 347 | 348 | var origin = req.headers.origin || ''; 349 | if (corsAnywhere.originBlacklist.indexOf(origin) >= 0) { 350 | res.writeHead(403, 'Forbidden', cors_headers); 351 | res.end('The origin "' + origin + '" was blacklisted by the operator of this proxy.'); 352 | return; 353 | } 354 | 355 | if (corsAnywhere.originWhitelist.length && corsAnywhere.originWhitelist.indexOf(origin) === -1) { 356 | res.writeHead(403, 'Forbidden', cors_headers); 357 | res.end('The origin "' + origin + '" was not whitelisted by the operator of this proxy.'); 358 | return; 359 | } 360 | 361 | var rateLimitMessage = corsAnywhere.checkRateLimit && corsAnywhere.checkRateLimit(origin); 362 | if (rateLimitMessage) { 363 | res.writeHead(429, 'Too Many Requests', cors_headers); 364 | res.end('The origin "' + origin + '" has sent too many requests.\n' + rateLimitMessage); 365 | return; 366 | } 367 | 368 | if (corsAnywhere.redirectSameOrigin && origin && location.href[origin.length] === '/' && 369 | location.href.lastIndexOf(origin, 0) === 0) { 370 | // Send a permanent redirect to offload the server. Badly coded clients should not waste our resources. 371 | cors_headers.vary = 'origin'; 372 | cors_headers['cache-control'] = 'private'; 373 | cors_headers.location = location.href; 374 | res.writeHead(301, 'Please use a direct request', cors_headers); 375 | res.end(); 376 | return; 377 | } 378 | 379 | var isRequestedOverHttps = req.connection.encrypted || /^\s*https/.test(req.headers['x-forwarded-proto']); 380 | var proxyBaseUrl = (isRequestedOverHttps ? 'https://' : 'http://') + req.headers.host; 381 | 382 | corsAnywhere.removeHeaders.forEach(function(header) { 383 | delete req.headers[header]; 384 | }); 385 | 386 | Object.keys(corsAnywhere.setHeaders).forEach(function(header) { 387 | req.headers[header] = corsAnywhere.setHeaders[header]; 388 | }); 389 | 390 | req.corsAnywhereRequestState.location = location; 391 | req.corsAnywhereRequestState.proxyBaseUrl = proxyBaseUrl; 392 | 393 | proxyRequest(req, res, proxy); 394 | }; 395 | } 396 | 397 | // Create server with default and given values 398 | // Creator still needs to call .listen() 399 | exports.createServer = function createServer(options) { 400 | options = options || {}; 401 | 402 | // Default options: 403 | var httpProxyOptions = { 404 | xfwd: true, // Append X-Forwarded-* headers 405 | }; 406 | // Allow user to override defaults and add own options 407 | if (options.httpProxyOptions) { 408 | Object.keys(options.httpProxyOptions).forEach(function(option) { 409 | httpProxyOptions[option] = options.httpProxyOptions[option]; 410 | }); 411 | } 412 | 413 | var proxy = httpProxy.createServer(httpProxyOptions); 414 | var requestHandler = getHandler(options, proxy); 415 | var server; 416 | if (options.httpsOptions) { 417 | server = require('https').createServer(options.httpsOptions, requestHandler); 418 | } else { 419 | server = require('http').createServer(requestHandler); 420 | } 421 | 422 | // When the server fails, just show a 404 instead of Internal server error 423 | proxy.on('error', function(err, req, res) { 424 | if (res.headersSent) { 425 | // This could happen when a protocol error occurs when an error occurs 426 | // after the headers have been received (and forwarded). Do not write 427 | // the headers because it would generate an error. 428 | // Prior to Node 13.x, the stream would have ended. 429 | // As of Node 13.x, we must explicitly close it. 430 | if (res.writableEnded === false) { 431 | res.end(); 432 | } 433 | return; 434 | } 435 | 436 | // When the error occurs after setting headers but before writing the response, 437 | // then any previously set headers must be removed. 438 | var headerNames = res.getHeaderNames ? res.getHeaderNames() : Object.keys(res._headers || {}); 439 | headerNames.forEach(function(name) { 440 | res.removeHeader(name); 441 | }); 442 | 443 | res.writeHead(404, {'Access-Control-Allow-Origin': '*'}); 444 | res.end('Not found because of proxy error: ' + err); 445 | }); 446 | 447 | return server; 448 | }; 449 | -------------------------------------------------------------------------------- /test/test.js: -------------------------------------------------------------------------------- 1 | /* eslint-env mocha */ 2 | require('./setup'); 3 | 4 | var createServer = require('../').createServer; 5 | var request = require('supertest'); 6 | var path = require('path'); 7 | var http = require('http'); 8 | var fs = require('fs'); 9 | var assert = require('assert'); 10 | 11 | var helpTextPath = path.join(__dirname, '../lib/help.txt'); 12 | var helpText = fs.readFileSync(helpTextPath, {encoding: 'utf8'}); 13 | 14 | request.Test.prototype.expectJSON = function(json, done) { 15 | this.expect(function(res) { 16 | // Assume that the response can be parsed as JSON (otherwise it throws). 17 | var actual = JSON.parse(res.text); 18 | assert.deepEqual(actual, json); 19 | }); 20 | return done ? this.end(done) : this; 21 | }; 22 | 23 | request.Test.prototype.expectNoHeader = function(header, done) { 24 | this.expect(function(res) { 25 | if (header.toLowerCase() in res.headers) { 26 | return 'Unexpected header in response: ' + header; 27 | } 28 | }); 29 | return done ? this.end(done) : this; 30 | }; 31 | 32 | var cors_anywhere; 33 | var cors_anywhere_port; 34 | function stopServer(done) { 35 | cors_anywhere.close(function() { 36 | done(); 37 | }); 38 | cors_anywhere = null; 39 | } 40 | 41 | describe('Basic functionality', function() { 42 | before(function() { 43 | cors_anywhere = createServer(); 44 | cors_anywhere_port = cors_anywhere.listen(0).address().port; 45 | }); 46 | after(stopServer); 47 | 48 | it('GET /', function(done) { 49 | request(cors_anywhere) 50 | .get('/') 51 | .type('text/plain') 52 | .expect('Access-Control-Allow-Origin', '*') 53 | .expect(200, helpText, done); 54 | }); 55 | 56 | it('GET /iscorsneeded', function(done) { 57 | request(cors_anywhere) 58 | .get('/iscorsneeded') 59 | .expectNoHeader('access-control-allow-origin', done); 60 | }); 61 | 62 | it('GET /example.com:65536', function(done) { 63 | request(cors_anywhere) 64 | .get('/example.com:65536') 65 | .expect('Access-Control-Allow-Origin', '*') 66 | .expect(400, 'Port number too large: 65536', done); 67 | }); 68 | 69 | it('GET /favicon.ico', function(done) { 70 | request(cors_anywhere) 71 | .get('/favicon.ico') 72 | .expect('Access-Control-Allow-Origin', '*') 73 | .expect(404, 'Invalid host: favicon.ico', done); 74 | }); 75 | 76 | it('GET /robots.txt', function(done) { 77 | request(cors_anywhere) 78 | .get('/robots.txt') 79 | .expect('Access-Control-Allow-Origin', '*') 80 | .expect(404, 'Invalid host: robots.txt', done); 81 | }); 82 | 83 | it('GET /http://robots.txt should be proxied', function(done) { 84 | request(cors_anywhere) 85 | .get('/http://robots.txt') 86 | .expect('Access-Control-Allow-Origin', '*') 87 | .expect(200, 'this is http://robots.txt', done); 88 | }); 89 | 90 | it('GET /example.com', function(done) { 91 | request(cors_anywhere) 92 | .get('/example.com') 93 | .expect('Access-Control-Allow-Origin', '*') 94 | .expect('x-request-url', 'http://example.com/') 95 | .expect(200, 'Response from example.com', done); 96 | }); 97 | 98 | it('GET /example.com:80', function(done) { 99 | request(cors_anywhere) 100 | .get('/example.com:80') 101 | .expect('Access-Control-Allow-Origin', '*') 102 | .expect('x-request-url', 'http://example.com:80/') 103 | .expect(200, 'Response from example.com', done); 104 | }); 105 | 106 | it('GET /example.com:443', function(done) { 107 | request(cors_anywhere) 108 | .get('/example.com:443') 109 | .expect('Access-Control-Allow-Origin', '*') 110 | .expect('x-request-url', 'https://example.com:443/') 111 | .expect(200, 'Response from https://example.com', done); 112 | }); 113 | 114 | it('GET //example.com', function(done) { 115 | // '/example.com' is an invalid URL. 116 | request(cors_anywhere) 117 | .get('//example.com') 118 | .expect('Access-Control-Allow-Origin', '*') 119 | .expect(200, helpText, done); 120 | }); 121 | 122 | it('GET /http://:1234', function(done) { 123 | // 'http://:1234' is an invalid URL. 124 | request(cors_anywhere) 125 | .get('/http://:1234') 126 | .expect('Access-Control-Allow-Origin', '*') 127 | .expect(200, helpText, done); 128 | }); 129 | 130 | it('GET /http:///', function(done) { 131 | // 'http://:1234' is an invalid URL. 132 | request(cors_anywhere) 133 | .get('/http:///') 134 | .expect('Access-Control-Allow-Origin', '*') 135 | .expect(200, helpText, done); 136 | }); 137 | 138 | it('GET /http:/notenoughslashes', function(done) { 139 | // 'http:/notenoughslashes' is an invalid URL. 140 | request(cors_anywhere) 141 | .get('/http:/notenoughslashes') 142 | .expect('Access-Control-Allow-Origin', '*') 143 | .expect(200, helpText, done); 144 | }); 145 | 146 | 147 | it('GET ///example.com', function(done) { 148 | // API base URL (with trailing slash) + '//example.com' 149 | request(cors_anywhere) 150 | .get('///example.com') 151 | .expect('Access-Control-Allow-Origin', '*') 152 | .expect('x-request-url', 'http://example.com/') 153 | .expect(200, 'Response from example.com', done); 154 | }); 155 | 156 | it('GET /http://example.com', function(done) { 157 | request(cors_anywhere) 158 | .get('/http://example.com') 159 | .expect('Access-Control-Allow-Origin', '*') 160 | .expect('x-request-url', 'http://example.com/') 161 | .expect(200, 'Response from example.com', done); 162 | }); 163 | 164 | it('POST plain text', function(done) { 165 | request(cors_anywhere) 166 | .post('/example.com/echopost') 167 | .send('{"this is a request body & should not be mangled":1.00}') 168 | .expect('Access-Control-Allow-Origin', '*') 169 | .expect('{"this is a request body & should not be mangled":1.00}', done); 170 | }); 171 | 172 | it('POST file', function(done) { 173 | request(cors_anywhere) 174 | .post('/example.com/echopost') 175 | .attach('file', path.join(__dirname, 'dummy.txt')) 176 | .expect('Access-Control-Allow-Origin', '*') 177 | .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); // eslint-disable-line max-len 178 | }); 179 | 180 | it('HEAD with redirect should be followed', function(done) { 181 | // Redirects are automatically followed, because redirects are to be 182 | // followed automatically per specification regardless of the HTTP verb. 183 | request(cors_anywhere) 184 | .head('/example.com/redirect') 185 | .redirects(0) 186 | .expect('Access-Control-Allow-Origin', '*') 187 | .expect('some-header', 'value') 188 | .expect('x-request-url', 'http://example.com/redirect') 189 | .expect('x-cors-redirect-1', '302 http://example.com/redirecttarget') 190 | .expect('x-final-url', 'http://example.com/redirecttarget') 191 | .expect('access-control-expose-headers', /some-header,x-final-url/) 192 | .expectNoHeader('header at redirect') 193 | .expect(200, undefined, done); 194 | }); 195 | 196 | it('GET with redirect should be followed', function(done) { 197 | request(cors_anywhere) 198 | .get('/example.com/redirect') 199 | .redirects(0) 200 | .expect('Access-Control-Allow-Origin', '*') 201 | .expect('some-header', 'value') 202 | .expect('x-request-url', 'http://example.com/redirect') 203 | .expect('x-cors-redirect-1', '302 http://example.com/redirecttarget') 204 | .expect('x-final-url', 'http://example.com/redirecttarget') 205 | .expect('access-control-expose-headers', /some-header,x-final-url/) 206 | .expectNoHeader('header at redirect') 207 | .expect(200, 'redirect target', done); 208 | }); 209 | 210 | it('GET with redirect loop should interrupt', function(done) { 211 | request(cors_anywhere) 212 | .get('/example.com/redirectloop') 213 | .redirects(0) 214 | .expect('Access-Control-Allow-Origin', '*') 215 | .expect('x-request-url', 'http://example.com/redirectloop') 216 | .expect('x-cors-redirect-1', '302 http://example.com/redirectloop') 217 | .expect('x-cors-redirect-2', '302 http://example.com/redirectloop') 218 | .expect('x-cors-redirect-3', '302 http://example.com/redirectloop') 219 | .expect('x-cors-redirect-4', '302 http://example.com/redirectloop') 220 | .expect('x-cors-redirect-5', '302 http://example.com/redirectloop') 221 | .expect('Location', /^http:\/\/127.0.0.1:\d+\/http:\/\/example.com\/redirectloop$/) 222 | .expect(302, 'redirecting ad infinitum...', done); 223 | }); 224 | 225 | it('POST with 302 redirect should be followed', function(done) { 226 | request(cors_anywhere) 227 | .post('/example.com/redirectpost') 228 | .redirects(0) 229 | .expect('Access-Control-Allow-Origin', '*') 230 | .expect('x-request-url', 'http://example.com/redirectpost') 231 | .expect('x-cors-redirect-1', '302 http://example.com/redirectposttarget') 232 | .expect('x-final-url', 'http://example.com/redirectposttarget') 233 | .expect('access-control-expose-headers', /x-final-url/) 234 | .expect(200, 'post target', done); 235 | }); 236 | 237 | it('GET with 302 redirect without Location header should not be followed', function(done) { 238 | // There is nothing to follow, so let the browser decide what to do with it. 239 | request(cors_anywhere) 240 | .get('/example.com/redirectwithoutlocation') 241 | .redirects(0) 242 | .expect('Access-Control-Allow-Origin', '*') 243 | .expect('x-request-url', 'http://example.com/redirectwithoutlocation') 244 | .expect('x-final-url', 'http://example.com/redirectwithoutlocation') 245 | .expect('access-control-expose-headers', /x-final-url/) 246 | .expect(302, 'maybe found', done); 247 | }); 248 | 249 | it('GET with 302 redirect to an invalid Location should not be followed', function(done) { 250 | // There is nothing to follow, so let the browser decide what to do with it. 251 | request(cors_anywhere) 252 | .get('/example.com/redirectinvalidlocation') 253 | .redirects(0) 254 | .expect('Access-Control-Allow-Origin', '*') 255 | .expect('x-request-url', 'http://example.com/redirectinvalidlocation') 256 | .expect('x-final-url', 'http://example.com/redirectinvalidlocation') 257 | .expect('access-control-expose-headers', /x-final-url/) 258 | .expect('Location', 'http:///') 259 | .expect(302, 'redirecting to junk...', done); 260 | }); 261 | 262 | it('POST with 307 redirect should not be handled', function(done) { 263 | // Because of implementation difficulties (having to keep the request body 264 | // in memory), handling HTTP 307/308 redirects is deferred to the requestor. 265 | request(cors_anywhere) 266 | .post('/example.com/redirect307') 267 | .redirects(0) 268 | .expect('Access-Control-Allow-Origin', '*') 269 | .expect('x-request-url', 'http://example.com/redirect307') 270 | .expect('Location', /^http:\/\/127.0.0.1:\d+\/http:\/\/example.com\/redirectposttarget$/) 271 | .expect('x-final-url', 'http://example.com/redirect307') 272 | .expect('access-control-expose-headers', /x-final-url/) 273 | .expect(307, 'redirecting...', done); 274 | }); 275 | 276 | it('OPTIONS /', function(done) { 277 | request(cors_anywhere) 278 | .options('/') 279 | .expect('Access-Control-Allow-Origin', '*') 280 | .expect(200, '', done); 281 | }); 282 | 283 | it('OPTIONS / with Access-Control-Request-Method / -Headers', function(done) { 284 | request(cors_anywhere) 285 | .options('/') 286 | .set('Access-Control-Request-Method', 'DELETE') 287 | .set('Access-Control-Request-Headers', 'X-Tralala') 288 | .expect('Access-Control-Allow-Origin', '*') 289 | .expect('Access-Control-Allow-Methods', 'DELETE') 290 | .expect('Access-Control-Allow-Headers', 'X-Tralala') 291 | .expect(200, '', done); 292 | }); 293 | 294 | it('OPTIONS //bogus', function(done) { 295 | // The preflight request always succeeds, regardless of whether the request 296 | // is valid. 297 | request(cors_anywhere) 298 | .options('//bogus') 299 | .expect('Access-Control-Allow-Origin', '*') 300 | .expect(200, '', done); 301 | }); 302 | 303 | it('X-Forwarded-* headers', function(done) { 304 | request(cors_anywhere) 305 | .get('/example.com/echoheaders') 306 | .set('test-include-xfwd', '') 307 | .expect('Access-Control-Allow-Origin', '*') 308 | .expectJSON({ 309 | host: 'example.com', 310 | 'x-forwarded-port': String(cors_anywhere_port), 311 | 'x-forwarded-proto': 'http', 312 | }, done); 313 | }); 314 | 315 | it('X-Forwarded-* headers (non-standard port)', function(done) { 316 | request(cors_anywhere) 317 | .get('/example.com:1337/echoheaders') 318 | .set('test-include-xfwd', '') 319 | .expect('Access-Control-Allow-Origin', '*') 320 | .expectJSON({ 321 | host: 'example.com:1337', 322 | 'x-forwarded-port': String(cors_anywhere_port), 323 | 'x-forwarded-proto': 'http', 324 | }, done); 325 | }); 326 | 327 | it('X-Forwarded-* headers (https)', function(done) { 328 | request(cors_anywhere) 329 | .get('/https://example.com/echoheaders') 330 | .set('test-include-xfwd', '') 331 | .expect('Access-Control-Allow-Origin', '*') 332 | .expectJSON({ 333 | host: 'example.com', 334 | 'x-forwarded-port': String(cors_anywhere_port), 335 | 'x-forwarded-proto': 'http', 336 | }, done); 337 | }); 338 | 339 | it('Ignore cookies', function(done) { 340 | request(cors_anywhere) 341 | .get('/example.com/setcookie') 342 | .expect('Access-Control-Allow-Origin', '*') 343 | .expect('Set-Cookie3', 'z') 344 | .expectNoHeader('set-cookie') 345 | .expectNoHeader('set-cookie2', done); 346 | }); 347 | }); 348 | 349 | describe('Proxy errors', function() { 350 | before(function() { 351 | cors_anywhere = createServer(); 352 | cors_anywhere_port = cors_anywhere.listen(0).address().port; 353 | }); 354 | after(stopServer); 355 | 356 | var bad_http_server; 357 | var bad_http_server_url; 358 | before(function() { 359 | bad_http_server = http.createServer(function(req, res) { 360 | res.writeHead(418, { 361 | 'Content-Length': 'Not a digit', 362 | }); 363 | res.end('This response has an invalid Content-Length header.'); 364 | }); 365 | bad_http_server_url = 'http://127.0.0.1:' + bad_http_server.listen(0).address().port; 366 | }); 367 | after(function(done) { 368 | bad_http_server.close(function() { 369 | done(); 370 | }); 371 | }); 372 | 373 | var bad_status_http_server; 374 | var bad_status_http_server_url; 375 | before(function() { 376 | bad_status_http_server = require('net').createServer(function(socket) { 377 | socket.setEncoding('utf-8'); 378 | socket.on('data', function(data) { 379 | if (data.indexOf('\r\n') >= 0) { 380 | // Assume end of headers. 381 | socket.write('HTTP/1.0 0\r\n'); 382 | socket.write('Content-Length: 0\r\n'); 383 | socket.end('\r\n'); 384 | } 385 | }); 386 | }); 387 | bad_status_http_server_url = 'http://127.0.0.1:' + bad_status_http_server.listen(0).address().port; 388 | }); 389 | after(function(done) { 390 | bad_status_http_server.close(function() { 391 | done(); 392 | }); 393 | }); 394 | 395 | var bad_tcp_server; 396 | var bad_tcp_server_url; 397 | before(function() { 398 | bad_tcp_server = require('net').createServer(function(socket) { 399 | socket.setEncoding('utf-8'); 400 | socket.on('data', function(data) { 401 | if (data.indexOf('\r\n') >= 0) { 402 | // Assume end of headers. 403 | socket.write('HTTP/1.1 418 OK\r\n'); 404 | socket.write('Transfer-Encoding: chunked\r\n'); 405 | socket.write('\r\n'); 406 | socket.end('JK I lied, this is NOT a chunked response!'); 407 | } 408 | }); 409 | }); 410 | bad_tcp_server_url = 'http://127.0.0.1:' + bad_tcp_server.listen(0).address().port; 411 | }); 412 | after(function(done) { 413 | bad_tcp_server.close(function() { 414 | done(); 415 | }); 416 | }); 417 | 418 | it('Proxy error', function(done) { 419 | request(cors_anywhere) 420 | .get('/example.com/proxyerror') 421 | .expect('Access-Control-Allow-Origin', '*') 422 | .expect(404, 'Not found because of proxy error: Error: throw node', done); 423 | }); 424 | 425 | it('Content-Length mismatch', function(done) { 426 | var errorMessage = 'Error: Parse Error: Invalid character in Content-Length'; 427 | // <13.0.0: https://github.com/nodejs/node/commit/ba565a37349e81c9d2402b0c8ef05ab39dca8968 428 | // <12.7.0: https://github.com/nodejs/node/pull/28817 429 | var nodev = process.versions.node.split('.').map(function(v) { return parseInt(v); }); 430 | if (nodev[0] < 12 || 431 | nodev[0] === 12 && nodev[1] < 7) { 432 | errorMessage = 'Error: Parse Error'; 433 | } 434 | request(cors_anywhere) 435 | .get('/' + bad_http_server_url) 436 | .expect('Access-Control-Allow-Origin', '*') 437 | .expect(404, 'Not found because of proxy error: ' + errorMessage, done); 438 | }); 439 | 440 | it('Invalid HTTP status code', function(done) { 441 | // Strict HTTP status validation was introduced in Node 4.5.5+, 5.11.0+. 442 | // https://github.com/nodejs/node/pull/6291 443 | var nodev = process.versions.node.split('.').map(function(v) { return parseInt(v); }); 444 | if (nodev[0] < 4 || 445 | nodev[0] === 4 && nodev[1] < 5 || 446 | nodev[0] === 4 && nodev[1] === 5 && nodev[2] < 5 || 447 | nodev[0] === 5 && nodev[1] < 11) { 448 | this.skip(); 449 | } 450 | 451 | var errorMessage = 'RangeError [ERR_HTTP_INVALID_STATUS_CODE]: Invalid status code: 0'; 452 | if (parseInt(process.versions.node, 10) < 9) { 453 | errorMessage = 'RangeError: Invalid status code: 0'; 454 | } 455 | request(cors_anywhere) 456 | .get('/' + bad_status_http_server_url) 457 | .expect('Access-Control-Allow-Origin', '*') 458 | .expect(404, 'Not found because of proxy error: ' + errorMessage, done); 459 | }); 460 | 461 | it('Content-Encoding invalid body', function(done) { 462 | // The HTTP status can't be changed because the headers have already been 463 | // sent. 464 | request(cors_anywhere) 465 | .get('/' + bad_tcp_server_url) 466 | .expect('Access-Control-Allow-Origin', '*') 467 | .expect(418, '', done); 468 | }); 469 | 470 | it('Invalid header values', function(done) { 471 | if (parseInt(process.versions.node, 10) < 6) { 472 | // >=6.0.0: https://github.com/nodejs/node/commit/7bef1b790727430cb82bf8be80cfe058480de100 473 | this.skip(); 474 | } 475 | // >=9.0.0: https://github.com/nodejs/node/commit/11a2ca29babcb35132e7d93244b69c544d52dfe4 476 | var errorMessage = 'TypeError [ERR_INVALID_CHAR]: Invalid character in header content ["headername"]'; 477 | if (parseInt(process.versions.node, 10) < 9) { 478 | // >=6.0.0, <9.0.0: https://github.com/nodejs/node/commit/7bef1b790727430cb82bf8be80cfe058480de100 479 | errorMessage = 'TypeError: The header content contains invalid characters'; 480 | } 481 | stopServer(function() { 482 | cors_anywhere = createServer({ 483 | // Setting an invalid header below in request(...).set(...) would trigger 484 | // a header validation error in superagent. So we use setHeaders to test 485 | // the attempt to proxy a request with invalid request headers. 486 | setHeaders: {headername: 'invalid\x01value'}, 487 | }); 488 | cors_anywhere_port = cors_anywhere.listen(0).address().port; 489 | request(cors_anywhere) 490 | .get('/' + bad_tcp_server_url) // Any URL that isn't intercepted by Nock would do. 491 | .expect('Access-Control-Allow-Origin', '*') 492 | .expect(404, 'Not found because of proxy error: ' + errorMessage, done); 493 | }); 494 | }); 495 | }); 496 | 497 | describe('server on https', function() { 498 | var NODE_TLS_REJECT_UNAUTHORIZED; 499 | before(function() { 500 | cors_anywhere = createServer({ 501 | httpsOptions: { 502 | key: fs.readFileSync(path.join(__dirname, 'key.pem')), 503 | cert: fs.readFileSync(path.join(__dirname, 'cert.pem')), 504 | }, 505 | }); 506 | cors_anywhere_port = cors_anywhere.listen(0).address().port; 507 | // Disable certificate validation in case the certificate expires. 508 | NODE_TLS_REJECT_UNAUTHORIZED = process.env.NODE_TLS_REJECT_UNAUTHORIZED; 509 | process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0'; 510 | }); 511 | after(function(done) { 512 | if (NODE_TLS_REJECT_UNAUTHORIZED === undefined) { 513 | delete process.env.NODE_TLS_REJECT_UNAUTHORIZED; 514 | } else { 515 | process.env.NODE_TLS_REJECT_UNAUTHORIZED = NODE_TLS_REJECT_UNAUTHORIZED; 516 | } 517 | stopServer(done); 518 | }); 519 | 520 | it('X-Forwarded-* headers (http)', function(done) { 521 | request(cors_anywhere) 522 | .get('/example.com/echoheaders') 523 | .set('test-include-xfwd', '') 524 | .expect('Access-Control-Allow-Origin', '*') 525 | .expectJSON({ 526 | host: 'example.com', 527 | 'x-forwarded-port': String(cors_anywhere_port), 528 | 'x-forwarded-proto': 'https', 529 | }, done); 530 | }); 531 | 532 | it('X-Forwarded-* headers (https)', function(done) { 533 | request(cors_anywhere) 534 | .get('/https://example.com/echoheaders') 535 | .set('test-include-xfwd', '') 536 | .expect('Access-Control-Allow-Origin', '*') 537 | .expectJSON({ 538 | host: 'example.com', 539 | 'x-forwarded-port': String(cors_anywhere_port), 540 | 'x-forwarded-proto': 'https', 541 | }, done); 542 | }); 543 | 544 | it('X-Forwarded-* headers (https, non-standard port)', function(done) { 545 | request(cors_anywhere) 546 | .get('/https://example.com:1337/echoheaders') 547 | .set('test-include-xfwd', '') 548 | .expect('Access-Control-Allow-Origin', '*') 549 | .expectJSON({ 550 | host: 'example.com:1337', 551 | 'x-forwarded-port': String(cors_anywhere_port), 552 | 'x-forwarded-proto': 'https', 553 | }, done); 554 | }); 555 | }); 556 | 557 | describe('originBlacklist', function() { 558 | before(function() { 559 | cors_anywhere = createServer({ 560 | originBlacklist: ['http://denied.origin.test'], 561 | }); 562 | cors_anywhere_port = cors_anywhere.listen(0).address().port; 563 | }); 564 | after(stopServer); 565 | 566 | it('GET /example.com with denied origin', function(done) { 567 | request(cors_anywhere) 568 | .get('/example.com/') 569 | .set('Origin', 'http://denied.origin.test') 570 | .expect('Access-Control-Allow-Origin', '*') 571 | .expect(403, done); 572 | }); 573 | 574 | it('GET /example.com without denied origin', function(done) { 575 | request(cors_anywhere) 576 | .get('/example.com/') 577 | .set('Origin', 'https://denied.origin.test') // Note: different scheme! 578 | .expect('Access-Control-Allow-Origin', '*') 579 | .expect(200, done); 580 | }); 581 | 582 | it('GET /example.com without origin', function(done) { 583 | request(cors_anywhere) 584 | .get('/example.com/') 585 | .expect('Access-Control-Allow-Origin', '*') 586 | .expect(200, done); 587 | }); 588 | }); 589 | 590 | describe('originWhitelist', function() { 591 | before(function() { 592 | cors_anywhere = createServer({ 593 | originWhitelist: ['https://permitted.origin.test'], 594 | }); 595 | cors_anywhere_port = cors_anywhere.listen(0).address().port; 596 | }); 597 | after(stopServer); 598 | 599 | it('GET /example.com with permitted origin', function(done) { 600 | request(cors_anywhere) 601 | .get('/example.com/') 602 | .set('Origin', 'https://permitted.origin.test') 603 | .expect('Access-Control-Allow-Origin', '*') 604 | .expect(200, done); 605 | }); 606 | 607 | it('GET /example.com without permitted origin', function(done) { 608 | request(cors_anywhere) 609 | .get('/example.com/') 610 | .set('Origin', 'http://permitted.origin.test') // Note: different scheme! 611 | .expect('Access-Control-Allow-Origin', '*') 612 | .expect(403, done); 613 | }); 614 | 615 | it('GET /example.com without origin', function(done) { 616 | request(cors_anywhere) 617 | .get('/example.com/') 618 | .expect('Access-Control-Allow-Origin', '*') 619 | .expect(403, done); 620 | }); 621 | }); 622 | 623 | describe('checkRateLimit', function() { 624 | afterEach(stopServer); 625 | 626 | it('GET /example.com without rate-limit', function(done) { 627 | cors_anywhere = createServer({ 628 | checkRateLimit: function() {}, 629 | }); 630 | cors_anywhere_port = cors_anywhere.listen(0).address().port; 631 | request(cors_anywhere) 632 | .get('/example.com/') 633 | .expect('Access-Control-Allow-Origin', '*') 634 | .expect(200, done); 635 | }); 636 | 637 | it('GET /example.com with rate-limit', function(done) { 638 | cors_anywhere = createServer({ 639 | checkRateLimit: function(origin) { 640 | // Non-empty value. Let's return the origin parameter so that we also verify that the 641 | // the parameter is really the origin. 642 | return '[' + origin + ']'; 643 | }, 644 | }); 645 | cors_anywhere_port = cors_anywhere.listen(0).address().port; 646 | request(cors_anywhere) 647 | .get('/example.com/') 648 | .set('Origin', 'http://example.net:1234') 649 | .expect('Access-Control-Allow-Origin', '*') 650 | .expect(429, done, 651 | 'The origin "http://example.net" has sent too many requests.\n[http://example.com:1234]'); 652 | }); 653 | }); 654 | 655 | describe('redirectSameOrigin', function() { 656 | before(function() { 657 | cors_anywhere = createServer({ 658 | redirectSameOrigin: true, 659 | }); 660 | cors_anywhere_port = cors_anywhere.listen(0).address().port; 661 | }); 662 | after(stopServer); 663 | 664 | it('GET /example.com with Origin: http://example.com', function(done) { 665 | request(cors_anywhere) 666 | .get('/example.com/') 667 | .set('Origin', 'http://example.com') 668 | .expect('Access-Control-Allow-Origin', '*') 669 | .expect('Cache-Control', 'private') 670 | .expect('Vary', 'origin') 671 | .expect('Location', 'http://example.com/') 672 | .expect(301, done); 673 | }); 674 | 675 | it('GET /example.com with Origin: https://example.com', function(done) { 676 | // Not same-origin because of different schemes. 677 | request(cors_anywhere) 678 | .get('/example.com/') 679 | .set('Origin', 'https://example.com') 680 | .expect('Access-Control-Allow-Origin', '*') 681 | .expect(200, 'Response from example.com', done); 682 | }); 683 | 684 | it('GET /example.com with Origin: http://example.com:1234', function(done) { 685 | // Not same-origin because of different ports. 686 | request(cors_anywhere) 687 | .get('/example.com/') 688 | .set('Origin', 'http://example.com:1234') 689 | .expect('Access-Control-Allow-Origin', '*') 690 | .expect(200, 'Response from example.com', done); 691 | }); 692 | 693 | it('GET /example.com:1234 with Origin: http://example.com', function(done) { 694 | // Not same-origin because of different ports. 695 | request(cors_anywhere) 696 | .get('/example.com:1234/') 697 | .set('Origin', 'http://example.com') 698 | .expect('Access-Control-Allow-Origin', '*') 699 | .expect(200, 'Response from example.com:1234', done); 700 | }); 701 | 702 | it('GET /example.com with Origin: http://example.com.test', function(done) { 703 | // Not same-origin because of different host names. 704 | request(cors_anywhere) 705 | .get('/example.com/') 706 | .set('Origin', 'http://example.com.test') 707 | .expect('Access-Control-Allow-Origin', '*') 708 | .expect(200, 'Response from example.com', done); 709 | }); 710 | 711 | it('GET /example.com.com with Origin: http://example.com', function(done) { 712 | // Not same-origin because of different host names. 713 | request(cors_anywhere) 714 | .get('/example.com.com/') 715 | .set('Origin', 'http://example.com') 716 | .expect('Access-Control-Allow-Origin', '*') 717 | .expect(200, 'Response from example.com.com', done); 718 | }); 719 | 720 | it('GET /prefix.example.com with Origin: http://example.com', function(done) { 721 | // Not same-origin because of different host names. 722 | request(cors_anywhere) 723 | .get('/prefix.example.com/') 724 | .set('Origin', 'http://example.com') 725 | .expect('Access-Control-Allow-Origin', '*') 726 | .expect(200, 'Response from prefix.example.com', done); 727 | }); 728 | }); 729 | 730 | describe('requireHeader', function() { 731 | before(function() { 732 | cors_anywhere = createServer({ 733 | requireHeader: ['origin', 'x-requested-with'], 734 | }); 735 | cors_anywhere_port = cors_anywhere.listen(0).address().port; 736 | }); 737 | after(stopServer); 738 | 739 | it('GET /example.com without header', function(done) { 740 | request(cors_anywhere) 741 | .get('/example.com/') 742 | .expect('Access-Control-Allow-Origin', '*') 743 | .expect(400, 'Missing required request header. Must specify one of: origin,x-requested-with', done); 744 | }); 745 | 746 | it('GET /example.com with X-Requested-With header', function(done) { 747 | request(cors_anywhere) 748 | .get('/example.com/') 749 | .set('X-Requested-With', '') 750 | .expect('Access-Control-Allow-Origin', '*') 751 | .expect(200, done); 752 | }); 753 | 754 | it('GET /example.com with Origin header', function(done) { 755 | request(cors_anywhere) 756 | .get('/example.com/') 757 | .set('Origin', 'null') 758 | .expect('Access-Control-Allow-Origin', '*') 759 | .expect(200, done); 760 | }); 761 | 762 | it('GET /example.com without header (requireHeader as string)', function(done) { 763 | stopServer(function() { 764 | cors_anywhere = createServer({ 765 | requireHeader: 'origin', 766 | }); 767 | cors_anywhere_port = cors_anywhere.listen(0).address().port; 768 | request(cors_anywhere) 769 | .get('/example.com/') 770 | .expect('Access-Control-Allow-Origin', '*') 771 | .expect(400, 'Missing required request header. Must specify one of: origin', done); 772 | }); 773 | }); 774 | 775 | it('GET /example.com with header (requireHeader as string)', function(done) { 776 | stopServer(function() { 777 | cors_anywhere = createServer({ 778 | requireHeader: 'origin', 779 | }); 780 | cors_anywhere_port = cors_anywhere.listen(0).address().port; 781 | request(cors_anywhere) 782 | .get('/example.com/') 783 | .set('Origin', 'null') 784 | .expect('Access-Control-Allow-Origin', '*') 785 | .expect(200, 'Response from example.com', done); 786 | }); 787 | }); 788 | 789 | it('GET /example.com without header (requireHeader as string, uppercase)', function(done) { 790 | stopServer(function() { 791 | cors_anywhere = createServer({ 792 | requireHeader: 'ORIGIN', 793 | }); 794 | cors_anywhere_port = cors_anywhere.listen(0).address().port; 795 | request(cors_anywhere) 796 | .get('/example.com/') 797 | .expect('Access-Control-Allow-Origin', '*') 798 | .expect(400, 'Missing required request header. Must specify one of: origin', done); 799 | }); 800 | }); 801 | 802 | it('GET /example.com with header (requireHeader as string, uppercase)', function(done) { 803 | stopServer(function() { 804 | cors_anywhere = createServer({ 805 | requireHeader: 'ORIGIN', 806 | }); 807 | cors_anywhere_port = cors_anywhere.listen(0).address().port; 808 | request(cors_anywhere) 809 | .get('/example.com/') 810 | .set('Origin', 'null') 811 | .expect('Access-Control-Allow-Origin', '*') 812 | .expect(200, 'Response from example.com', done); 813 | }); 814 | }); 815 | 816 | it('GET /example.com (requireHeader is an empty array)', function(done) { 817 | stopServer(function() { 818 | cors_anywhere = createServer({ 819 | requireHeader: [], 820 | }); 821 | cors_anywhere_port = cors_anywhere.listen(0).address().port; 822 | request(cors_anywhere) 823 | .get('/example.com/') 824 | .expect('Access-Control-Allow-Origin', '*') 825 | .expect(200, 'Response from example.com', done); 826 | }); 827 | }); 828 | }); 829 | 830 | describe('removeHeaders', function() { 831 | before(function() { 832 | cors_anywhere = createServer({ 833 | removeHeaders: ['cookie', 'cookie2'], 834 | }); 835 | cors_anywhere_port = cors_anywhere.listen(0).address().port; 836 | }); 837 | after(stopServer); 838 | 839 | it('GET /example.com with request cookie', function(done) { 840 | request(cors_anywhere) 841 | .get('/example.com/echoheaders') 842 | .set('cookie', 'a') 843 | .set('cookie2', 'b') 844 | .expect('Access-Control-Allow-Origin', '*') 845 | .expectJSON({ 846 | host: 'example.com', 847 | }, done); 848 | }); 849 | 850 | it('GET /example.com with unknown header', function(done) { 851 | request(cors_anywhere) 852 | .get('/example.com/echoheaders') 853 | .set('cookie', 'a') 854 | .set('cookie2', 'b') 855 | .set('cookie3', 'c') 856 | .expect('Access-Control-Allow-Origin', '*') 857 | .expectJSON({ 858 | host: 'example.com', 859 | cookie3: 'c', 860 | }, done); 861 | }); 862 | }); 863 | 864 | describe('setHeaders', function() { 865 | before(function() { 866 | cors_anywhere = createServer({ 867 | setHeaders: {'x-powered-by': 'CORS Anywhere'}, 868 | }); 869 | cors_anywhere_port = cors_anywhere.listen(0).address().port; 870 | }); 871 | after(stopServer); 872 | 873 | it('GET /example.com', function(done) { 874 | request(cors_anywhere) 875 | .get('/example.com/echoheaders') 876 | .expect('Access-Control-Allow-Origin', '*') 877 | .expectJSON({ 878 | host: 'example.com', 879 | 'x-powered-by': 'CORS Anywhere', 880 | }, done); 881 | }); 882 | 883 | it('GET /example.com should replace header', function(done) { 884 | request(cors_anywhere) 885 | .get('/example.com/echoheaders') 886 | .set('x-powered-by', 'should be replaced') 887 | .expect('Access-Control-Allow-Origin', '*') 888 | .expectJSON({ 889 | host: 'example.com', 890 | 'x-powered-by': 'CORS Anywhere', 891 | }, done); 892 | }); 893 | }); 894 | 895 | describe('setHeaders + removeHeaders', function() { 896 | before(function() { 897 | // setHeaders takes precedence over removeHeaders 898 | cors_anywhere = createServer({ 899 | removeHeaders: ['x-powered-by'], 900 | setHeaders: {'x-powered-by': 'CORS Anywhere'}, 901 | }); 902 | cors_anywhere_port = cors_anywhere.listen(0).address().port; 903 | }); 904 | after(stopServer); 905 | 906 | it('GET /example.com', function(done) { 907 | request(cors_anywhere) 908 | .get('/example.com/echoheaders') 909 | .expect('Access-Control-Allow-Origin', '*') 910 | .expectJSON({ 911 | host: 'example.com', 912 | 'x-powered-by': 'CORS Anywhere', 913 | }, done); 914 | }); 915 | 916 | it('GET /example.com should replace header', function(done) { 917 | request(cors_anywhere) 918 | .get('/example.com/echoheaders') 919 | .set('x-powered-by', 'should be replaced') 920 | .expect('Access-Control-Allow-Origin', '*') 921 | .expectJSON({ 922 | host: 'example.com', 923 | 'x-powered-by': 'CORS Anywhere', 924 | }, done); 925 | }); 926 | }); 927 | 928 | describe('Access-Control-Max-Age set', function() { 929 | before(function() { 930 | cors_anywhere = createServer({ 931 | corsMaxAge: 600, 932 | }); 933 | cors_anywhere_port = cors_anywhere.listen(0).address().port; 934 | }); 935 | after(stopServer); 936 | 937 | it('GET /', function(done) { 938 | request(cors_anywhere) 939 | .get('/') 940 | .type('text/plain') 941 | .expect('Access-Control-Allow-Origin', '*') 942 | .expect('Access-Control-Max-Age', '600') 943 | .expect(200, helpText, done); 944 | }); 945 | 946 | it('GET /example.com', function(done) { 947 | request(cors_anywhere) 948 | .get('/example.com') 949 | .expect('Access-Control-Allow-Origin', '*') 950 | .expect('Access-Control-Max-Age', '600') 951 | .expect(200, 'Response from example.com', done); 952 | }); 953 | }); 954 | 955 | describe('Access-Control-Max-Age not set', function() { 956 | before(function() { 957 | cors_anywhere = createServer(); 958 | cors_anywhere_port = cors_anywhere.listen(0).address().port; 959 | }); 960 | after(stopServer); 961 | 962 | it('GET /', function(done) { 963 | request(cors_anywhere) 964 | .get('/') 965 | .type('text/plain') 966 | .expect('Access-Control-Allow-Origin', '*') 967 | .expectNoHeader('Access-Control-Max-Age') 968 | .expect(200, helpText, done); 969 | }); 970 | 971 | it('GET /example.com', function(done) { 972 | request(cors_anywhere) 973 | .get('/example.com') 974 | .expect('Access-Control-Allow-Origin', '*') 975 | .expectNoHeader('Access-Control-Max-Age') 976 | .expect(200, 'Response from example.com', done); 977 | }); 978 | }); 979 | 980 | describe('httpProxyOptions.xfwd=false', function() { 981 | before(function() { 982 | cors_anywhere = createServer({ 983 | httpProxyOptions: { 984 | xfwd: false, 985 | }, 986 | }); 987 | cors_anywhere_port = cors_anywhere.listen(0).address().port; 988 | }); 989 | after(stopServer); 990 | 991 | it('X-Forwarded-* headers should not be set', function(done) { 992 | request(cors_anywhere) 993 | .get('/example.com/echoheaders') 994 | .set('test-include-xfwd', '') 995 | .expect('Access-Control-Allow-Origin', '*') 996 | .expectJSON({ 997 | host: 'example.com', 998 | }, done); 999 | }); 1000 | }); 1001 | 1002 | describe('httpProxyOptions.getProxyForUrl', function() { 1003 | var proxy_server; 1004 | var proxy_url; 1005 | before(function() { 1006 | // Using a real server instead of a mock because Nock doesn't can't mock proxies. 1007 | proxy_server = http.createServer(function(req, res) { 1008 | res.end(req.method + ' ' + req.url + ' Host=' + req.headers.host); 1009 | }); 1010 | proxy_url = 'http://127.0.0.1:' + proxy_server.listen(0).address().port; 1011 | 1012 | cors_anywhere = createServer({ 1013 | httpProxyOptions: { 1014 | xfwd: false, 1015 | }, 1016 | }); 1017 | cors_anywhere_port = cors_anywhere.listen(0).address().port; 1018 | }); 1019 | afterEach(function() { 1020 | // Assuming that they were not set before. 1021 | delete process.env.https_proxy; 1022 | delete process.env.http_proxy; 1023 | delete process.env.no_proxy; 1024 | }); 1025 | after(function(done) { 1026 | proxy_server.close(function() { 1027 | done(); 1028 | }); 1029 | }); 1030 | after(stopServer); 1031 | 1032 | it('http_proxy should be respected for matching domains', function(done) { 1033 | process.env.http_proxy = proxy_url; 1034 | 1035 | request(cors_anywhere) 1036 | .get('/http://example.com') 1037 | .expect('Access-Control-Allow-Origin', '*') 1038 | .expect(200, 'GET http://example.com/ Host=example.com', done); 1039 | }); 1040 | 1041 | it('http_proxy should be ignored for http URLs', function(done) { 1042 | process.env.http_proxy = proxy_url; 1043 | request(cors_anywhere) 1044 | .get('/https://example.com') 1045 | .expect('Access-Control-Allow-Origin', '*') 1046 | .expect(200, 'Response from https://example.com', done); 1047 | }); 1048 | 1049 | it('https_proxy should be respected for matching domains', function(done) { 1050 | process.env.https_proxy = proxy_url; 1051 | 1052 | request(cors_anywhere) 1053 | .get('/https://example.com') 1054 | .expect('Access-Control-Allow-Origin', '*') 1055 | .expect(200, 'GET https://example.com/ Host=example.com', done); 1056 | }); 1057 | 1058 | it('https_proxy should be ignored for http URLs', function(done) { 1059 | process.env.https_proxy = proxy_url; 1060 | request(cors_anywhere) 1061 | .get('/http://example.com') 1062 | .expect('Access-Control-Allow-Origin', '*') 1063 | .expect(200, 'Response from example.com', done); 1064 | }); 1065 | 1066 | it('https_proxy + no_proxy should not intercept requests in no_proxy', function(done) { 1067 | process.env.https_proxy = proxy_url; 1068 | process.env.no_proxy = 'example.com:443'; 1069 | request(cors_anywhere) 1070 | .get('/https://example.com') 1071 | .expect('Access-Control-Allow-Origin', '*') 1072 | .expect(200, 'Response from https://example.com', done); 1073 | }); 1074 | }); 1075 | 1076 | describe('helpFile', function() { 1077 | 1078 | afterEach(stopServer); 1079 | 1080 | it('GET / with custom text helpFile', function(done) { 1081 | var customHelpTextPath = path.join(__dirname, './customHelp.txt'); 1082 | var customHelpText = fs.readFileSync(customHelpTextPath, {encoding: 'utf8'}); 1083 | 1084 | cors_anywhere = createServer({ 1085 | helpFile: customHelpTextPath, 1086 | }); 1087 | cors_anywhere_port = cors_anywhere.listen(0).address().port; 1088 | 1089 | request(cors_anywhere) 1090 | .get('/') 1091 | .type('text/plain') 1092 | .expect('Access-Control-Allow-Origin', '*') 1093 | .expect(200, customHelpText, done); 1094 | }); 1095 | 1096 | it('GET / with custom HTML helpFile', function(done) { 1097 | var customHelpTextPath = path.join(__dirname, './customHelp.html'); 1098 | var customHelpText = fs.readFileSync(customHelpTextPath, {encoding: 'utf8'}); 1099 | 1100 | cors_anywhere = createServer({ 1101 | helpFile: customHelpTextPath, 1102 | }); 1103 | cors_anywhere_port = cors_anywhere.listen(0).address().port; 1104 | 1105 | request(cors_anywhere) 1106 | .get('/') 1107 | .type('text/html') 1108 | .expect('Access-Control-Allow-Origin', '*') 1109 | .expect(200, customHelpText, done); 1110 | }); 1111 | 1112 | it('GET / with non-existent help file', function(done) { 1113 | var customHelpTextPath = path.join(__dirname, 'Some non-existing file.'); 1114 | 1115 | cors_anywhere = createServer({ 1116 | helpFile: customHelpTextPath, 1117 | }); 1118 | cors_anywhere_port = cors_anywhere.listen(0).address().port; 1119 | 1120 | request(cors_anywhere) 1121 | .get('/') 1122 | .type('text/plain') 1123 | .expect('Access-Control-Allow-Origin', '*') 1124 | .expect(500, '', done); 1125 | }); 1126 | }); 1127 | --------------------------------------------------------------------------------