Skip to content

Instantly share code, notes, and snippets.

@Potherca
Last active March 2, 2025 19:26
Show Gist options
  • Save Potherca/a179c2692ca65cfcb3079db1c968168e to your computer and use it in GitHub Desktop.
Save Potherca/a179c2692ca65cfcb3079db1c968168e to your computer and use it in GitHub Desktop.
Cloudflare worker to act as a CORS Proxy (and reduce the times you curse) https://curse.pother.ca/

cors (noun)

A Middle English form of curse.

Call this with an URL appended to receive that URL with Access-Control-Allow-Origin: * header.

Don't forget to include your API key in the Authorization header!


<script>
fetch('https://curse.pother.ca/https://example.com', {
    headers: {
      "Authorization": "token <api-key>"
    }
})
</script>
// @TODO: Support more than one API key (How/Where to store?)
function buildRequestOptions(request) {
const requestOptions = {}
for (const [name, value] of Object.entries(request)) {
requestOptions[name] = value
}
requestOptions.redirect = 'follow'
return requestOptions
}
function buildUrl(request, path) {
let url
/**
* There is a chance that this is a request done by a page loaded through this proxy.
* If that is the case, the hostname should match us, and there should be a valid URL in the referrers' path.
* Note: The header name "referer" is actually a misspelling of the word "referrer".
* For more details see: https://en.wikipedia.org/wiki/HTTP_referer
*/
const referrer = request.headers.get('Referer')
let validReferrer = getValidReferrer(referrer, path)
try {
url = new URL(path)
} catch (e) {
if (validReferrer) {
// Try again with referrer URL
url = validReferrer
} else {
throw new Error('Invalid referrer')
}
}
return url
}
function createErrorResponse(title, body, headers, status, statusText) {
headers['Content-Type'] = 'text/html; charset=utf-8'
const message = `<!doctype html><html lang="en"><meta charset="UTF-8">
<style>title{display:inline;}h1:after{content:" (${status})"; color: #CBB}h1:before{content:"Error: "; color:crimson}</style>
<h1><title>${title}</title></h1>
${body}
</html>
`
return new Response(message, {headers, status: status, statusText: statusText})
}
async function createFetchResponse(request, path, headers) {
let response
const url = buildUrl(request, path, headers)
try {
response = await makeRequest(request, url)
} catch (error) {
return createErrorResponse(
`The provided URL <a href="${path}">${path}<a/> could not be reached.`,
null, headers, 502, 'Naughty Gateway',
)
}
const responseHeaders = createResponseHeaders(response, headers)
return new Response(response.body, {
headers: responseHeaders,
status: response.status,
statusText: response.statusText,
})
}
function createHomepageResponse(headers, request) {
const message = `<!doctype html>
<html lang="en">
<meta charset="UTF-8">
<dl>
<dt><h1><strong>cors</strong> <small>(noun)</small></h1></dt>
<dd><em>A Middle English form of curse.</em></dd>
</dl>
<p>
Call this with a URL appended to receive that URL
<em>with</em> <code>Access-Control-Allow-Origin: *</code> headers.
</p>
<p>
All request Headers will be passed along by the proxy.
</p>
<p>
Don't forget to include your API key in the <code>Authorization</code> header!
</p>
<pre><code>
&lt;script>
fetch('${request.url}https://example.com', {
headers: {
"Authorization": "token &lt;api-key>"
}
})
&lt;/script>
</code></pre>
<p>If your request requires a <code>Authorization</code> header, the API key for Curse can be passed
in a <code>Curse-Authorization</code> header instead:
<pre><code>
&lt;script>
fetch('${request.url}https://example.com', {
headers: {
"Curse-Authorization": "token &lt;api-key>"
}
})
&lt;/script>
</code></pre>
`
headers['Content-Type'] = 'text/html; charset=utf-8'
return new Response(message, {headers, status: 200, statusText: 'Awesome!'})
}
function createPreflightResponse(request, headers) {
headers['Access-Control-Allow-Headers'] = request.headers.get('Access-Control-Request-Headers')
return new Response(null, {headers})
}
function createResponseHeaders(response, headers) {
const responseHeaders = new Headers(response.headers)
Object.keys(headers).map(function (name) {
responseHeaders.set(name, headers[name])
})
responseHeaders.delete('content-security-policy-report-only')
return responseHeaders
}
function getApiKey(request) {
let apikey = request.headers.get('Authorization')
if (request.headers.has('Curse-Authorization')) {
apikey = request.headers.get('Curse-Authorization')
}
return apikey
}
function getValidReferrer(referrer, path) {
let validReferrer
if (referrer) {
const referrerUrl = new URL(referrer)
const referrerHostname = referrerUrl.hostname
const referrerPathname = referrerUrl.pathname
let referrerPath
// @FIXME: How do we get the current Worker URL to check against?
// Doing this hard-coded for now :-/
if (referrerHostname === 'curse.potherca.workers.dev') {
referrerPath = referrerPathname.substring(1)
.replace('http:/', 'http://')
.replace('https:/', 'https://')
}
try {
validReferrer = new URL(`${referrerPath}/${path}`)
} catch (e) {
}
}
return validReferrer
}
async function makeRequest(request, url) {
const requestOptions = buildRequestOptions(request)
return await fetch(url, requestOptions)
}
function validateRequest(env, request, path, headers) {
let errorResponse
const allowedOrigins = env.ALLOWED_ORIGINS.split(',').map(o => o.trim())
let apikey = getApiKey(request)
let origin = request.headers.get('Origin')
const originHost = new URL(origin)?.hostname
if ( ! apikey || apikey !== env.API_KEY) {
errorResponse = createErrorResponse(
`The required <code>Authorization</code> header is ${apikey ? 'incorrect' : 'missing'}`,
null, headers, 407, 'Please provide a valid API key.',
)
} else if ( ! origin) {
errorResponse = createErrorResponse(
'Origin header missing',
null, headers, 403, 'Forbidden, requires Origin',
)
} else if (allowedOrigins !== null && ! allowedOrigins.includes(originHost) && originHost !== 'dash.cloudflare.com') {
errorResponse = createErrorResponse(
`Provided origin "${originHost}" is not allowed`,
null, headers, 403, 'Forbidden, invalid Origin',
)
} else {
try {
buildUrl(request, path, headers)
} catch (error) {
errorResponse = createErrorResponse(
`The provided path "<a href="${path}">${path}<a/>" is not a valid URL`,
null, headers, 400, 'Invalid URL Provided',
)
}
}
return errorResponse
}
export default {
async fetch(request, env) {
const {pathname, searchParams} = new URL(request.url)
let path = pathname.substring(1)
.replace('http:/', 'http://')
.replace('https:/', 'https://')
if (searchParams.size > 0) {
path += '?' + searchParams
}
const headers = {
'Access-Control-Allow-Headers': '*',
'Access-Control-Allow-Methods': '*',
'Access-Control-Allow-Origin': '*',
'Access-Control-Expose-Headers': '*',
'Content-Security-Policy': [
'child-src *',
'connect-src *',
'default-src *',
'font-src *',
'frame-src *',
'img-src * "self" data: blob:',
'manifest-src *',
'media-src *',
'object-src *',
'prefetch-src *',
'script-src * "unsafe-eval"',
'script-src-attr *',
'script-src-elem * "self" "unsafe-inline"',
'style-src *',
'style-src-attr "unsafe-inline"',
'style-src-elem * "self" "unsafe-inline"',
'worker-src *',
].join(';'),
'Permissions-Policy': 'autoplay=*, camera=*, fullscreen=*, geolocation=*, microphone=*, web-share=*',
'Referrer-Policy': 'unsafe-url',
'X-Clacks-Overhead': 'GNU Terry Pratchett',
'X-Frame-Options': 'SAMEORIGIN',
'X-Requested-Path': path,
}
// Check if all required environment variables are set.
const requiredEnv = [
'API_KEY',
'ALLOWED_ORIGINS',
]
const missingEnv = requiredEnv.filter(key => ! env[key])
let response
if (missingEnv.length > 0) {
response = createErrorResponse(
'Missing environment variables',
`<ul><li>${missingEnv.join('</li><li>')}</li></ul>`, headers, 500, 'Please set missing ENV vars.',
)
} else if (
request.headers.get('Origin') !== null
&& request.headers.get('Access-Control-Request-Headers') !== null
&& request.headers.get('Access-Control-Request-Method') !== null
) {
// Send CORS preflight response.
response = createPreflightResponse(request, headers)
} else if (path === '') {
response = createHomepageResponse(headers, request)
} else {
const errorResponse = validateRequest(env, request, path, headers)
if (errorResponse) {
response = errorResponse
} else {
response = await createFetchResponse(request, path, headers)
}
}
return response
},
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment