|
// @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> |
|
<script> |
|
fetch('${request.url}https://example.com', { |
|
headers: { |
|
"Authorization": "token <api-key>" |
|
} |
|
}) |
|
</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> |
|
<script> |
|
fetch('${request.url}https://example.com', { |
|
headers: { |
|
"Curse-Authorization": "token <api-key>" |
|
} |
|
}) |
|
</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 |
|
}, |
|
} |