Created
November 6, 2025 10:37
-
-
Save dexit/da0a02be25f2ae05deec1e6e52277766 to your computer and use it in GitHub Desktop.
cf-cors-proxy-requests-postcodes.js
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // --- CONFIGURATION VIA ENVIRONMENT VARIABLES --- | |
| // In your Worker's Settings -> Variables, set the following: | |
| // | |
| // 1. ALLOWED_ORIGINS (Whitelist for target URLs for the proxy) | |
| // Value: *.example.com, api.another.com, my-domain.net | |
| // | |
| // 2. PROXY_ENDPOINT (The path the proxy will run on) | |
| // Value: /corsproxy/ | |
| // | |
| // 3. SEARCH_PARAM (The query parameter for the proxy, e.g., 'url' or 'apiurl') | |
| // Value: apiurl | |
| const HEADERS_TO_STRIP = [ | |
| 'content-security-policy', | |
| 'x-frame-options', | |
| 'x-content-type-options', | |
| ]; | |
| // --- PROXY HELPER FUNCTIONS (Unchanged) --- | |
| function isUrlAllowed(targetUrl, allowedOriginsCommaSeparated) { | |
| if (!allowedOriginsCommaSeparated) return false; | |
| const whitelist = allowedOriginsCommaSeparated.split(',').map(domain => domain.trim()); | |
| const hostname = new URL(targetUrl).hostname; | |
| for (const pattern of whitelist) { | |
| if (pattern === '*') return true; | |
| if (pattern.startsWith('*.')) { | |
| const domain = pattern.substring(2); | |
| if (hostname.endsWith('.' + domain) || hostname === domain) return true; | |
| } else { | |
| if (hostname === pattern) return true; | |
| } | |
| } | |
| return false; | |
| } | |
| async function handleProxyOptions(request) { | |
| const corsHeaders = { | |
| "Access-Control-Allow-Origin": "*", | |
| "Access-Control-Allow-Methods": "GET, HEAD, POST, PUT, DELETE, OPTIONS", | |
| "Access-Control-Max-Age": "86400", | |
| }; | |
| if (request.headers.get("Origin") !== null && request.headers.get("Access-Control-Request-Method") !== null && request.headers.get("Access-Control-Request-Headers") !== null) { | |
| return new Response(null, { | |
| headers: { ...corsHeaders, "Access-Control-Allow-Headers": request.headers.get("Access-Control-Request-Headers") }, | |
| }); | |
| } else { | |
| return new Response(null, { headers: { Allow: "GET, HEAD, POST, PUT, DELETE, OPTIONS" } }); | |
| } | |
| } | |
| async function handleProxyRequest(request, env) { | |
| const url = new URL(request.url); | |
| const definedSearchParam = env.SEARCH_PARAM || 'apiurl'; | |
| const allowedOrigins = env.ALLOWED_ORIGINS || 'pathwaygroup.co.uk,pathwayskillszone.ac.uk,cloudflare.com,workers.dev'; | |
| const targetUrl = url.searchParams.get(definedSearchParam); | |
| if (!targetUrl) { | |
| return new Response(`Error: "${definedSearchParam}" query parameter is missing.`, { status: 400 }); | |
| } | |
| if (!isUrlAllowed(targetUrl, allowedOrigins)) { | |
| return new Response(`Error: The requested URL is not in the whitelist.`, { status: 403 }); | |
| } | |
| const forwardedRequest = new Request(targetUrl, request); | |
| forwardedRequest.headers.set("Origin", new URL(targetUrl).origin); | |
| let response = await fetch(forwardedRequest); | |
| response = new Response(response.body, response); | |
| response.headers.set("Access-Control-Allow-Origin", url.origin); | |
| response.headers.append("Vary", "Origin"); | |
| HEADERS_TO_STRIP.forEach(header => response.headers.delete(header)); | |
| return response; | |
| } | |
| // --- REVISED: POSTCODE LOOKUP WITH FALLBACK LOGIC --- | |
| // /corsproxy/?apiurl=https://pathwaygroup.co.uk/dev/hubhook/hspics/src/postcodes/v2/api/asf?postcode=CV213FE | |
| // | |
| /** | |
| * Handles requests to the /postcodes/ endpoint with sequential fallback logic. | |
| * @param {Request} request The incoming request object. | |
| */ | |
| async function handlePostcodeRequest(request) { | |
| const { pathname } = new URL(request.url); | |
| const postcode = decodeURIComponent(pathname.split('/')[2] || "").trim(); | |
| const corsJsonResponse = (data, status) => { | |
| return new Response(JSON.stringify(data), { | |
| status: status, | |
| headers: { | |
| 'Content-Type': 'application/json', | |
| 'Access-Control-Allow-Origin': '*', | |
| 'Cache-Control': 's-maxage=86400' // Cache successful responses for 1 day | |
| }, | |
| }); | |
| }; | |
| if (!postcode) { | |
| return corsJsonResponse({ error: "Postcode missing. Use the format /postcodes/YourPostcode" }, 400); | |
| } | |
| const encodedPostcode = encodeURIComponent(postcode); | |
| const fetchHeaders = { 'User-Agent': 'Cloudflare-Worker-Postcode-Aggregator/1.0' }; | |
| // --- Step 1: Try active postcodes from postcodes.io --- | |
| const activeResponse = await fetch(`https://api.postcodes.io/postcodes/${encodedPostcode}`, { headers: fetchHeaders }); | |
| if (activeResponse.ok) { | |
| const data = await activeResponse.json(); | |
| return corsJsonResponse({ postcode, source: 'postcodes.io (active)', data: data.result }, 200); | |
| } | |
| // --- Step 2: If not found (404), try terminated postcodes --- | |
| if (activeResponse.status === 404) { | |
| const terminatedResponse = await fetch(`https://api.postcodes.io/terminated_postcodes/${encodedPostcode}`, { headers: fetchHeaders }); | |
| if (terminatedResponse.ok) { | |
| const data = await terminatedResponse.json(); | |
| return corsJsonResponse({ postcode, source: 'postcodes.io (terminated)', data: data.result }, 200); | |
| } | |
| } | |
| // --- Step 3: If both postcodes.io lookups fail, fall back to OpenStreetMap --- | |
| const osmResponse = await fetch(`https://nominatim.openstreetmap.org/search?q=${encodedPostcode}&format=jsonv2&limit=5`, { headers: fetchHeaders }); | |
| if (osmResponse.ok) { | |
| const data = await osmResponse.json(); | |
| if (data && data.length > 0) { | |
| return corsJsonResponse({ postcode, source: 'openstreetmap', data: data }, 200); | |
| } | |
| } | |
| // --- Final Step: If all sources fail, return a 404 --- | |
| return corsJsonResponse({ error: "Postcode not found in any available source." }, 404); | |
| } | |
| // --- MAIN ROUTER (Unchanged) --- | |
| export default { | |
| async fetch(request, env, ctx) { | |
| const url = new URL(request.url); | |
| const proxyEndpoint = env.PROXY_ENDPOINT || "/corsproxy/"; | |
| // Route 1: CORS Proxy | |
| if (url.pathname.startsWith(proxyEndpoint)) { | |
| if (request.method === "OPTIONS") { | |
| return handleProxyOptions(request); | |
| } else if (["GET", "HEAD", "POST", "PUT", "DELETE"].includes(request.method)) { | |
| return handleProxyRequest(request, env); | |
| } else { | |
| return new Response(null, { status: 405, statusText: "Method Not Allowed" }); | |
| } | |
| } | |
| // Route 2: Postcode Lookup | |
| else if (url.pathname.startsWith("/postcodes/")) { | |
| if (request.method === "GET" || request.method === "OPTIONS") { | |
| return handlePostcodeRequest(request); | |
| } else { | |
| return new Response(null, { status: 405, statusText: "Method Not Allowed" }); | |
| } | |
| } | |
| // Default Route | |
| else { | |
| return new Response("Worker is running. Use the /corsproxy/ or /postcodes/ endpoints.", { | |
| headers: { "Content-Type": "text/plain" }, | |
| }); | |
| } | |
| }, | |
| }; |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment