added static cors-anywhere

This commit is contained in:
zino
2021-01-20 13:14:30 +01:00
parent fda9de0fa5
commit 918e80eaac
25 changed files with 4827 additions and 0 deletions

View File

@@ -0,0 +1 @@
coverage/

28
cors-anywhere/.eslintrc Normal file
View File

@@ -0,0 +1,28 @@
{
"env": {
"node": true
},
"rules": {
"array-bracket-spacing": [2, "never"],
"block-scoped-var": 2,
"brace-style": [2, "1tbs", {"allowSingleLine": true}],
"comma-dangle": [2, "always-multiline"],
"computed-property-spacing": [2, "never"],
"curly": 2,
"eol-last": 2,
"eqeqeq": [2, "smart"],
"max-len": [1, 125],
"new-cap": 1,
"no-extend-native": 2,
"no-mixed-spaces-and-tabs": 2,
"no-trailing-spaces": 2,
"no-undef": 2,
"no-unused-vars": 1,
"no-use-before-define": [2, "nofunc"],
"object-curly-spacing": [2, "never"],
"quotes": [2, "single", "avoid-escape"],
"semi": [2, "always"],
"keyword-spacing": 2,
"space-unary-ops": 2
}
}

6
cors-anywhere/.gitignore vendored Normal file
View File

@@ -0,0 +1,6 @@
*.swp
*.tmp
*.log
coverage/
node_modules/

12
cors-anywhere/.travis.yml Normal file
View File

@@ -0,0 +1,12 @@
language: node_js
node_js:
- 0.10
- 4
- 6
- 7
- 8
- 9
script:
- npm run lint
- npm run test
- npm run test-coverage && cat coverage/lcov.info | ./node_modules/.bin/coveralls && rm -rf coverage

1
cors-anywhere/Procfile Normal file
View File

@@ -0,0 +1 @@
web: node server.js

181
cors-anywhere/README.md Normal file
View File

@@ -0,0 +1,181 @@
[![Build Status](https://travis-ci.org/Rob--W/cors-anywhere.svg?branch=master)](https://travis-ci.org/Rob--W/cors-anywhere)
[![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)
**CORS Anywhere** is a NodeJS proxy which adds CORS headers to the proxied request.
The url to proxy is literally taken from the path, validated and proxied. The protocol
part of the proxied URI is optional, and defaults to "http". If port 443 is specified,
the protocol defaults to "https".
This package does not put any restrictions on the http methods or headers, except for
cookies. Requesting [user credentials](http://www.w3.org/TR/cors/#user-credentials) is disallowed.
The app can be configured to require a header for proxying a request, for example to avoid
a direct visit from the browser.
## Example
```javascript
// Listen on a specific host via the HOST environment variable
var host = process.env.HOST || '0.0.0.0';
// Listen on a specific port via the PORT environment variable
var port = process.env.PORT || 8080;
var cors_proxy = require('cors-anywhere');
cors_proxy.createServer({
originWhitelist: [], // Allow all origins
requireHeader: ['origin', 'x-requested-with'],
removeHeaders: ['cookie', 'cookie2']
}).listen(port, host, function() {
console.log('Running CORS Anywhere on ' + host + ':' + port);
});
```
Request examples:
* `http://localhost:8080/http://google.com/` - Google.com with CORS headers
* `http://localhost:8080/google.com` - Same as previous.
* `http://localhost:8080/google.com:443` - Proxies `https://google.com/`
* `http://localhost:8080/` - Shows usage text, as defined in `libs/help.txt`
* `http://localhost:8080/favicon.ico` - Replies 404 Not found
Live examples:
* https://cors-anywhere.herokuapp.com/
* https://robwu.nl/cors-anywhere.html - This demo shows how to use the API.
## Documentation
### Client
To use the API, just prefix the URL with the API URL. Take a look at [demo.html](demo.html) for an example.
A concise summary of the documentation is provided at [lib/help.txt](lib/help.txt).
If you want to automatically enable cross-domain requests when needed, use the following snippet:
```javascript
(function() {
var cors_api_host = 'cors-anywhere.herokuapp.com';
var cors_api_url = 'https://' + cors_api_host + '/';
var slice = [].slice;
var origin = window.location.protocol + '//' + window.location.host;
var open = XMLHttpRequest.prototype.open;
XMLHttpRequest.prototype.open = function() {
var args = slice.call(arguments);
var targetOrigin = /^https?:\/\/([^\/]+)/i.exec(args[1]);
if (targetOrigin && targetOrigin[0].toLowerCase() !== origin &&
targetOrigin[1] !== cors_api_host) {
args[1] = cors_api_url + args[1];
}
return open.apply(this, args);
};
})();
```
If you're using jQuery, you can also use the following code **instead of** the previous one:
```javascript
jQuery.ajaxPrefilter(function(options) {
if (options.crossDomain && jQuery.support.cors) {
options.url = 'https://cors-anywhere.herokuapp.com/' + options.url;
}
});
```
### Server
The module exports `createServer(options)`, which creates a server that handles
proxy requests. The following options are supported:
* function `getProxyForUrl` - If set, specifies which intermediate proxy to use for a given URL.
If the return value is void, a direct request is sent. The default implementation is
[`proxy-from-env`](https://github.com/Rob--W/proxy-from-env), which respects the standard proxy
environment variables (e.g. `https_proxy`, `no_proxy`, etc.).
* array of strings `originBlacklist` - If set, requests whose origin is listed are blocked.
Example: `['https://bad.example.com', 'http://bad.example.com']`
* array of strings `originWhitelist` - If set, requests whose origin is not listed are blocked.
If this list is empty, all origins are allowed.
Example: `['https://good.example.com', 'http://good.example.com']`
* function `checkRateLimit` - If set, it is called with the origin (string) of the request. If this
function returns a non-empty string, the request is rejected and the string is send to the client.
* boolean `redirectSameOrigin` - If true, requests to URLs from the same origin will not be proxied but redirected.
The primary purpose for this option is to save server resources by delegating the request to the client
(since same-origin requests should always succeed, even without proxying).
* array of strings `requireHeader` - If set, the request must include this header or the API will refuse to proxy.
Recommended if you want to prevent users from using the proxy for normal browsing.
Example: `['Origin', 'X-Requested-With']`.
* array of lowercase strings `removeHeaders` - Exclude certain headers from being included in the request.
Example: `["cookie"]`
* dictionary of lowercase strings `setHeaders` - Set headers for the request (overwrites existing ones).
Example: `{"x-powered-by": "CORS Anywhere"}`
* number `corsMaxAge` - If set, an Access-Control-Max-Age request header with this value (in seconds) will be added.
Example: `600` - Allow CORS preflight request to be cached by the browser for 10 minutes.
* string `helpFile` - Set the help file (shown at the homepage).
Example: `"myCustomHelpText.txt"`
For advanced users, the following options are also provided.
* `httpProxyOptions` - Under the hood, [http-proxy](https://github.com/nodejitsu/node-http-proxy)
is used to proxy requests. Use this option if you really need to pass options
to http-proxy. The documentation for these options can be found [here](https://github.com/nodejitsu/node-http-proxy#options).
* `httpsOptions` - If set, a `https.Server` will be created. The given options are passed to the
[`https.createServer`](https://nodejs.org/api/https.html#https_https_createserver_options_requestlistener) method.
For even more advanced usage (building upon CORS Anywhere),
see the sample code in [test/test-examples.js](test/test-examples.js).
### Demo server
A public demo of CORS Anywhere is available at https://cors-anywhere.herokuapp.com. This server is
only provided so that you can easily and quickly try out CORS Anywhere. To ensure that the service
stays available to everyone, the number of requests per period is limited, except for requests from
some explicitly whitelisted origins.
If you expect lots of traffic, please host your own instance of CORS Anywhere, and make sure that
the CORS Anywhere server only whitelists your site to prevent others from using your instance of
CORS Anywhere as an open proxy.
For instance, to run a CORS Anywhere server that accepts any request from some example.com sites on
port 8080, use:
```
export PORT=8080
export CORSANYWHERE_WHITELIST=https://example.com,http://example.com,http://example.com:8080
node server.js
```
This application can immediately be run on Heroku, see https://devcenter.heroku.com/articles/nodejs
for instructions. Note that their [Acceptable Use Policy](https://www.heroku.com/policy/aup) forbids
the use of Heroku for operating an open proxy, so make sure that you either enforce a whitelist as
shown above, or severly rate-limit the number of requests.
For example, to blacklist abuse.example.com and rate-limit everything to 50 requests per 3 minutes,
except for my.example.com and my2.example.com (which may be unlimited), use:
```
export PORT=8080
export CORSANYWHERE_BLACKLIST=https://abuse.example.com,http://abuse.example.com
export CORSANYWHERE_RATELIMIT='50 3 my.example.com my2.example.com'
node server.js
```
## License
Copyright (C) 2013 - 2016 Rob Wu <rob@robwu.nl>
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
of the Software, and to permit persons to whom the Software is furnished to do
so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

107
cors-anywhere/demo.html Normal file
View File

@@ -0,0 +1,107 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Demo of CORS Anywhere</title>
<meta name="viewport" content="width=device-width">
<style>
html, body {
margin: 0;
height: 100%;
padding: 3px;
font-family: Arial, sans-serif;
font-size: 16px;
}
* {
-moz-box-sizing: border-box;
box-sizing: border-box;
}
label { display: block; }
input {
display: block;
width: 100%;
padding: 8px 5px;
border: 1px solid #CCC;
}
button {
display: inline-block;
width: 49%;
padding: 8px;
}
textarea {
width: 100%;
height: 100%;
}
#top {
height: 180px;
position: relative;
}
#bottom {
height: 100%;
margin-top: -180px;
padding-top: 180px;
}
</style>
</head>
<body>
<div id="top">
CORS Anywhere demo &bull; <a href="https://github.com/Rob--W/cors-anywhere/">Github</a> &bull; <a href="http://cors-anywhere.herokuapp.com">Live server</a>.
<label>
Url to be fetched (example: <a href="//robwu.nl/dump.php">robwu.nl/dump.php</a>)
<input type="url" id="url" value="">
</label>
<label>
If using POST, enter the data:
<input type="text" id="data">
</label>
<label>
<button id="get">GET</button><button id="post">POST</button>
</label>
</div>
<div id="bottom">
<textarea id="output"></textarea>
</div>
<script>
var cors_api_url = "https://zinomedia.de:8088/";
function doCORSRequest(options, printResult) {
var x = new XMLHttpRequest();
x.open(options.method, cors_api_url + options.url);
x.onload = x.onerror = function() {
printResult(
options.method + ' ' + options.url + '\n' +
x.status + ' ' + x.statusText + '\n\n' +
(x.responseText || '')
);
};
if (/^POST/i.test(options.method)) {
x.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
}
x.send(options.data);
}
// Bind event
(function() {
var urlField = document.getElementById('url');
var dataField = document.getElementById('data');
var outputField = document.getElementById('output');
document.getElementById('get').onclick =
document.getElementById('post').onclick = function(e) {
e.preventDefault();
doCORSRequest({
method: this.id === 'post' ? 'POST' : 'GET',
url: urlField.value,
data: dataField.value
}, function printResult(result) {
outputField.value = result;
});
};
})();
if (typeof console === 'object') {
console.log('// To test a local CORS Anywhere server, set cors_api_url. For example:');
console.log('cors_api_url = "http://localhost:8080/"');
}
</script>
</body>
</html>

View File

@@ -0,0 +1,431 @@
// © 2013 - 2016 Rob Wu <rob@robwu.nl>
// Released under the MIT license
'use strict';
var httpProxy = require('http-proxy');
var net = require('net');
var url = require('url');
var regexp_tld = require('./regexp-top-level-domain');
var getProxyForUrl = require('proxy-from-env').getProxyForUrl;
var help_text = {};
function showUsage(help_file, headers, response) {
var isHtml = /\.html$/.test(help_file);
headers['content-type'] = isHtml ? 'text/html' : 'text/plain';
if (help_text[help_file] != null) {
response.writeHead(200, headers);
response.end(help_text[help_file]);
} else {
require('fs').readFile(help_file, 'utf8', function(err, data) {
if (err) {
console.error(err);
response.writeHead(500, headers);
response.end();
} else {
help_text[help_file] = data;
showUsage(help_file, headers, response); // Recursive call, but since data is a string, the recursion will end
}
});
}
}
/**
* Check whether the specified hostname is valid.
*
* @param hostname {string} Host name (excluding port) of requested resource.
* @return {boolean} Whether the requested resource can be accessed.
*/
function isValidHostName(hostname) {
return !!(
regexp_tld.test(hostname) ||
net.isIPv4(hostname) ||
net.isIPv6(hostname)
);
}
/**
* Adds CORS headers to the response headers.
*
* @param headers {object} Response headers
* @param request {ServerRequest}
*/
function withCORS(headers, request) {
headers['access-control-allow-origin'] = '*';
var corsMaxAge = request.corsAnywhereRequestState.corsMaxAge;
if (corsMaxAge) {
headers['access-control-max-age'] = corsMaxAge;
}
if (request.headers['access-control-request-method']) {
headers['access-control-allow-methods'] = request.headers['access-control-request-method'];
delete request.headers['access-control-request-method'];
}
if (request.headers['access-control-request-headers']) {
headers['access-control-allow-headers'] = request.headers['access-control-request-headers'];
delete request.headers['access-control-request-headers'];
}
headers['access-control-expose-headers'] = Object.keys(headers).join(',');
return headers;
}
/**
* Performs the actual proxy request.
*
* @param req {ServerRequest} Incoming http request
* @param res {ServerResponse} Outgoing (proxied) http request
* @param proxy {HttpProxy}
*/
function proxyRequest(req, res, proxy) {
var location = req.corsAnywhereRequestState.location;
req.url = location.path;
var proxyOptions = {
changeOrigin: false,
prependPath: false,
target: location,
headers: {
host: location.host,
},
// HACK: Get hold of the proxyReq object, because we need it later.
// https://github.com/nodejitsu/node-http-proxy/blob/v1.11.1/lib/http-proxy/passes/web-incoming.js#L144
buffer: {
pipe: function(proxyReq) {
var proxyReqOn = proxyReq.on;
// Intercepts the handler that connects proxyRes to res.
// https://github.com/nodejitsu/node-http-proxy/blob/v1.11.1/lib/http-proxy/passes/web-incoming.js#L146-L158
proxyReq.on = function(eventName, listener) {
if (eventName !== 'response') {
return proxyReqOn.call(this, eventName, listener);
}
return proxyReqOn.call(this, 'response', function(proxyRes) {
if (onProxyResponse(proxy, proxyReq, proxyRes, req, res)) {
try {
listener(proxyRes);
} catch (err) {
// Wrap in try-catch because an error could occur:
// "RangeError: Invalid status code: 0"
// https://github.com/Rob--W/cors-anywhere/issues/95
// https://github.com/nodejitsu/node-http-proxy/issues/1080
// Forward error (will ultimately emit the 'error' event on our proxy object):
// https://github.com/nodejitsu/node-http-proxy/blob/v1.11.1/lib/http-proxy/passes/web-incoming.js#L134
proxyReq.emit('error', err);
}
}
});
};
return req.pipe(proxyReq);
},
},
};
var proxyThroughUrl = req.corsAnywhereRequestState.getProxyForUrl(location.href);
if (proxyThroughUrl) {
proxyOptions.target = proxyThroughUrl;
proxyOptions.toProxy = true;
// If a proxy URL was set, req.url must be an absolute URL. Then the request will not be sent
// directly to the proxied URL, but through another proxy.
req.url = location.href;
}
// Start proxying the request
try {
proxy.web(req, res, proxyOptions);
} catch (err) {
proxy.emit('error', err, req, res);
}
}
/**
* This method modifies the response headers of the proxied response.
* If a redirect is detected, the response is not sent to the client,
* and a new request is initiated.
*
* client (req) -> CORS Anywhere -> (proxyReq) -> other server
* client (res) <- CORS Anywhere <- (proxyRes) <- other server
*
* @param proxy {HttpProxy}
* @param proxyReq {ClientRequest} The outgoing request to the other server.
* @param proxyRes {ServerResponse} The response from the other server.
* @param req {IncomingMessage} Incoming HTTP request, augmented with property corsAnywhereRequestState
* @param req.corsAnywhereRequestState {object}
* @param req.corsAnywhereRequestState.location {object} See parseURL
* @param req.corsAnywhereRequestState.getProxyForUrl {function} See proxyRequest
* @param req.corsAnywhereRequestState.proxyBaseUrl {string} Base URL of the CORS API endpoint
* @param req.corsAnywhereRequestState.maxRedirects {number} Maximum number of redirects
* @param req.corsAnywhereRequestState.redirectCount_ {number} Internally used to count redirects
* @param res {ServerResponse} Outgoing response to the client that wanted to proxy the HTTP request.
*
* @returns {boolean} true if http-proxy should continue to pipe proxyRes to res.
*/
function onProxyResponse(proxy, proxyReq, proxyRes, req, res) {
var requestState = req.corsAnywhereRequestState;
var statusCode = proxyRes.statusCode;
if (!requestState.redirectCount_) {
res.setHeader('x-request-url', requestState.location.href);
}
// Handle redirects
if (statusCode === 301 || statusCode === 302 || statusCode === 303 || statusCode === 307 || statusCode === 308) {
var locationHeader = proxyRes.headers.location;
if (locationHeader) {
locationHeader = url.resolve(requestState.location.href, locationHeader);
if (statusCode === 301 || statusCode === 302 || statusCode === 303) {
// Exclude 307 & 308, because they are rare, and require preserving the method + request body
requestState.redirectCount_ = requestState.redirectCount_ + 1 || 1;
if (requestState.redirectCount_ <= requestState.maxRedirects) {
// Handle redirects within the server, because some clients (e.g. Android Stock Browser)
// cancel redirects.
// Set header for debugging purposes. Do not try to parse it!
res.setHeader('X-CORS-Redirect-' + requestState.redirectCount_, statusCode + ' ' + locationHeader);
req.method = 'GET';
req.headers['content-length'] = '0';
delete req.headers['content-type'];
requestState.location = parseURL(locationHeader);
// Remove all listeners (=reset events to initial state)
req.removeAllListeners();
// Remove the error listener so that the ECONNRESET "error" that
// may occur after aborting a request does not propagate to res.
// https://github.com/nodejitsu/node-http-proxy/blob/v1.11.1/lib/http-proxy/passes/web-incoming.js#L134
proxyReq.removeAllListeners('error');
proxyReq.once('error', function catchAndIgnoreError() {});
proxyReq.abort();
// Initiate a new proxy request.
proxyRequest(req, res, proxy);
return false;
}
}
proxyRes.headers.location = requestState.proxyBaseUrl + '/' + locationHeader;
}
}
// Strip cookies
delete proxyRes.headers['set-cookie'];
delete proxyRes.headers['set-cookie2'];
proxyRes.headers['x-final-url'] = requestState.location.href;
withCORS(proxyRes.headers, req);
return true;
}
/**
* @param req_url {string} The requested URL (scheme is optional).
* @return {object} URL parsed using url.parse
*/
function parseURL(req_url) {
var match = req_url.match(/^(?:(https?:)?\/\/)?(([^\/?]+?)(?::(\d{0,5})(?=[\/?]|$))?)([\/?][\S\s]*|$)/i);
// ^^^^^^^ ^^^^^^^^ ^^^^^^^ ^^^^^^^^^^^^
// 1:protocol 3:hostname 4:port 5:path + query string
// ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
// 2:host
if (!match) {
return null;
}
if (!match[1]) {
// Scheme is omitted.
if (req_url.lastIndexOf('//', 0) === -1) {
// "//" is omitted.
req_url = '//' + req_url;
}
req_url = (match[4] === '443' ? 'https:' : 'http:') + req_url;
}
return url.parse(req_url);
}
// Request handler factory
function getHandler(options, proxy) {
var corsAnywhere = {
getProxyForUrl: getProxyForUrl, // Function that specifies the proxy to use
maxRedirects: 5, // Maximum number of redirects to be followed.
originBlacklist: [], // Requests from these origins will be blocked.
originWhitelist: [], // If non-empty, requests not from an origin in this list will be blocked.
checkRateLimit: null, // Function that may enforce a rate-limit by returning a non-empty string.
redirectSameOrigin: false, // Redirect the client to the requested URL for same-origin requests.
requireHeader: null, // Require a header to be set?
removeHeaders: [], // Strip these request headers.
setHeaders: {}, // Set these request headers.
corsMaxAge: 0, // If set, an Access-Control-Max-Age header with this value (in seconds) will be added.
helpFile: __dirname + '/help.txt',
};
Object.keys(corsAnywhere).forEach(function(option) {
if (Object.prototype.hasOwnProperty.call(options, option)) {
corsAnywhere[option] = options[option];
}
});
// Convert corsAnywhere.requireHeader to an array of lowercase header names, or null.
if (corsAnywhere.requireHeader) {
if (typeof corsAnywhere.requireHeader === 'string') {
corsAnywhere.requireHeader = [corsAnywhere.requireHeader.toLowerCase()];
} else if (!Array.isArray(corsAnywhere.requireHeader) || corsAnywhere.requireHeader.length === 0) {
corsAnywhere.requireHeader = null;
} else {
corsAnywhere.requireHeader = corsAnywhere.requireHeader.map(function(headerName) {
return headerName.toLowerCase();
});
}
}
var hasRequiredHeaders = function(headers) {
return !corsAnywhere.requireHeader || corsAnywhere.requireHeader.some(function(headerName) {
return Object.hasOwnProperty.call(headers, headerName);
});
};
return function(req, res) {
req.corsAnywhereRequestState = {
getProxyForUrl: corsAnywhere.getProxyForUrl,
maxRedirects: corsAnywhere.maxRedirects,
corsMaxAge: corsAnywhere.corsMaxAge,
};
var cors_headers = withCORS({}, req);
if (req.method === 'OPTIONS') {
// Pre-flight request. Reply successfully:
res.writeHead(200, cors_headers);
res.end();
return;
}
var location = parseURL(req.url.slice(1));
if (!location) {
// Invalid API call. Show how to correctly use the API
showUsage(corsAnywhere.helpFile, cors_headers, res);
return;
}
if (location.host === 'iscorsneeded') {
// Is CORS needed? This path is provided so that API consumers can test whether it's necessary
// to use CORS. The server's reply is always No, because if they can read it, then CORS headers
// are not necessary.
res.writeHead(200, {'Content-Type': 'text/plain'});
res.end('no');
return;
}
if (location.port > 65535) {
// Port is higher than 65535
res.writeHead(400, 'Invalid port', cors_headers);
res.end('Port number too large: ' + location.port);
return;
}
if (!/^\/https?:/.test(req.url) && !isValidHostName(location.hostname)) {
// Don't even try to proxy invalid hosts (such as /favicon.ico, /robots.txt)
res.writeHead(404, 'Invalid host', cors_headers);
res.end('Invalid host: ' + location.hostname);
return;
}
if (!hasRequiredHeaders(req.headers)) {
res.writeHead(400, 'Header required', cors_headers);
res.end('Missing required request header. Must specify one of: ' + corsAnywhere.requireHeader);
return;
}
var origin = req.headers.origin || '';
if (corsAnywhere.originBlacklist.indexOf(origin) >= 0) {
res.writeHead(403, 'Forbidden', cors_headers);
res.end('The origin "' + origin + '" was blacklisted by the operator of this proxy.');
return;
}
if (corsAnywhere.originWhitelist.length && corsAnywhere.originWhitelist.indexOf(origin) === -1) {
res.writeHead(403, 'Forbidden', cors_headers);
res.end('The origin "' + origin + '" was not whitelisted by the operator of this proxy.');
return;
}
var rateLimitMessage = corsAnywhere.checkRateLimit && corsAnywhere.checkRateLimit(origin);
if (rateLimitMessage) {
res.writeHead(429, 'Too Many Requests', cors_headers);
res.end('The origin "' + origin + '" has sent too many requests.\n' + rateLimitMessage);
return;
}
if (corsAnywhere.redirectSameOrigin && origin && location.href[origin.length] === '/' &&
location.href.lastIndexOf(origin, 0) === 0) {
// Send a permanent redirect to offload the server. Badly coded clients should not waste our resources.
cors_headers.vary = 'origin';
cors_headers['cache-control'] = 'private';
cors_headers.location = location.href;
res.writeHead(301, 'Please use a direct request', cors_headers);
res.end();
return;
}
var isRequestedOverHttps = req.connection.encrypted || /^\s*https/.test(req.headers['x-forwarded-proto']);
var proxyBaseUrl = (isRequestedOverHttps ? 'https://' : 'http://') + req.headers.host;
corsAnywhere.removeHeaders.forEach(function(header) {
delete req.headers[header];
});
Object.keys(corsAnywhere.setHeaders).forEach(function(header) {
req.headers[header] = corsAnywhere.setHeaders[header];
});
req.corsAnywhereRequestState.location = location;
req.corsAnywhereRequestState.proxyBaseUrl = proxyBaseUrl;
proxyRequest(req, res, proxy);
};
}
// Create server with default and given values
// Creator still needs to call .listen()
exports.createServer = function createServer(options) {
options = options || {};
// Default options:
var httpProxyOptions = {
xfwd: true, // Append X-Forwarded-* headers
};
// Allow user to override defaults and add own options
if (options.httpProxyOptions) {
Object.keys(options.httpProxyOptions).forEach(function(option) {
httpProxyOptions[option] = options.httpProxyOptions[option];
});
}
var proxy = httpProxy.createServer(httpProxyOptions);
var requestHandler = getHandler(options, proxy);
var server;
if (options.httpsOptions) {
server = require('https').createServer(options.httpsOptions, requestHandler);
} else {
server = require('http').createServer(requestHandler);
}
// When the server fails, just show a 404 instead of Internal server error
proxy.on('error', function(err, req, res) {
if (res.headersSent) {
// This could happen when a protocol error occurs when an error occurs
// after the headers have been received (and forwarded). Do not write
// the headers because it would generate an error.
return;
}
// When the error occurs after setting headers but before writing the response,
// then any previously set headers must be removed.
var headerNames = res.getHeaderNames ? res.getHeaderNames() : Object.keys(res._headers || {});
headerNames.forEach(function(name) {
res.removeHeader(name);
});
res.writeHead(404, {'Access-Control-Allow-Origin': '*'});
res.end('Not found because of proxy error: ' + err);
});
return server;
};

View File

@@ -0,0 +1,30 @@
This API enables cross-origin requests to anywhere.
Usage:
/ Shows help
/iscorsneeded This is the only resource on this host which is served without CORS headers.
/<url> Create a request to <url>, and includes CORS headers in the response.
If the protocol is omitted, it defaults to http (https if port 443 is specified).
Cookies are disabled and stripped from requests.
Redirects are automatically followed. For debugging purposes, each followed redirect results
in the addition of a X-CORS-Redirect-n header, where n starts at 1. These headers are not
accessible by the XMLHttpRequest API.
After 5 redirects, redirects are not followed any more. The redirect response is sent back
to the browser, which can choose to follow the redirect (handled automatically by the browser).
The requested URL is available in the X-Request-URL response header.
The final URL, after following all redirects, is available in the X-Final-URL response header.
To prevent the use of the proxy for casual browsing, the API requires either the Origin
or the X-Requested-With header to be set. To avoid unnecessary preflight (OPTIONS) requests,
it's recommended to not manually set these headers in your code.
Demo : https://robwu.nl/cors-anywhere.html
Source code : https://github.com/Rob--W/cors-anywhere/
Documentation : https://github.com/Rob--W/cors-anywhere/#documentation

View File

@@ -0,0 +1,74 @@
'use strict';
module.exports = function createRateLimitChecker(CORSANYWHERE_RATELIMIT) {
// Configure rate limit. The following format is accepted for CORSANYWHERE_RATELIMIT:
// <max requests per period> <period in minutes> <non-ratelimited hosts>
// where <non-ratelimited hosts> is a space-separated list of strings or regexes (/.../) that
// matches the whole host (ports have to be listed explicitly if applicable).
// <period in minutes> cannot be zero.
//
// Examples:
// - Allow any origin to make one request per 5 minutes:
// 1 5
//
// - Allow example.com to make an unlimited number of requests, and the others 1 per 5 minutes.
// 1 5 example.com
//
// - Allow example.com, or any subdomain to make any number of requests and block the rest:
// 0 1 /(.*\.)?example\.com/
//
// - Allow example.com and www.example.com, and block the rest:
// 0 1 example.com www.example.com
var rateLimitConfig = /^(\d+) (\d+)(?:\s*$|\s+(.+)$)/.exec(CORSANYWHERE_RATELIMIT);
if (!rateLimitConfig) {
// No rate limit by default.
return function checkRateLimit() {};
}
var maxRequestsPerPeriod = parseInt(rateLimitConfig[1]);
var periodInMinutes = parseInt(rateLimitConfig[2]);
var unlimitedPattern = rateLimitConfig[3]; // Will become a RegExp or void.
if (unlimitedPattern) {
var unlimitedPatternParts = [];
unlimitedPattern.trim().split(/\s+/).forEach(function(unlimitedHost, i) {
var startsWithSlash = unlimitedHost.charAt(0) === '/';
var endsWithSlash = unlimitedHost.slice(-1) === '/';
if (startsWithSlash || endsWithSlash) {
if (unlimitedHost.length === 1 || !startsWithSlash || !endsWithSlash) {
throw new Error('Invalid CORSANYWHERE_RATELIMIT. Regex at index ' + i +
' must start and end with a slash ("/").');
}
unlimitedHost = unlimitedHost.slice(1, -1);
// Throws if the pattern is invalid.
new RegExp(unlimitedHost);
} else {
// Just escape RegExp characters even though they cannot appear in a host name.
// The only actual important escape is the dot.
unlimitedHost = unlimitedHost.replace(/[$()*+.?[\\\]^{|}]/g, '\\$&');
}
unlimitedPatternParts.push(unlimitedHost);
});
unlimitedPattern = new RegExp('^(?:' + unlimitedPatternParts.join('|') + ')$', 'i');
}
var accessedHosts = Object.create(null);
setInterval(function() {
accessedHosts = Object.create(null);
}, periodInMinutes * 60000);
var rateLimitMessage = 'The number of requests is limited to ' + maxRequestsPerPeriod +
(periodInMinutes === 1 ? ' per minute' : ' per ' + periodInMinutes + ' minutes') + '. ' +
'Please self-host CORS Anywhere if you need more quota. ' +
'See https://github.com/Rob--W/cors-anywhere#demo-server';
return function checkRateLimit(origin) {
var host = origin.replace(/^[\w\-]+:\/\//i, '');
if (unlimitedPattern && unlimitedPattern.test(host)) {
return;
}
var count = accessedHosts[host] || 0;
++count;
if (count > maxRequestsPerPeriod) {
return rateLimitMessage;
}
accessedHosts[host] = count;
};
};

File diff suppressed because one or more lines are too long

2024
cors-anywhere/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,51 @@
{
"name": "cors-anywhere",
"version": "0.4.1",
"description": "CORS Anywhere is a reverse proxy which adds CORS headers to the proxied request. Request URL is taken from the path",
"license": "MIT",
"author": "Rob Wu <rob@robwu.nl>",
"repository": {
"type": "git",
"url": "https://github.com/Rob--W/cors-anywhere.git"
},
"bugs": {
"url": "https://github.com/Rob--W/cors-anywhere/issues/",
"email": "rob@robwu.nl"
},
"keywords": [
"cors",
"cross-domain",
"http-proxy",
"proxy",
"heroku"
],
"main": "./lib/cors-anywhere.js",
"files": [
"lib/",
"test/",
"Procfile",
"demo.html",
"server.js"
],
"dependencies": {
"http-proxy": "1.11.1",
"proxy-from-env": "0.0.1"
},
"devDependencies": {
"coveralls": "^2.11.6",
"eslint": "^2.2.0",
"istanbul": "^0.4.2",
"lolex": "^1.5.0",
"mocha": "^3.4.2",
"nock": "^8.2.1",
"supertest": "^2.0.1"
},
"scripts": {
"lint": "eslint .",
"test": "mocha ./test/test*.js --reporter spec",
"test-coverage": "istanbul cover ./node_modules/.bin/_mocha -- test/test.js test/test-ratelimit.js --reporter spec"
},
"engines": {
"node": ">=0.10.0"
}
}

66
cors-anywhere/server.js Normal file
View File

@@ -0,0 +1,66 @@
// Listen on a specific host via the HOST environment variable
var host = process.env.HOST || '0.0.0.0';
// Listen on a specific port via the PORT environment variable
var port = process.env.PORT || 8088;
// Grab the blacklist from the command-line so that we can update the blacklist without deploying
// again. CORS Anywhere is open by design, and this blacklist is not used, except for countering
// immediate abuse (e.g. denial of service). If you want to block all origins except for some,
// use originWhitelist instead.
var originBlacklist = parseEnvList(process.env.CORSANYWHERE_BLACKLIST);
var originWhitelist = parseEnvList(process.env.CORSANYWHERE_WHITELIST);
function parseEnvList(env) {
if (!env) {
return [];
}
return env.split(',');
}
// Set up rate-limiting to avoid abuse of the public CORS Anywhere server.
var checkRateLimit = require('./lib/rate-limit')(process.env.CORSANYWHERE_RATELIMIT);
// console.log(checkRateLimit);
// var cors_proxy = require('./lib/cors-anywhere');
// cors_proxy.createServer({
// originBlacklist: originBlacklist,
// originWhitelist: originWhitelist,
// // requireHeader: ['origin', 'x-requested-with'],
// // checkRateLimit: checkRateLimit,
// removeHeaders: [
// 'cookie',
// 'cookie2',
// // Strip Heroku-specific headers
// 'x-heroku-queue-wait-time',
// 'x-heroku-queue-depth',
// 'x-heroku-dynos-in-use',
// 'x-request-start',
// ],
// redirectSameOrigin: true,
// httpProxyOptions: {
// // Do not add X-Forwarded-For, etc. headers, because Heroku already adds it.
// xfwd: false,
// },
// }).listen(port, host, function() {
// console.log('Running CORS Anywhere on ' + host + ':' + port);
// });
var fs = require('fs');
var host = process.env.HOST || '0.0.0.0';
var port = process.env.PORT || 8088;
var cors_proxy = require('./lib/cors-anywhere');
cors_proxy.createServer({
httpsOptions: {
key: fs.readFileSync('/etc/letsencrypt/live/zinomedia.de/privkey.pem'),
cert: fs.readFileSync('/etc/letsencrypt/live/zinomedia.de/fullchain.pem')
},
// originWhitelist: ['https://heise.de'],
originWhitelist: ['https://zinomedia.de' , 'https://purchase.tickets.com', 'http://purchase.tickets.com', 'http://zinomedia.de', 'https://purchase.tickets.zinomedia.de', 'http://purchase.tickets.zinomedia.de', 'http://seatmap-testing.zinomedia.de', 'https://seatmap-testing.zinomedia.de'],
// requireHeader: ['origin', 'x-requested-with'],
requireHeader: [],
removeHeaders: ['cookie', 'cookie2'],
// checkRateLimit: checkRateLimit,
}).listen(port, host, function() {
console.log('Running CORS Anywhere on ' + host + ':' + port);
});

View File

@@ -0,0 +1,12 @@
-----BEGIN CERTIFICATE-----
MIIBsTCCARoCCQDp0DuED0RAJzANBgkqhkiG9w0BAQsFADAdMRswGQYDVQQDDBJj
b3JzLWFueXdoZXJlIHRlc3QwHhcNMTUwNTA2MDcyOTM1WhcNMTUwNjA1MDcyOTM1
WjAdMRswGQYDVQQDDBJjb3JzLWFueXdoZXJlIHRlc3QwgZ8wDQYJKoZIhvcNAQEB
BQADgY0AMIGJAoGBALzTF5ClJKvkB6h9h7kLORV+mMV3ySDs+oGZn0NgXM+yb9Zh
69r5e95zZJl/V432LFdy0hkEcVteUkC2REWG8D4COGfiwWsXyZdaP1qqLpDpPAMm
v6xFHjW6rVuxzfr4GUjE0Zh9Fg2R2SbtCOcHS/LZoDVOqOvn6+urP6XFY4aFAgMB
AAEwDQYJKoZIhvcNAQELBQADgYEAYXMhS8ouff/c8lSUUs/CLh010cj5RPk/ivS7
aN2PArzQ6pZvhpgJKf7XAQksBtLYYZMzIpG6W8zhPSbqzly7lELAdE+sxcbbfu8A
FMjNVFQ2Fm1c8ImX8qpE3nhVrPAiwfPjGBqKHTl730gvbh1XH9TC4O4dZcbEomX3
5MsxQfc=
-----END CERTIFICATE-----

View File

@@ -0,0 +1,64 @@
// When this module is loaded, CORS Anywhere is started.
// Then, a request is generated to warm up the server (just in case).
// Then the base URL of CORS Anywhere is sent to the parent process.
// ...
// When the parent process is done, it sends an empty message to this child
// process, which in turn records the change in used heap space.
// The difference in heap space is finally sent back to the parent process.
// ...
// The parent process should then kill this child.
process.on('uncaughtException', function(e) {
console.error('Uncaught exception in child process: ' + e);
console.error(e.stack);
process.exit(-1);
});
// Invoke memoryUsage() without using its result to make sure that any internal
// datastructures that supports memoryUsage() is initialized and won't pollute
// the memory usage measurement later on.
process.memoryUsage();
var heapUsedStart = 0;
function getMemoryUsage(callback) {
// Note: Requires --expose-gc
// 6 is the minimum amount of gc() calls before calling gc() again does not
// reduce memory any more.
for (var i = 0; i < 6; ++i) {
global.gc();
}
callback(process.memoryUsage().heapUsed);
}
var server;
if (process.argv.indexOf('use-http-instead-of-cors-anywhere') >= 0) {
server = require('http').createServer(function(req, res) { res.end(); });
} else {
server = require('../').createServer();
}
server.listen(0, function() {
// Perform 1 request to warm up.
require('http').get({
hostname: '127.0.0.1',
port: server.address().port,
path: '/http://invalid:99999',
agent: false,
}, function() {
notifyParent();
});
function notifyParent() {
getMemoryUsage(function(usage) {
heapUsedStart = usage;
process.send('http://127.0.0.1:' + server.address().port + '/');
});
}
});
process.once('message', function() {
getMemoryUsage(function(heapUsedEnd) {
var delta = heapUsedEnd - heapUsedStart;
process.send(delta);
});
});

View File

@@ -0,0 +1,7 @@
<html>
<head>
</head>
<body>
<h2>Custom HTML help!!</h2>
</body>
</html>

View File

@@ -0,0 +1 @@
Server is OK!!

View File

@@ -0,0 +1 @@
dummy content

View File

@@ -0,0 +1,15 @@
-----BEGIN RSA PRIVATE KEY-----
MIICXQIBAAKBgQC80xeQpSSr5AeofYe5CzkVfpjFd8kg7PqBmZ9DYFzPsm/WYeva
+Xvec2SZf1eN9ixXctIZBHFbXlJAtkRFhvA+Ajhn4sFrF8mXWj9aqi6Q6TwDJr+s
RR41uq1bsc36+BlIxNGYfRYNkdkm7QjnB0vy2aA1Tqjr5+vrqz+lxWOGhQIDAQAB
AoGBAISy8OelN01Zlowxk/VWTsqtSl3UHdP21uHHfWaTTQZlxzTpYiBknkmp3LQH
CxfoPidCuSX9ulBUzAdQUFBwUVp8wyPIRjpNyRiD58dLNxG0G+OACqnLxNWqIf6F
vS3UqrRGIA5u+GSz+0g3DAeVA5JmsAyHQGkJsh3pcuD8/7wNAkEA7MScGfySy9td
dDBekVU5/GaVg4DA4ELtDNfa99ARB89XP0ps/XrOPEL9yxTjWIHH+qxuhpfG6zGN
ouxZlvBT9wJBAMwpig4A4JE8M8pBDwMY4213gud8B1grQTbhz5bv51aTaIEQFcxw
sGfEmAfVToI+kVTrdFggy42YCSMSvwuF4mMCQQDZHkqPwf/TlSwT2i8+UstD28aL
uswkWvsKZf9UdKbJZKd7UIK1x6HLvRsC2frJNOnvw6PvJMuy7dQWbWqScXxtAkBv
/5msdO68vbnriiUiHdUliBpXwsKEq7Xq1ZV7x7+wzszVgG106ZzcUAzWvz2CVbCE
VWZNsi/4TR82DmKff6LhAkBA/xceWaZjxh5dkWkIrMFWd2GFhGlpfwYw7oELwRL8
RYXzc1Mr2fDdZDgwgjg67JQqIhOQ3E4RGKPgZ+E7Pk3/
-----END RSA PRIVATE KEY-----

152
cors-anywhere/test/setup.js Normal file
View File

@@ -0,0 +1,152 @@
var nock = require('nock');
if (parseInt(process.versions.node, 10) >= 8) {
// See DEP0066 at https://nodejs.org/api/deprecations.html.
// _headers and _headerNames have been removed from Node v8, which causes
// nock <= 9.0.13 to fail. The snippet below monkey-patches the library, see
// https://github.com/node-nock/nock/pull/929/commits/f6369d0edd2a172024124f
// for the equivalent logic without proxies.
Object.defineProperty(require('http').ClientRequest.prototype, '_headers', {
get: function() {
var request = this;
// eslint-disable-next-line no-undef
return new Proxy(request.getHeaders(), {
set: function(target, property, value) {
request.setHeader(property, value);
return true;
},
});
},
set: function() {
// Ignore.
},
});
}
nock.enableNetConnect('127.0.0.1');
function echoheaders(origin) {
nock(origin)
.persist()
.get('/echoheaders')
.reply(function() {
var headers = this.req.headers;
var excluded_headers = [
'accept-encoding',
'user-agent',
'connection',
// Remove this header since its value is platform-specific.
'x-forwarded-for',
'test-include-xfwd',
];
if (!('test-include-xfwd' in headers)) {
excluded_headers.push('x-forwarded-port');
excluded_headers.push('x-forwarded-proto');
}
var response = {};
Object.keys(headers).forEach(function(name) {
if (excluded_headers.indexOf(name) === -1) {
response[name] = headers[name];
}
});
return response;
});
}
nock('http://example.com')
.persist()
.get('/')
.reply(200, 'Response from example.com')
.post('/echopost')
.reply(200, function(uri, requestBody) {
return requestBody;
})
.get('/setcookie')
.reply(200, '', {
'Set-Cookie': 'x',
'Set-Cookie2': 'y',
'Set-Cookie3': 'z', // This is not a special cookie setting header.
})
.get('/redirecttarget')
.reply(200, 'redirect target', {
'Some-header': 'value',
})
.head('/redirect')
.reply(302, '', {
Location: '/redirecttarget',
})
.get('/redirect')
.reply(302, 'redirecting...', {
'header at redirect': 'should not be here',
Location: '/redirecttarget',
})
.get('/redirectposttarget')
.reply(200, 'post target')
.post('/redirectposttarget')
.reply(200, 'post target (POST)')
.post('/redirectpost')
.reply(302, 'redirecting...', {
Location: '/redirectposttarget',
})
.post('/redirect307')
.reply(307, 'redirecting...', {
Location: '/redirectposttarget',
})
.get('/redirect2redirect')
.reply(302, 'redirecting to redirect...', {
Location: '/redirect',
})
.get('/redirectloop')
.reply(302, 'redirecting ad infinitum...', {
Location: '/redirectloop',
})
.get('/redirectwithoutlocation')
.reply(302, 'maybe found')
.get('/proxyerror')
.replyWithError('throw node')
;
nock('https://example.com')
.persist()
.get('/')
.reply(200, 'Response from https://example.com')
;
nock('http://example.com.com')
.persist()
.get('/')
.reply(200, 'Response from example.com.com')
;
nock('http://example.com:1234')
.persist()
.get('/')
.reply(200, 'Response from example.com:1234')
;
nock('http://prefix.example.com')
.persist()
.get('/')
.reply(200, 'Response from prefix.example.com')
;
echoheaders('http://example.com');
echoheaders('http://example.com:1337');
echoheaders('https://example.com');
echoheaders('https://example.com:1337');
nock('http://robots.txt')
.get('/')
.reply(200, 'this is http://robots.txt');

View File

@@ -0,0 +1,133 @@
/**
* CORS Anywhere is designed for use as a standalone server. Sometimes you want
* to have extra functionality on top of the default CORS server. If it may be
* useful to others, please open a feature request on the issue tracker at
* https://github.com/Rob--W/cors-anywhere/issues.
*
* If it is only useful to your application, look below for some examples.
* These examples are provided as-is without guarantees. Use at your own risk.
*/
/* eslint-env mocha */
require('./setup');
var createServer = require('../').createServer;
var assert = require('assert');
var request = require('supertest');
var http = require('http');
describe('Examples', function() {
// Note: In the examples below we don't listen on any port after calling
// createServer() because it is not needed to start listening on a port if the
// CORS Anywhere is only used internally.
// And normally you have to listen on some port, like this:
//
// http_server.listen(port_number);
//
// But in these test, the call to request() automatically handles that part so
// the examples don't have an explicit .listen() call.
it('Rewrite proxy URL', function(done) {
var cors_anywhere = createServer();
var http_server = http.createServer(function(req, res) {
// For testing, check whether req.url is the same as what we input below.
assert.strictEqual(req.url, '/dummy-for-testing');
// Basic example: Always proxy example.com.
req.url = '/http://example.com';
cors_anywhere.emit('request', req, res);
});
request(http_server)
.get('/dummy-for-testing')
.expect('Access-Control-Allow-Origin', '*')
.expect('x-request-url', 'http://example.com/')
.expect(200, 'Response from example.com', done);
});
it('Transform response to uppercase (streaming)', function(done) {
var cors_anywhere = createServer();
var http_server = http.createServer(function(req, res) {
var originalWrite = res.write;
res.write = function(data, encoding, callback) {
if (Buffer.isBuffer(data)) {
data = data.toString();
}
assert.strictEqual(typeof data, 'string');
// This example shows how to transform the response to upper case.
data = data.toUpperCase();
originalWrite.call(this, data, encoding, callback);
};
cors_anywhere.emit('request', req, res);
});
request(http_server)
.get('/example.com')
.expect('Access-Control-Allow-Origin', '*')
.expect('x-request-url', 'http://example.com/')
.expect(200, 'RESPONSE FROM EXAMPLE.COM', done);
});
it('Transform response to uppercase (buffered)', function(done) {
var cors_anywhere = createServer();
var http_server = http.createServer(function(req, res) {
var originalWrite = res.write;
var originalEnd = res.end;
var buffers = [];
res.write = function(data, encoding, callback) {
assert.ok(Buffer.isBuffer(data) || typeof data === 'string');
buffers.push(data);
if (callback) {
process.nextTick(callback, null);
}
};
res.end = function(data, encoding, callback) {
if (data) {
this.write(data, encoding);
}
// After calling .end(), .write shouldn't be called any more. So let's
// restore it so that the default error handling for writing to closed
// streams would occur.
this.write = originalWrite;
// Combine all chunks. Note that we're assuming that all chunks are
// utf8 strings or buffers whose content is utf8-encoded. If this
// assumption is not true, then you have to update the .write method
// above.
data = buffers.join('');
// This example shows how to transform the response to upper case.
data = data.toUpperCase();
// .end should be called once, so let's restore it so that any default
// error handling occurs if it occurs again.
this.end = originalEnd;
this.end(data, 'utf8', callback);
};
cors_anywhere.emit('request', req, res);
});
request(http_server)
.get('/example.com')
.expect('Access-Control-Allow-Origin', '*')
.expect('x-request-url', 'http://example.com/')
.expect(200, 'RESPONSE FROM EXAMPLE.COM', done);
});
});

View File

@@ -0,0 +1,111 @@
/* eslint-env mocha */
// Run this specific test using:
// npm test -- -f memory
var http = require('http');
var path = require('path');
var url = require('url');
var fork = require('child_process').fork;
describe('memory usage', function() {
var cors_api_url;
var server;
var cors_anywhere_child;
before(function(done) {
server = http.createServer(function(req, res) {
res.writeHead(200);
res.end();
}).listen(0, function() {
done();
});
});
after(function(done) {
server.close(function() {
done();
});
});
beforeEach(function(done) {
var cors_module_path = path.join(__dirname, 'child');
var args = [];
// Uncomment this if you want to compare the performance of CORS Anywhere
// with the standard no-op http module.
// args.push('use-http-instead-of-cors-anywhere');
cors_anywhere_child = fork(cors_module_path, args, {
execArgv: ['--expose-gc'],
});
cors_anywhere_child.once('message', function(cors_url) {
cors_api_url = cors_url;
done();
});
});
afterEach(function() {
cors_anywhere_child.kill();
});
/**
* Perform N CORS Anywhere proxy requests to a simple test server.
*
* @param {number} n - number of repetitions.
* @param {number} requestSize - Approximate size of request in kilobytes.
* @param {number} memMax - Expected maximum memory usage in kilobytes.
* @param {function} done - Upon success, called without arguments.
* Upon failure, called with the error as parameter.
*/
function performNRequests(n, requestSize, memMax, done) {
var remaining = n;
var request = url.parse(
cors_api_url + 'http://127.0.0.1:' + server.address().port);
request.agent = false; // Force Connection: Close
request.headers = {
'Long-header': new Array(requestSize * 1e3).join('x'),
};
(function requestAgain() {
if (remaining-- === 0) {
cors_anywhere_child.once('message', function(memory_usage_delta) {
console.log('Memory usage delta: ' + memory_usage_delta +
' (' + n + ' requests of ' + requestSize + ' kb each)');
if (memory_usage_delta > memMax * 1e3) {
// Note: Even if this error is reached, always profile (e.g. using
// node-inspector) whether it is a true leak, and not e.g. noise
// caused by the implementation of V8/Node.js.
// Uncomment args.push('use-http-instead-of-cors-anywhere') at the
// fork() call to get a sense of what's normal.
throw new Error('Possible memory leak: ' + memory_usage_delta +
' bytes was not released, which exceeds the ' + memMax +
' kb limit by ' +
Math.round(memory_usage_delta / memMax / 10 - 100) + '%.');
}
done();
});
cors_anywhere_child.send(null);
return;
}
http.request(request, function() {
requestAgain();
}).on('error', function(error) {
done(error);
}).end();
})();
}
it('100 GET requests 50k', function(done) {
// This test is just for comparison with the following tests.
performNRequests(100, 50, 1200, done);
});
// 100x 1k and 100x 50k for comparison.
// Both should use about the same amount of memory if there is no leak.
it('1000 GET requests 1k', function(done) {
// Every request should complete within 10ms.
this.timeout(1000 * 10);
performNRequests(1000, 1, 2000, done);
});
it('1000 GET requests 50k', function(done) {
// Every request should complete within 10ms.
this.timeout(1000 * 10);
performNRequests(1000, 50, 2000, done);
});
});

View File

@@ -0,0 +1,231 @@
/* eslint-env mocha */
var createRateLimitChecker = require('../lib/rate-limit');
var lolex = require('lolex');
var assert = require('assert');
function assertNotLimited(rateLimitReturnValue) {
if (rateLimitReturnValue) {
assert.fail('Expected no limit, but got ' + rateLimitReturnValue);
}
}
function assertLimited(rateLimitReturnValue, limit, period) {
var msg;
if (period === 1) {
msg = 'The number of requests is limited to ' + limit + ' per minute. ';
} else {
msg = 'The number of requests is limited to ' + limit + ' per ' + period + ' minutes. ';
}
msg += 'Please self-host CORS Anywhere if you need more quota. ' +
'See https://github.com/Rob--W/cors-anywhere#demo-server';
assert.equal(rateLimitReturnValue, msg);
}
describe('Rate limit', function() {
var clock;
beforeEach(function() {
clock = lolex.install();
});
afterEach(function() {
clock.uninstall();
});
it('is unlimited by default', function() {
var checkRateLimit = createRateLimitChecker();
assertNotLimited(checkRateLimit('http://example.com'));
assertNotLimited(checkRateLimit('https://example.com'));
assertNotLimited(checkRateLimit('https://example.com:1234'));
checkRateLimit = createRateLimitChecker('');
assertNotLimited(checkRateLimit('http://example.com'));
checkRateLimit = createRateLimitChecker(' ');
assertNotLimited(checkRateLimit('http://example.com'));
});
it('zero per minute / 5 minutes', function() {
var checkRateLimit = createRateLimitChecker('0 1');
assertLimited(checkRateLimit('http://example.com'), 0, 1);
assertLimited(checkRateLimit('https://example.com'), 0, 1);
checkRateLimit = createRateLimitChecker('0 5');
assertLimited(checkRateLimit('http://example.com'), 0, 5);
assertLimited(checkRateLimit('https://example.com'), 0, 5);
});
it('one per minute', function() {
var checkRateLimit = createRateLimitChecker('1 1');
assertNotLimited(checkRateLimit('http://example.com'));
assertLimited(checkRateLimit('http://example.com'), 1, 1);
assertNotLimited(checkRateLimit('http://example.com:1234'));
assertLimited(checkRateLimit('http://example.com:1234'), 1, 1);
clock.tick(59000);
assertLimited(checkRateLimit('http://example.com'), 1, 1);
clock.tick(1000);
assertNotLimited(checkRateLimit('http://example.com'));
assertLimited(checkRateLimit('http://example.com'), 1, 1);
assertNotLimited(checkRateLimit('http://example.com:1234'));
assertLimited(checkRateLimit('http://example.com:1234'), 1, 1);
});
it('different domains, one per minute', function() {
var checkRateLimit = createRateLimitChecker('1 1');
assertNotLimited(checkRateLimit('http://example.com'));
assertNotLimited(checkRateLimit('http://example.net'));
assertNotLimited(checkRateLimit('http://wexample.net'));
assertNotLimited(checkRateLimit('http://xample.net'));
assertNotLimited(checkRateLimit('http://www.example.net'));
assertLimited(checkRateLimit('http://example.com'), 1, 1);
assertLimited(checkRateLimit('http://example.net'), 1, 1);
assertLimited(checkRateLimit('http://wexample.net'), 1, 1);
assertLimited(checkRateLimit('http://xample.net'), 1, 1);
assertLimited(checkRateLimit('http://www.example.net'), 1, 1);
clock.tick(60000); // 1 minute
assertNotLimited(checkRateLimit('http://example.com'));
assertNotLimited(checkRateLimit('http://example.net'));
assertNotLimited(checkRateLimit('http://wexample.net'));
assertNotLimited(checkRateLimit('http://xample.net'));
assertNotLimited(checkRateLimit('http://www.example.net'));
});
it('unlimited domains, string', function() {
var checkRateLimit = createRateLimitChecker('1 2 example.com');
assertNotLimited(checkRateLimit('http://example.com'));
assertNotLimited(checkRateLimit('http://example.com'));
assertNotLimited(checkRateLimit('http://wexample.com'));
assertNotLimited(checkRateLimit('http://xample.com'));
assertNotLimited(checkRateLimit('http://www.example.com'));
assertLimited(checkRateLimit('http://wexample.com'), 1, 2);
assertLimited(checkRateLimit('http://xample.com'), 1, 2);
assertLimited(checkRateLimit('http://www.example.com'), 1, 2);
});
it('unlimited domains, RegExp', function() {
var checkRateLimit = createRateLimitChecker('1 2 /example\\.com/');
assertNotLimited(checkRateLimit('http://example.com'));
assertNotLimited(checkRateLimit('http://example.com'));
assertNotLimited(checkRateLimit('http://wexample.com'));
assertNotLimited(checkRateLimit('http://xample.com'));
assertNotLimited(checkRateLimit('http://www.example.com'));
assertLimited(checkRateLimit('http://wexample.com'), 1, 2);
assertLimited(checkRateLimit('http://xample.com'), 1, 2);
assertLimited(checkRateLimit('http://www.example.com'), 1, 2);
});
it('multiple domains, string', function() {
var checkRateLimit = createRateLimitChecker('1 2 a b cc ');
assertNotLimited(checkRateLimit('http://a'));
assertNotLimited(checkRateLimit('http://a'));
assertNotLimited(checkRateLimit('http://b'));
assertNotLimited(checkRateLimit('http://b'));
assertNotLimited(checkRateLimit('http://cc'));
assertNotLimited(checkRateLimit('http://cc'));
assertNotLimited(checkRateLimit('http://c'));
assertLimited(checkRateLimit('http://c'), 1, 2);
});
it('multiple domains, RegExp', function() {
var checkRateLimit = createRateLimitChecker('1 2 /a/ /b/ /cc/ ');
assertNotLimited(checkRateLimit('http://a'));
assertNotLimited(checkRateLimit('http://a'));
assertNotLimited(checkRateLimit('http://b'));
assertNotLimited(checkRateLimit('http://b'));
assertNotLimited(checkRateLimit('http://cc'));
assertNotLimited(checkRateLimit('http://cc'));
assertNotLimited(checkRateLimit('http://ccc'));
assertLimited(checkRateLimit('http://ccc'), 1, 2);
});
it('multiple domains, string and RegExp', function() {
var checkRateLimit = createRateLimitChecker('1 2 a /b/');
assertNotLimited(checkRateLimit('http://a'));
assertNotLimited(checkRateLimit('http://a'));
assertNotLimited(checkRateLimit('http://b'));
assertNotLimited(checkRateLimit('http://b'));
assertNotLimited(checkRateLimit('http://ab'));
assertLimited(checkRateLimit('http://ab'), 1, 2);
});
it('multiple domains, RegExp and string', function() {
var checkRateLimit = createRateLimitChecker('1 2 /a/ b');
assertNotLimited(checkRateLimit('http://a'));
assertNotLimited(checkRateLimit('http://a'));
assertNotLimited(checkRateLimit('http://b'));
assertNotLimited(checkRateLimit('http://b'));
assertNotLimited(checkRateLimit('http://ab'));
assertLimited(checkRateLimit('http://ab'), 1, 2);
});
it('wildcard subdomains', function() {
var checkRateLimit = createRateLimitChecker('0 1 /(.*\\.)?example\\.com/');
assertNotLimited(checkRateLimit('http://example.com'));
assertNotLimited(checkRateLimit('http://www.example.com'));
assertLimited(checkRateLimit('http://xexample.com'), 0, 1);
assertLimited(checkRateLimit('http://example.com.br'), 0, 1);
});
it('wildcard ports', function() {
var checkRateLimit = createRateLimitChecker('0 1 /example\\.com(:\\d{1,5})?/');
assertNotLimited(checkRateLimit('http://example.com'));
assertNotLimited(checkRateLimit('http://example.com:1234'));
});
it('empty host', function() {
var checkRateLimit = createRateLimitChecker('0 1');
assertLimited(checkRateLimit(''), 0, 1);
// Empty host actually means empty origin. But let's also test for 'http://'.
assertLimited(checkRateLimit('http://'), 0, 1);
checkRateLimit = createRateLimitChecker('0 1 ');
assertLimited(checkRateLimit(''), 0, 1);
assertLimited(checkRateLimit('http://'), 0, 1);
checkRateLimit = createRateLimitChecker('0 1 //');
assertNotLimited(checkRateLimit(''));
assertNotLimited(checkRateLimit('http://'));
});
it('null origin', function() {
var checkRateLimit = createRateLimitChecker('0 1');
assertLimited(checkRateLimit('null'), 0, 1);
assertLimited(checkRateLimit('http://null'), 0, 1);
checkRateLimit = createRateLimitChecker('0 1 null');
assertNotLimited(checkRateLimit('null'));
assertNotLimited(checkRateLimit('http://null'));
checkRateLimit = createRateLimitChecker('0 1 /null/');
assertNotLimited(checkRateLimit('null'));
assertNotLimited(checkRateLimit('http://null'));
});
it('case-insensitive', function() {
var checkRateLimit = createRateLimitChecker('0 1 NULL');
assertNotLimited(checkRateLimit('null'));
assertNotLimited(checkRateLimit('http://null'));
checkRateLimit = createRateLimitChecker('0 1 /NULL/');
assertNotLimited(checkRateLimit('null'));
assertNotLimited(checkRateLimit('http://null'));
});
it('bad input', function() {
assert.throws(function() {
createRateLimitChecker('0 1 /');
}, /Invalid CORSANYWHERE_RATELIMIT\. Regex at index 0 must start and end with a slash \("\/"\)\./);
assert.throws(function() {
createRateLimitChecker('0 1 a /');
}, /Invalid CORSANYWHERE_RATELIMIT\. Regex at index 1 must start and end with a slash \("\/"\)\./);
assert.throws(function() {
createRateLimitChecker('0 1 /(/');
}, /Invalid regular expression/);
});
});

1080
cors-anywhere/test/test.js Normal file

File diff suppressed because it is too large Load Diff