Skip to content

Commit

Permalink
Approved paths and auth injection
Browse files Browse the repository at this point in the history
From Merge pull request #1 from kendraio/approved-paths-and-auth-injection
  • Loading branch information
lukestanley committed May 9, 2023
2 parents fb7b5e4 + 9ace7b9 commit 838dd34
Show file tree
Hide file tree
Showing 4 changed files with 110 additions and 36 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -19,4 +19,3 @@ covid19.mathdro.id # COVID-19 stats endpoint, maybe down
covid19-7n89em8jy-mathdroid.vercel.app # dev build of COVID-19 stats endpoint
raw.githubusercontent.com # Raw file access to public GitHub repos
api.linear.app # Linear GraphQL API to see tasks
www.mygreenpod.com # Woocommerce API
9 changes: 9 additions & 0 deletions conf/pathAllowlist.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"example.com": [
"/allowed-path",
"/another-allowed-path"
],
"www.mygreenpod.com": [
"/wp-json/wc/v3/products"
]
}
83 changes: 68 additions & 15 deletions lib/cors-anywhere.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,34 @@ var getProxyForUrl = require('proxy-from-env').getProxyForUrl;

var help_text = {};

/**
* Converts a hostname to an environment variable name.
* @param {string} hostname - The hostname to convert.
* @returns {string} - The converted environment variable name.
*/
function convertHostnameToEnvVarName(hostname) {
// Because environment variables do not allow dots in their names,
// we convert hostnames to environment variable names.
// E.g for 'www.example.com' we expect the env key name to be: 'SECRET_KEY_WWW_DOT_EXAMPLE_DOT_COM'
return 'SECRET_KEY_' + hostname.toUpperCase().replace(/\./g, '_DOT_');
}

/**
* Injects a secret key into the request headers if exists.
* @param {object} reqHeaders - The request headers to inject the secret key into.
* @param {string} locationHostname - The hostname of the location being requested.
*/
function injectSecretKey(reqHeaders, locationHostname) {
// If a secret key is set as an environment variable for the requested host,
// we get it so we can inject it into the request headers.
const envVarName = convertHostnameToEnvVarName(locationHostname);
const secretKey = process.env[envVarName];

if (secretKey) {
reqHeaders['Authorization'] = secretKey;
}
}

