Skip to content

Commit

Permalink
CORS Anywhere - Initial commit
Browse files Browse the repository at this point in the history
  • Loading branch information
Rob--W committed Jan 3, 2013
0 parents commit 8a367bd
Show file tree
Hide file tree
Showing 7 changed files with 221 additions and 0 deletions.
1 change: 1 addition & 0 deletions Procfile
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
web: node server.js
19 changes: 19 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
**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".

## Example
```javascript
var host = '127.0.0.1';
var port = 8080;

var cors_proxy = require("cors-anywhere");
cors_proxy.createServer().listen(port, host, function() {
console.log('Running CORS Anywhere on ' + host + ':' + port);
});
```

The package also includes a Procfile, to run the app on Heroku. More information about
Heroku can be found at https://devcenter.heroku.com/articles/nodejs.
167 changes: 167 additions & 0 deletions lib/cors-anywhere.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
// © 2013 Rob W <[email protected]>
// Released under the MIT license

var httpProxy = require('http-proxy');
var net = require('net');
var regexp_tld = require('./regexp-top-level-domain');

var help_file = __dirname + '/help.txt';
var help_text;
function showUsage(res) {
if (help_text != null) {
res.writeHead(200, {'content-type': 'text/plain'});
res.end(help_text);
} else {
require('fs').readFile(help_file, 'utf8', function(err, data) {
if (err) {
console.error(err);
res.writeHead(500, {});
res.end();
} else {
help_text = data;
showUsage(res); // Recursive call, but since data is a string, the recursion will end
}
});
}
}

function hasNoContent(hostname) {
// Show 404 for non-requests. For instance when hostname is favicon.ico, robots.txt, ...
return !(
regexp_tld.test(hostname) ||
net.isIPv4(hostname) ||
net.isIPv6(hostname)
);
}

function handleCookies(isAllowed, headers) {
// Assumed that all headers' names are lowercase
if (!isAllowed) {
delete headers['set-cookie'];
delete headers['set-cookie2'];
return;
}
// TODO: Parse cookies, and change Domain and Secure flag to match the API domain,
// and change Path to /<website>/....
//if (headers['set-cookie']) headers['set-cookie'] = _parseCookie(headers['set-cookie']);
//if (headers['set-cookie2']) headers['set-cookie2'] = _parseCookie(headers['set-cookie']);
}

// Called on every request
var handler = exports.handler = function(req, res, proxy) {

var cors_headers = {
'access-control-allow-origin': req.headers.origin || '*'
};
if (proxy.withCredentials) {
// Allow sending of credentials ONLY if it's explicitly allowed on creation of the proxy.
cors_headers['access-control-allow-credentials'] = 'true';
}
if (req.headers['access-control-request-method']) {
cors_headers['access-control-allow-methods'] = req.headers['access-control-request-method'];
}
if (req.headers['access-control-request-headers']) {
cors_headers['access-control-allow-headers'] = req.headers['access-control-request-headers'];
}

if (req.method == 'OPTIONS') {
// Pre-flight request. Reply successfully:
res.writeHead(200, cors_headers);
res.end();
return;
} else {
// Actual request. First, extract the desired URL from the request:
var host, hostname, port, path, match;
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 || (match[2].indexOf('.') === -1 && match[2].indexOf(':') === -1) || match[4] > 65535) {
// Incorrect usage. Show how to do it correctly.
showUsage(res);
return;
} else if (match[2] === '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;
} else if (match[4] > 65535) {
// Port is higher than 65535
res.writeHead(400, 'Invalid port', cors_headers);
res.end();
return;
} else if ( hasNoContent(match[3]) ) {
// Don't even try to proxy invalid hosts
res.writeHead(404, cors_headers);
res.end();
return;
} else {
host = match[2];
hostname = match[3];
// Read port from input: :<port> / 443 if https / 80 by default
port = match[4] ? +match[4] : (match[1] && match[1].toLowerCase() === 'https:' ? 443 : 80);
path = match[5];
}
// Change the requested path:
req.url = path;

// Hook res.writeHead method to set the correct header
var res_writeHead = res.writeHead;
res.writeHead = function(statusCode, reasonPhrase, headers) {
if (typeof reasonPhrase === 'object') {
headers = reasonPhrase;
}
if (!headers) headers = cors_headers;
else {
var header;
for (header in cors_headers) {
// We define the cors_headers object, so we can be damn sure that hasOwnProperty is not a key of it.
// and therefor we can use hOP directly instead of Object.prototype.hOP.call(...)
if (cors_headers.hasOwnProperty(header)) {
headers[header] = cors_headers[header];
}
}

if ((statusCode === 301 || statusCode === 302) && headers.location) {
// Handle redirects
// The X-Forwarded-Proto header is set by Heroku, and also by the http-proxy library when xforward is true)
var proxy_base_url = (req.headers['x-forwarded-proto'] || 'http') + '://' + req.headers['host'];
headers.location = proxy_base_url + '/' + headers.location;
}
handleCookies(proxy.withCredentials, headers);
}
return res_writeHead.apply(this, arguments); // headers are magically updated when variables are modified
};

// Finally, proxy the request
proxy.proxyRequest(req, res, {
host: hostname,
port: port
});
}
};

// Create server with default/recommended values
// Creator still needs to call .listen()
var createServer = exports.createServer = function() {
if (arguments.length) {
console.log('Warning: corsproxy.createServer ignores all arguments.');
}
var options = {
changeOrigin: true,
xforward: true
};
var server = httpProxy.createServer(options, handler);
// When the server fails, just show a 404 instead of Internal server error
server.proxy.on('proxyError', function(err, req, res) {
res.writeHead(404, {});
res.end();
});
// Disable Cookies etc. If you want to enable cookies, please implement a cookie parser which
// correctly uses the Path flag to separate cookies.
server.proxy.withCredentials = false;
return server;
};
8 changes: 8 additions & 0 deletions lib/help.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
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.

The protocol can be omitted. It defaults to http:, unless port 443 is specified.

6 changes: 6 additions & 0 deletions lib/regexp-top-level-domain.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

13 changes: 13 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
{
"name": "cors-anywhere",
"version": "0.1.0",
"description": "Proxies requests and adds the necessary CORS headers to it. URL is parsed from the path.",
"license": "MIT",
"author": {
"name": "Rob W",
"email": "[email protected]"
},
"dependencies": {
"http-proxy": "~0.8"
}
}
7 changes: 7 additions & 0 deletions server.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
var host = '127.0.0.1';
var port = 8080;

var cors_proxy = require("./lib/cors-anywhere");
cors_proxy.createServer().listen(port, host, function() {
console.log('Running CORS Anywhere on ' + host + ':' + port);
});

0 comments on commit 8a367bd

Please sign in to comment.