From 8a367bda4b0621cae838337ab3f17364df04be0b Mon Sep 17 00:00:00 2001 From: Rob W Date: Thu, 3 Jan 2013 18:37:26 +0100 Subject: [PATCH] CORS Anywhere - Initial commit --- Procfile | 1 + README.md | 19 ++++ lib/cors-anywhere.js | 167 +++++++++++++++++++++++++++++++++ lib/help.txt | 8 ++ lib/regexp-top-level-domain.js | 6 ++ package.json | 13 +++ server.js | 7 ++ 7 files changed, 221 insertions(+) create mode 100644 Procfile create mode 100644 README.md create mode 100644 lib/cors-anywhere.js create mode 100644 lib/help.txt create mode 100644 lib/regexp-top-level-domain.js create mode 100644 package.json create mode 100644 server.js diff --git a/Procfile b/Procfile new file mode 100644 index 0000000..489b270 --- /dev/null +++ b/Procfile @@ -0,0 +1 @@ +web: node server.js diff --git a/README.md b/README.md new file mode 100644 index 0000000..0369be2 --- /dev/null +++ b/README.md @@ -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. diff --git a/lib/cors-anywhere.js b/lib/cors-anywhere.js new file mode 100644 index 0000000..f21108d --- /dev/null +++ b/lib/cors-anywhere.js @@ -0,0 +1,167 @@ +// © 2013 Rob W +// 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 //.... + //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: : / 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; +}; diff --git a/lib/help.txt b/lib/help.txt new file mode 100644 index 0000000..5a95a50 --- /dev/null +++ b/lib/help.txt @@ -0,0 +1,8 @@ +Usage: + +/ Shows help +/iscorsneeded This is the only resource on this host which is served without CORS headers. +/ Create a request to , and includes CORS headers in the response. + +The protocol can be omitted. It defaults to http:, unless port 443 is specified. + diff --git a/lib/regexp-top-level-domain.js b/lib/regexp-top-level-domain.js new file mode 100644 index 0000000..12f5fe5 --- /dev/null +++ b/lib/regexp-top-level-domain.js @@ -0,0 +1,6 @@ +// Based on http://data.iana.org/TLD/tlds-alpha-by-domain.txt +// '/\\.(?:' + document.body.firstChild.textContent.trim().split('\n').slice(1).join('|') + ')$/i'; + +// # Version 2013010201, Last Updated Thu Jan 3 07:07:01 2013 UTC +var regexp = /\.(?:AC|AD|AE|AERO|AF|AG|AI|AL|AM|AN|AO|AQ|AR|ARPA|AS|ASIA|AT|AU|AW|AX|AZ|BA|BB|BD|BE|BF|BG|BH|BI|BIZ|BJ|BM|BN|BO|BR|BS|BT|BV|BW|BY|BZ|CA|CAT|CC|CD|CF|CG|CH|CI|CK|CL|CM|CN|CO|COM|COOP|CR|CU|CV|CW|CX|CY|CZ|DE|DJ|DK|DM|DO|DZ|EC|EDU|EE|EG|ER|ES|ET|EU|FI|FJ|FK|FM|FO|FR|GA|GB|GD|GE|GF|GG|GH|GI|GL|GM|GN|GOV|GP|GQ|GR|GS|GT|GU|GW|GY|HK|HM|HN|HR|HT|HU|ID|IE|IL|IM|IN|INFO|INT|IO|IQ|IR|IS|IT|JE|JM|JO|JOBS|JP|KE|KG|KH|KI|KM|KN|KP|KR|KW|KY|KZ|LA|LB|LC|LI|LK|LR|LS|LT|LU|LV|LY|MA|MC|MD|ME|MG|MH|MIL|MK|ML|MM|MN|MO|MOBI|MP|MQ|MR|MS|MT|MU|MUSEUM|MV|MW|MX|MY|MZ|NA|NAME|NC|NE|NET|NF|NG|NI|NL|NO|NP|NR|NU|NZ|OM|ORG|PA|PE|PF|PG|PH|PK|PL|PM|PN|POST|PR|PRO|PS|PT|PW|PY|QA|RE|RO|RS|RU|RW|SA|SB|SC|SD|SE|SG|SH|SI|SJ|SK|SL|SM|SN|SO|SR|ST|SU|SV|SX|SY|SZ|TC|TD|TEL|TF|TG|TH|TJ|TK|TL|TM|TN|TO|TP|TR|TRAVEL|TT|TV|TW|TZ|UA|UG|UK|US|UY|UZ|VA|VC|VE|VG|VI|VN|VU|WF|WS|XN--0ZWM56D|XN--11B5BS3A9AJ6G|XN--3E0B707E|XN--45BRJ9C|XN--80AKHBYKNJ4F|XN--80AO21A|XN--90A3AC|XN--9T4B11YI5A|XN--CLCHC0EA0B2G2A9GCD|XN--DEBA0AD|XN--FIQS8S|XN--FIQZ9S|XN--FPCRJ9C3D|XN--FZC2C9E2C|XN--G6W251D|XN--GECRJ9C|XN--H2BRJ9C|XN--HGBK6AJ7F53BBA|XN--HLCJ6AYA9ESC7A|XN--J6W193G|XN--JXALPDLP|XN--KGBECHTV|XN--KPRW13D|XN--KPRY57D|XN--LGBBAT1AD8J|XN--MGB9AWBF|XN--MGBAAM7A8H|XN--MGBAYH7GPA|XN--MGBBH1A71E|XN--MGBC0A9AZCG|XN--MGBERP4A5D4AR|XN--MGBX4CD0AB|XN--O3CW4H|XN--OGBPF8FL|XN--P1AI|XN--PGBS0DH|XN--S9BRJ9C|XN--WGBH1C|XN--WGBL6A|XN--XKC2AL3HYE2A|XN--XKC2DL3A5EE0H|XN--YFRO4I67O|XN--YGBI2AMMX|XN--ZCKZAH|XXX|YE|YT|ZA|ZM|ZW)$/i +module.exports = regexp; diff --git a/package.json b/package.json new file mode 100644 index 0000000..07fe3fc --- /dev/null +++ b/package.json @@ -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": "gwnRob@gmail.com" + }, + "dependencies": { + "http-proxy": "~0.8" + } +} diff --git a/server.js b/server.js new file mode 100644 index 0000000..68b856a --- /dev/null +++ b/server.js @@ -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); +});