diff --git a/README.md b/README.md index c503b1a..1181c95 100644 --- a/README.md +++ b/README.md @@ -5,8 +5,10 @@ part of the proxied URI is optional, and defaults to "http". If port 443 is spec the protocol defaults to "https". This package does not put any restrictions on the http methods or headers, except for -cookies. Credentials are disabled by default, because leaking cookies between different -domains is insecure. +cookies. Requesting [user credentials](http://www.w3.org/TR/cors/#user-credentials) is disallowed. + +Redirects are not automatically followed. Instead, the server replies with http status code 333 and +includes an absolute URL in the `location` response header. 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. @@ -21,7 +23,6 @@ var port = process.env.PORT || 8080; var cors_proxy = require("cors-anywhere"); cors_proxy.createServer({ requireHeader: 'x-requested-with', - withCredentials: false, removeHeaders: ['cookie', 'cookie2'] }).listen(port, host, function() { console.log('Running CORS Anywhere on ' + host + ':' + port); @@ -30,11 +31,11 @@ cors_proxy.createServer({ ``` 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 +* `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: @@ -50,10 +51,6 @@ The module exports two properties: `getHandler` and `createServer`. * `createServer(options)` creates a server with the default handler. The following options are recognized by both methods: -* boolean `withCredentials` - If true, [user credentials](http://www.w3.org/TR/cors/#user-credentials) - such as cookies are accepted in the request. It's recommended to set this flag to `false`, because - cookies ought not to be leaked to other domains. If you want to use `withCredentials`, make sure that - you implement cookie parsing and transforming so that the `path` flag of the cookie is set correctly. * string `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 browsing. Example: `X-Requested-With` * array of lowercase strings `removeHeaders` - Exclude certain headers from being included in the request. diff --git a/lib/cors-anywhere.js b/lib/cors-anywhere.js index 589de4b..84af7e1 100644 --- a/lib/cors-anywhere.js +++ b/lib/cors-anywhere.js @@ -3,6 +3,7 @@ var httpProxy = require('http-proxy'); var net = require('net'); +var url = require('url'); var regexp_tld = require('./regexp-top-level-domain'); var help_file = __dirname + '/help.txt'; @@ -35,23 +36,86 @@ function hasNoContent(hostname) { ); } -function handleCookies(isAllowed, headers) { - // Assumed that all headers' names are lowercase - if (!isAllowed) { - delete headers['set-cookie']; - delete headers['set-cookie2']; +// First argument: The response.headers object +// Second argument: The request object. +function withCORS(headers, request) { + var origin = request.headers.origin || 'null'; + headers['access-control-allow-origin'] = origin === 'null' ? '*' : origin; + if (request.headers['access-control-request-method']) { + headers['access-control-allow-methods'] = request.headers['access-control-request-method']; + delete req.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']; + } + return headers; +} +function getProto(req) { + return req.isSpdy ? 'https' : (req.connection.pair ? 'https' : 'http'); +} +function _clone(obj) { + var clone = {}; + Object.keys(clone).forEach(function(key) { + clone[key] = obj[key]; + }); + return clone; +} +function isForbidden(host) { + return false; // TODO +} +function proxyRequest(req, res, proxy, full_url, proxyOptions) { + if (isForbidden(proxyOptions.host)) { + res.writeHead(403, 'Refused to visit', withCORS({'Location': full_url}, req)); 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']); + // Hook res.writeHead, .write and .end to queue the response + var base_host = getProto(req) + req.headers.host; + var res_writeHead = res.writeHead; + var res_write = res.write; + var res_end = res.end; + + res.writeHead = function(statusCode, reasonPhrase, headers) { + if (typeof reasonPhrase === 'object') { + headers = reasonPhrase; + reasonPhrase = undefined; + } + if (!headers) headers = withCORS({}, req); + else { + withCORS(headers, req); + + // Handle redirects + if (statusCode === 301 || statusCode === 302 || statusCode === 303 || statusCode === 307 || statusCode === 308) { + if (headers['location']) { + headers['location'] = url.resolve(full_url, headers['location']); + } + // Don't use 301 or 302 because browsers may cancel the request (observed in Chrome with a custom request header) + statusCode = 333; + reasonPhrase = 'Redirect'; + } + + // Don't slip through cookies + delete headers['set-cookie']; + delete headers['set-cookie2']; + + // Informational purposes + headers['x-request-url'] = full_url; + } + if (reasonPhrase) { + return res_writeHead.call(res, statusCode, reasonPhrase, headers); + } else { + return res_writeHead.call(res, statusCode, headers); + } + }; + + // Start proxying the request + proxy.proxyRequest(req, res, proxyOptions); } + // Called on every request var getHandler = exports.getHandler = function(options) { var corsAnywhere = { - withCredentials: false, // Toggle credentials/cookies requireHeader: null, // Require a header to be set? removeHeaders: [] // Strip these request headers }; @@ -64,20 +128,7 @@ var getHandler = exports.getHandler = function(options) { } return function(req, res, proxy) { - var cors_headers = { - 'access-control-allow-origin': req.headers.origin || '*' - }; - if (corsAnywhere.withCredentials) { // <-- If the corsAnywhere property does not exists, throw an error. - // 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']; - } - + var cors_headers = withCORS({}, req); if (req.method == 'OPTIONS') { // Pre-flight request. Reply successfully: res.writeHead(200, cors_headers); @@ -85,7 +136,7 @@ var getHandler = exports.getHandler = function(options) { return; } else { // Actual request. First, extract the desired URL from the request: - var host, hostname, port, path, match; + var full_url, 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 @@ -117,11 +168,17 @@ var getHandler = exports.getHandler = function(options) { res.end('Missing ' + corsAnywhere.requireHeader + ' header!'); return; } else { + full_url = match[0].substr(1); 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]; + + if (!match[1]) { + if (full_url.charAt(0) !== '/') full_url = '//' + full_url; + full_url = (port === 443 ? 'https:' : 'http:') + full_url; + } } // Change the requested path: req.url = path; @@ -130,36 +187,7 @@ var getHandler = exports.getHandler = function(options) { delete req.headers[header]; }); - // 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, { + proxyRequest(req, res, proxy, full_url, { host: hostname, port: port }); diff --git a/lib/help.txt b/lib/help.txt index 49edfd3..369cd41 100644 --- a/lib/help.txt +++ b/lib/help.txt @@ -6,7 +6,18 @@ Usage: /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. +If the protocol is omitted, it defaults to http (https if port 443 is specified). + +Cookies are disabled and stripped from requests. + +Redirects are not automatically followed: The API response has status code 333. +The client ought to confirm this redirection by creating a new request. + +The requested URL is available in the X-Request-URL response header. Non-existence of this +header implies that the requested URL was not recognized. + +This API has one requirement: The X-Requested-With header must be set. + Demo : http://rob.lekensteyn.nl/cors-anywhere.html Source code : https://github.com/Rob--W/cors-anywhere/ diff --git a/server.js b/server.js index ae94631..fdd0637 100644 --- a/server.js +++ b/server.js @@ -5,7 +5,6 @@ var port = process.env.PORT || 8080; var cors_proxy = require("./lib/cors-anywhere"); cors_proxy.createServer({ requireHeader: 'x-requested-with', - withCredentials: false, removeHeaders: ['cookie', 'cookie2'] }).listen(port, host, function() { console.log('Running CORS Anywhere on ' + host + ':' + port);