CORS is an acronym for Cross-origin Resource Sharing, it is an HTTP header that allows a server choose an origin other than its own to have access to its resources. The origin in this case include domain and port.
For instance I as the admin of https://xyz.com can determine if I want other domains to access resources on my server.
Having gotten a basis of what CORS is, it is worthy of note that for security reasons browsers implement a CORS safety feature by default when a fetch API is being called from script. What happens is that the browser checks the header of the requested resources to see if the header contains the right CORS header. This situation can be easily solved if you have access to the server, as you can easily include the origin on the server page.
Most developers get frustrated when they are trying to fetch a third party API using a script and run into CORS error. I had such case with justsms.com.ng, their API can only be called from a script and I ran into CORS error on the browser. I did not have access to the server and I was consuming the API via localhost, so contacting support would not be helpful.
// ----------------------------------------------------------------------------------
// CORSflare - v1.0.5
// ref.: https://github.com/Darkseal/CORSflare
// A lightweight JavaScript CORS Reverse Proxy designed to run in a Cloudflare Worker
// ----------------------------------------------------------------------------------
// ----------------------------------------------------------------------------------
// CONFIGURATION SETTINGS
// ----------------------------------------------------------------------------------
// The hostname of the upstream website to proxy(example: `www.google.com`).
const upstream = 'www.justsms.com.ng';
// The hostname of the upstream website to proxy for requests coming from mobile devices(example: `www.google.com`).
// if the upstream website doesn't have a dedicated hostname for mobile devices, you can set it to NULL.
const upstream_mobile = null;
// Custom pathname for the upstream website ('/' will work for most scenarios)
const upstream_path = '/index.php';
// Set it to TRUE to allow the default upstream to be overridden with a customizable GET parameter (see `upstream_get_parameter` below), FALSE to prevent that.
// WARNING: this feature will allow third-parties to replace this proxy's default upstream with an upstream of their choice: activate it only if you know
// what you're doing (see ref. below): also, it could be wise to change the default `upstream_get_parameter` with a unique string to reduce the risk of hijacks.
// ref.: https://github.com/Darkseal/CORSflare/issues/1
const upstream_allow_override = false;
// The GET parameter that can be used to override the default upstream if `upstream_allow_override` is set to TRUE.
const upstream_get_parameter = 'CORSflare_upstream';
// An array of countries and regions that won't be able to use the proxy.
const blocked_regions = ['CN', 'KP', 'SY', 'PK', 'CU'];
// An array of IP addresses that won't be able to use the proxy.
const blocked_ip_addresses = ['0.0.0.0', '5.5.5.5'];
// Set this value to TRUE to fetch the upstream website using HTTPS, FALSE to use HTTP.
// If the upstream website doesn't support HTTPS, this must be set to FALSE; also, if the proxy is HTTPS,
// you'll need to enable the replacement_rules rule to HTTPS proxy an HTTP-only website (see below).
const https = true;
// Set this value to TRUE to forcefully apply the "SameSite=None" and "Secure" directives to all cookies generated by the upstream.
// If you plan to put this proxy within a <iframe> and allow users to POST FORM data, you might need this option to make auth cookies work.
// ref.: https://sites.google.com/a/chromium.org/dev/updates/same-site/faq
// ref.: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie/SameSite
// NOTE: This is an experimental feature and could prevent some cookies from being generated due to a known bug of the Fetch API
// affecting the "Set-Cookie" response headers: use it at your own risk.
const set_cookie_samesite_none = false;
// an array of HTTP Response Headers to add (or to update, in case they're already present in the upstream response)
const http_response_headers_set = {
// use these headers to bypass DENY and SAMEORIGIN policies for IFRAME, OBJECT, EMBED and so on for most browsers.
// NOTE: be sure to replace "https://www.example.com" with the domain of the HTML page containing the IFRAME.
// ref.: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Frame-Options
// ref.: https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP
'X-Frame-Options': 'ALLOW FROM https://www.example.com', // IE
'Content-Security-Policy': "frame-ancestors 'self' https://www.example.com;", // Chrome, Firefox, etc.
// use this header to bypass the same-origin policy for XMLHttpRequest, Fetch API and so on
// ref.: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Allow-Origin
'Access-Control-Allow-Origin': '*',
// use this header to accept (and respond to) preflight requests when the request's credentials mode is set to 'include'
// ref.: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Allow-Credentials
'Access-Control-Allow-Credentials': true,
// use this header to override the Cache-Control settings of the upstream pages. Allowed values include:
// 'must-revalidate', 'no-cache', 'no-store', 'no-transform', 'public', 'private',
// 'proxy-revalidate', 'max-age=<seconds>', 's-maxage=<seconds>'.
// ref.: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cache-Control
// 'Cache-Control': 'no-cache'
};
// an array of HTTP Response Headers to delete (if present in the upstream response)
const http_response_headers_delete = [
'Content-Security-Policy-Report-Only',
'Clear-Site-Data'
];
// ----------------------------------------------------------------------------------
// TEXT REPLACEMENT RULES
// ----------------------------------------------------------------------------------
// The replacement_rules array can be used to configure the text replacement rules
// that will be applied by the proxy before serving any text/html resource back to the user.
// The common usage of such rules is to "fix" non-standard internal URLs and/or local paths
// within the upstream's HTML pages (css, js, internal links, custom fonts, and so on) and force them
// to pass to the proxy; however, they can also be used to alter the response content in various ways
// (change a logo, modify the page title, add a custom css/js, and so on).
// Each rule must be defined in the following way:
// '<source_string>' : '<replacement_string>'
// The following dynamic placeholder can be used within the source and replacement strings:
// {upstream_hostname} : will be replaced with the upstream's hostname
// {proxy_hostname} : will be replaced with this proxy's hostname
// HINT: Rules are processed from top to bottom: put the most specific rules before the generic ones.
const replacement_rules = {
// enable this rule only if you need to HTTPS proxy an HTTP-only website
'http://{upstream_hostname}/': 'https://{proxy_hostname}/',
// this rule should be always enabled (replaces the upstream hostname for internal links, CSS, JS, and so on)
'{upstream_hostname}': '{proxy_hostname}',
}
// the replacement_rules will be only applied to the returned content
// with the following content types specified by the replacement_content_types array.
const replacement_content_types = ['text/html'];
// Set this value to TRUE to allow RegEx syntax in replacement rules (see URLs below for details):
// - https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions
// - https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions/Cheatsheet
// NOTE: if RegEx syntax is enabled, RegEx special chars in search patterns will have to be escaped
// using a double back slash (\\) accordingly.
const replacement_use_regex = true;
// ----------------------------------------------------------------------------------
// MAIN CODE
// ----------------------------------------------------------------------------------
var regexp_upstreamHostname = (replacement_use_regex)
? new RegExp('{upstream_hostname}', 'g')
: null;
var regexp_proxyHostname = (replacement_use_regex)
? new RegExp('{proxy_hostname}', 'g')
: null;
addEventListener('fetch', event => {
event.respondWith(fetchAndApply(event.request));
})
async function fetchAndApply(request) {
var r = request.headers.get('cf-ipcountry');
const region = (r) ? r.toUpperCase() : null;
const ip_address = request.headers.get('cf-connecting-ip');
const user_agent = request.headers.get('user-agent');
let response = null;
let url = new URL(request.url);
let url_hostname = url.hostname;
let upstream_GET = (upstream_allow_override) ? url.searchParams.get(upstream_get_parameter) : null;
if (https == true) {
url.protocol = 'https:';
} else {
url.protocol = 'http:';
}
var upstream_domain = null;
if (upstream_GET) {
upstream_domain = upstream_GET;
}
else if (upstream_mobile && await is_mobile_user_agent(user_agent)) {
upstream_domain = upstream_mobile;
}
else {
upstream_domain = upstream;
}
url.host = upstream_domain;
if (url.pathname == '/') {
url.pathname = upstream_path;
} else {
url.pathname = upstream_path + url.pathname;
}
if (blocked_regions.includes(region) || blocked_ip_addresses.includes(ip_address)) {
response = new Response('Access denied', {
status: 403
});
} else {
let method = request.method;
let request_headers = request.headers;
let new_request_headers = new Headers(request_headers);
let request_content_type = new_request_headers.get('content-type');
new_request_headers.set('Host', upstream_domain);
new_request_headers.set('Origin', upstream_domain);
new_request_headers.set('Referer', url.protocol + '//' + url_hostname);
var params = {
method: method,
headers: new_request_headers,
// this is required to properly handle standard HTTP redirects, as we need to alter the "location" header:
// the default "follow" value would auto-resolve them with the upstream URL, which is not what we want.
redirect: 'manual'
}
// if the request is supposed to contain Form Data, populates the request body accordingly
if (method.toUpperCase() === "POST" && request_content_type) {
let request_content_type_toLower = request_content_type.toLowerCase();
if (request_content_type_toLower.includes("application/x-www-form-urlencoded")
|| request_content_type_toLower.includes("multipart/form-data")
|| request_content_type_toLower.includes("application/json")
) {
let reqText = await request.text(); // TODO: this won't work for multipart/form-data
if (reqText) {
params.body = reqText;
}
}
}
let original_response = await fetch(url.href, params);
connection_upgrade = new_request_headers.get("Upgrade");
if (connection_upgrade && connection_upgrade.toLowerCase() == "websocket") {
return original_response;
}
let original_response_clone = original_response.clone();
let response_headers = original_response_clone.headers;
let response_status = original_response_clone.status;
let original_text = null;
let new_response_headers = new Headers(response_headers);
let new_response_status = response_status;
if (http_response_headers_set) {
for (let k in http_response_headers_set) {
var v = http_response_headers_set[k];
new_response_headers.set(k, v);
}
}
if (http_response_headers_delete) {
for (let k of http_response_headers_delete) {
new_response_headers.delete(k);
}
}
// Patch "x-pjax-url" header to handle pushState ajax redirects
if (new_response_headers.get("x-pjax-url")) {
new_response_headers.set("x-pjax-url", new_response_headers.get("x-pjax-url")
.replace(url.protocol + "//", "https://")
.replace(upstream_domain, url_hostname));
}
// Patch "location" header to handle standard 301/302/303/307/308 HTTP redirects
if (new_response_headers.get("location")) {
var location = new_response_headers.get("location");
// specific behaviour for GDrive-based redirects: see https://github.com/Darkseal/CORSflare/issues/1 for details.
if (upstream_allow_override && location.includes("googleusercontent.com")) {
var new_upstream = location.substring(8, location.indexOf("/", 8));
location = location + "&" + upstream_get_parameter + "=" + new_upstream;
new_response_headers.set("location", location
.replace(url.protocol + "//", "https://")
.replace(new_upstream, url_hostname));
}
else {
new_response_headers.set("location", location
.replace(url.protocol + "//", "https://")
.replace(upstream_domain, url_hostname));
}
}
// Patch "set-cookie" headers by forcefully apply "SameSite=None" and "Secure" directives to allow cross-domain usage
// (if "set_cookie_samesite_none" is set to TRUE: see that configuration option's comment block for details and references)
if (set_cookie_samesite_none && new_response_headers.has("set-cookie")) {
// NOTE: unfortunately the Fetch API Headers object doesn't support multiple Set-Cookie headers due to a bug in Fetch API's
// "Headers" interface, as they are merged into a single comma-separated string (which is incompatible with most browsers).
// ref.: https://stackoverflow.com/questions/63204093/how-to-get-set-multiple-set-cookie-response-headers-using-fetch-api
// For that very reason, we can only support the * first * set-cookie header here.
var firstCookie = new_response_headers.get("set-cookie").split(',').shift();
new_response_headers.set("set-cookie", firstCookie
.split("SameSite=Lax; Secure").join("")
.split("SameSite=Lax").join("")
.split("SameSite=Strict; Secure").join("")
.split("SameSite=Strict").join("")
.split("SameSite=None; Secure").join("")
.split("SameSite=None").join("")
.replace(/^;+$/g, '')
+ "; SameSite=None; Secure");
}
let response_content_type = new_response_headers.get('content-type');
if (response_content_type
&& replacement_content_types.some(v => response_content_type.toLowerCase().includes(v))) {
original_text = await replace_response_text(original_response_clone, upstream_domain, url_hostname);
} else {
original_text = original_response_clone.body;
}
response = new Response(original_text, {
status: new_response_status,
headers: new_response_headers
})
}
return response;
}
async function replace_response_text(response, upstream_domain, host_name) {
let text = await response.text()
if (replacement_rules) {
for (let k in replacement_rules) {
var v = replacement_rules[k];
if (replacement_use_regex) {
k = k.replace(regexp_upstreamHostname, upstream_domain);
k = k.replace(regexp_proxyHostname, host_name);
v = v.replace(regexp_upstreamHostname, upstream_domain);
v = v.replace(regexp_proxyHostname, host_name);
text = text.replace(new RegExp(k, 'g'), v);
}
else {
k = k.split('{upstream_hostname}').join(upstream_domain);
k = k.split('{proxy_hostname}').join(host_name);
v = v.split('{upstream_hostname}').join(upstream_domain);
v = v.split('{proxy_hostname}').join(host_name);
text = text.split(k).join(v);
}
}
}
return text;
}
async function is_mobile_user_agent(user_agent_info) {
var agents = ["Android", "iPhone", "SymbianOS", "Windows Phone", "iPad", "iPod"];
for (var v = 0; v < agents.length; v++) {
if (user_agent_info.indexOf(agents[v]) > 0) {
return true;
}
}
return false;
}
- Click on "Save and Deploy" and that's it you can now consume your API using your Workers URL as the endpoint.
- Note in this instance justsms.com.ng was used, fell free to edit and change the upstream to whatever URI you trying to fetch.
Comments
Post a Comment