function showUsage(help_file, headers, response) {
var isHtml = /\.html$/.test(help_file);
headers['content-type'] = isHtml ? 'text/html' : 'text/plain';
Expand Down Expand Up @@ -262,12 +290,13 @@ function parseURL(req_url) {
// Request handler factory
function getHandler(options, proxy) {
var corsAnywhere = {
pathAllowlist: options.pathAllowlist || {},
handleInitialRequest: null, // Function that may handle the request instead, by returning a truthy value.
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.
destinationWhitelist: [], // if non-empty, contains a list of allowed destrination hostnames
originDenylist: [], // Requests from these origins will be blocked.
originAllowlist: [], // If non-empty, requests not from an origin in this list will be blocked.
destinationAllowlist: [], // if non-empty, contains a list of allowed destination hostnames
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?
Expand Down Expand Up @@ -322,10 +351,6 @@ function getHandler(options, proxy) {
var location = parseURL(req.headers['target-url']);



//console.log('incoming request for '+location);
//console.log(location);

if (corsAnywhere.handleInitialRequest && corsAnywhere.handleInitialRequest(req, res, location)) {
return;
}
Expand All @@ -344,10 +369,35 @@ function getHandler(options, proxy) {
return;
}

if (corsAnywhere.destinationWhitelist.length && corsAnywhere.destinationWhitelist.indexOf(location.hostname) === -1) {
res.writeHead(403, 'Forbidden', cors_headers);
res.end('The destination "' + location.hostname + '" was not whitelisted.');
return;
let matchesPathAllowListItem = false;
if (Object.keys(corsAnywhere.pathAllowlist).length > 0) {
const allowedPaths = corsAnywhere.pathAllowlist[location.hostname];
if (allowedPaths) {
console.log('allowedPaths: ' + allowedPaths);
for (const allowedPath of allowedPaths) {
console.log('allowedPath: ' + allowedPath);
if (location.path.startsWith(allowedPath)) {
matchesPathAllowListItem = true;
break;
}
}
}

if (!matchesPathAllowListItem) {
res.writeHead(403, 'Forbidden', cors_headers);
res.end('The destination "' + location.hostname + '" with path "' + location.path + '" is not allowed.');
return;
}
}

if (
(corsAnywhere.destinationAllowlist.length &&
corsAnywhere.destinationAllowlist.indexOf(location.hostname) === -1) &&
!matchesPathAllowListItem){
res.writeHead(403, 'Forbidden', cors_headers);
res.end('The destination "' + location.hostname + '" was not allow-listed.');
return;

}


Expand Down Expand Up @@ -381,15 +431,15 @@ function getHandler(options, proxy) {
}

var origin = req.headers.origin || '';
if (corsAnywhere.originBlacklist.indexOf(origin) >= 0) {
if (corsAnywhere.originDenylist.indexOf(origin) >= 0) {
res.writeHead(403, 'Forbidden', cors_headers);
res.end('The origin "' + origin + '" was blacklisted by the operator of this proxy.');
res.end('The origin "' + origin + '" was deny-listed by the operator of this proxy.');
return;
}

if (corsAnywhere.originWhitelist.length && corsAnywhere.originWhitelist.indexOf(origin) === -1) {
if (corsAnywhere.originAllowlist.length && corsAnywhere.originAllowlist.indexOf(origin) === -1) {
res.writeHead(403, 'Forbidden', cors_headers);
res.end('The origin "' + origin + '" was not whitelisted by the operator of this proxy.');
res.end('The origin "' + origin + '" was not allow-listed by the operator of this proxy.');
return;
}

Expand Down Expand Up @@ -427,6 +477,9 @@ function getHandler(options, proxy) {
req.corsAnywhereRequestState.location = location;
req.corsAnywhereRequestState.proxyBaseUrl = proxyBaseUrl;

if(matchesPathAllowListItem){
injectSecretKey(req.headers, location.hostname)
}

console.log("Proxying request to '"+location.hostname+"'");
proxyRequest(req, res, proxy);
Expand Down
53 changes: 33 additions & 20 deletions server.js
Original file line number Diff line number Diff line change
@@ -1,23 +1,34 @@
const fs = require('fs');

// Listen on a specific host via the HOST environment variable
var host = process.env.HOST || '0.0.0.0';
const host = process.env.HOST || '0.0.0.0';
// Listen on a specific port via the PORT environment variable
var port = process.env.PORT || 8080;
const port = process.env.PORT || 8080;

// 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
// Grab the denylist from the command-line so that we can update the denylist without deploying
// again. CORS Anywhere is open by design, and this denylist 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);
// use originAllowlist instead.
const originDenylist = parseEnvList(process.env.CORSANYWHERE_BLACKLIST);
const originAllowlist = parseEnvList(process.env.CORSANYWHERE_WHITELIST);
console.log('originDenylist', originDenylist);
console.log('originAllowlist', originAllowlist);

// load a whitelist from a text file, and remove everything after the first space, then remove empty rows
// load a allowlist from a text file, and remove everything after the first space, then remove empty rows
let pathAllowlist = {};
try {
pathAllowlist = JSON.parse(fs.readFileSync('./conf/pathAllowlist.json'));
} catch (error) {
console.log('Error reading or parsing pathAllowlist.json file:', error.message);
}
console.log('Allowed paths and hosts');
console.log(pathAllowlist);

var destinationWhitelist = [];
let destinationAllowlist = [];
try {
var fs = require('fs');
destinationWhitelist=fs.readFileSync('./conf/destinationWhitelist.txt').toString().split("\n").map( row => row.split(" ")[0]).filter(n=>n);
} catch {
// file didn't exist
destinationAllowlist=fs.readFileSync('./conf/destinationAllowlist.txt').toString().split("\n").map( row => row.split(" ")[0]).filter(n=>n);
} catch (error) {
console.log('Error reading destinationAllowlist.txt file:', error.message);
}

function parseEnvList(env) {
Expand All @@ -28,14 +39,15 @@ function parseEnvList(env) {
}

// Set up rate-limiting to avoid abuse of the public CORS Anywhere server.
var checkRateLimit = require('./lib/rate-limit')(process.env.CORSANYWHERE_RATELIMIT);
const checkRateLimit = require('./lib/rate-limit')(process.env.CORSANYWHERE_RATELIMIT);

var cors_proxy = require('./lib/cors-anywhere');
const cors_proxy = require('./lib/cors-anywhere');
cors_proxy.createServer({
originBlacklist: originBlacklist,
originWhitelist: originWhitelist,
destinationWhitelist: destinationWhitelist,
originDenylist: originDenylist,
originAllowlist: originAllowlist,
destinationAllowlist: destinationAllowlist,
requireHeader: ['target-url'],
pathAllowlist: pathAllowlist,
checkRateLimit: checkRateLimit,
removeHeaders: [
'cookie',
Expand All @@ -59,8 +71,9 @@ cors_proxy.createServer({
}).listen(port, host, function() {
console.log('Running Kendraio CORS proxy on ' + host + ':' + port);

if (destinationWhitelist.length) {
if (destinationAllowlist.length) {
console.log('Allowed destinations');
console.log(destinationWhitelist);
console.log(destinationAllowlist);
}

});

0 comments on commit 838dd34

Please sign in to comment.