Fix for 3xx redirects; Disabled credentials

This commit is contained in:
Rob W
2013-01-03 23:28:30 +01:00
parent 7e198f7455
commit 544a52b0ff
4 changed files with 104 additions and 69 deletions

View File

@@ -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.

View File

@@ -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 /<website>/....
//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: :<port> / 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
});

View File

@@ -6,7 +6,18 @@ Usage:
/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.
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/

View File

@@ -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);