cors: add self hostable cors proxy

This commit is contained in:
Abdullah Atta
2025-12-29 12:33:11 +05:00
parent a3235ca381
commit 75369a5988
6 changed files with 340 additions and 3 deletions

19
cors-proxy/Dockerfile Normal file
View File

@@ -0,0 +1,19 @@
FROM oven/bun:1.3.5-slim
RUN mkdir -p /home/bun/app && chown -R bun:bun /home/bun/app
WORKDIR /home/bun/app
USER bun
COPY --chown=bun:bun package.json bun.lock .
RUN bun install --frozen-lockfile
COPY --chown=bun:bun . .
RUN bun run build
EXPOSE 3000
CMD ["bun", "run", "start"]

18
cors-proxy/bun.lock Normal file
View File

@@ -0,0 +1,18 @@
{
"lockfileVersion": 1,
"workspaces": {
"": {
"name": "cors-proxy",
"devDependencies": {
"bun-types": "latest",
},
},
},
"packages": {
"@types/node": ["@types/node@24.10.2", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-WOhQTZ4G8xZ1tjJTvKOpyEVSGgOTvJAfDK3FNFgELyaTpzhdgHVHeqW8V+UJvzF5BT+/B54T/1S2K6gd9c7bbA=="],
"bun-types": ["bun-types@1.3.4", "", { "dependencies": { "@types/node": "*" } }, "sha512-5ua817+BZPZOlNaRgGBpZJOSAQ9RQ17pkwPD0yR7CfJg+r8DgIILByFifDTa+IPDDxzf5VNhtNlcKqFzDgJvlQ=="],
"undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="],
}
}

23
cors-proxy/package.json Normal file
View File

@@ -0,0 +1,23 @@
{
"name": "@streetwriters/cors-proxy",
"version": "1.0.0",
"description": "Production-ready CORS proxy server built with Bun",
"main": "src/index.ts",
"type": "module",
"scripts": {
"build": "bun build src/index.ts --outdir dist --target bun",
"start": "bun run dist/index.js",
"dev": "bun --watch src/index.ts"
},
"keywords": [
"cors",
"proxy",
"bun",
"server"
],
"author": "",
"license": "MIT",
"devDependencies": {
"bun-types": "latest"
}
}

227
cors-proxy/src/index.ts Normal file
View File

@@ -0,0 +1,227 @@
/**
* Production-ready CORS Proxy Server
* Built with Bun runtime
*/
const PORT = Bun.env.PORT || 3000;
const ALLOWED_ORIGINS = Bun.env.ALLOWED_ORIGINS?.split(",") || ["*"];
const MAX_REDIRECTS = 5;
// CORS headers configuration
const corsHeaders = {
"Access-Control-Allow-Origin": "*",
"Access-Control-Allow-Methods":
"GET, POST, PUT, DELETE, OPTIONS, HEAD, PATCH",
"Access-Control-Allow-Headers":
"Content-Type, Authorization, X-Requested-With, Accept, Accept-Language, Referer, Origin",
"Access-Control-Max-Age": "86400",
"Access-Control-Expose-Headers":
"Content-Length, Content-Type, Date, Server, X-Powered-By",
};
// Log request for monitoring
function logRequest(method: string, url: string, status: number) {
const timestamp = new Date().toISOString();
console.log(`[${timestamp}] ${method} ${url} - ${status}`);
}
// Validate URL
function isValidUrl(urlString: string): boolean {
try {
const url = new URL(urlString);
return url.protocol === "http:" || url.protocol === "https:";
} catch {
return false;
}
}
// Handle proxied request with redirect support
async function proxyRequest(
targetUrl: string,
redirectCount = 0
): Promise<Response> {
if (redirectCount >= MAX_REDIRECTS) {
return new Response("Too many redirects", {
status: 508,
headers: corsHeaders,
});
}
try {
const response = await fetch(targetUrl, {
method: "GET",
redirect: "manual",
});
// Handle redirects manually
if (response.status >= 300 && response.status < 400) {
const location = response.headers.get("Location");
if (location) {
const redirectUrl = new URL(location, targetUrl).toString();
return proxyRequest(redirectUrl, redirectCount + 1);
}
}
// Get response headers
const responseHeaders = new Headers(corsHeaders);
// Forward important headers (but NOT content-encoding since fetch auto-decompresses)
const headersToForward = [
"content-type",
"content-length",
"cache-control",
"etag",
"last-modified",
"content-disposition",
"content-range",
"accept-ranges",
"vary",
"date",
"expires",
"age",
];
headersToForward.forEach((header) => {
const value = response.headers.get(header);
if (value) {
responseHeaders.set(header, value);
}
});
// Remove headers that might reveal proxy usage
responseHeaders.delete("x-powered-by");
responseHeaders.delete("server");
responseHeaders.delete("via");
responseHeaders.delete("x-proxy");
responseHeaders.delete("x-cache");
return new Response(response.body, {
status: response.status,
statusText: response.statusText,
headers: responseHeaders,
});
} catch (error) {
const errorMessage =
error instanceof Error ? error.message : "Unknown error";
console.error(`Proxy error: ${errorMessage}`);
return new Response(`Proxy error: ${errorMessage}`, {
status: 502,
headers: corsHeaders,
});
}
}
// Main server
const server = Bun.serve({
port: PORT,
async fetch(req) {
const url = new URL(req.url);
// Health check endpoint
if (url.pathname === "/health") {
logRequest(req.method, url.pathname, 200);
return new Response("OK", {
status: 200,
headers: corsHeaders,
});
}
// Handle CORS preflight
if (req.method === "OPTIONS") {
logRequest(req.method, url.pathname, 204);
return new Response(null, {
status: 204,
headers: corsHeaders,
});
}
// Root endpoint with usage info
if (url.pathname === "/") {
const usage = {
service: "CORS Proxy Server",
version: "1.0.0",
usage: {
method1: "GET /<url>",
method2: "GET /?url=<encoded-url>",
example1: `${url.origin}/https://example.com/image.jpg`,
example2: `${url.origin}/?url=${encodeURIComponent(
"https://example.com/image.jpg"
)}`,
},
endpoints: {
health: "/health",
proxy: "/<target-url> or /?url=<target-url>",
},
};
logRequest(req.method, url.pathname, 200);
return new Response(JSON.stringify(usage, null, 2), {
status: 200,
headers: {
...corsHeaders,
"Content-Type": "application/json",
},
});
}
// Get target URL from path or query parameter
let targetUrl: string | null = null;
// Method 1: Direct path (preferred) - http://localhost:3000/https://example.com/image.jpg
if (url.pathname !== "/" && url.pathname !== "/health") {
// Remove leading slash and reconstruct the full URL with query string
targetUrl = url.pathname.slice(1);
if (url.search) {
targetUrl += url.search;
}
}
// Method 2: Query parameter - http://localhost:3000/?url=https://example.com/image.jpg
if (!targetUrl) {
targetUrl = url.searchParams.get("url");
}
if (!targetUrl) {
logRequest(req.method, url.pathname, 400);
return new Response(
"Missing URL. Use http://localhost:3000/<url> or /?url=<url>",
{
status: 400,
headers: corsHeaders,
}
);
}
// Decode URL if needed
try {
targetUrl = decodeURIComponent(targetUrl);
} catch {
// URL might not be encoded, use as is
}
// Validate URL
if (!isValidUrl(targetUrl)) {
logRequest(req.method, targetUrl, 400);
return new Response("Invalid URL provided", {
status: 400,
headers: corsHeaders,
});
}
// Proxy the request
const response = await proxyRequest(targetUrl);
logRequest(req.method, targetUrl, response.status);
return response;
},
error(error) {
console.error("Server error:", error);
return new Response("Internal Server Error", {
status: 500,
headers: corsHeaders,
});
},
});
console.log(`🚀 CORS Proxy Server running on http://localhost:${server.port}`);
console.log(`📋 Health check: http://localhost:${server.port}/health`);
console.log(`🌍 Environment: ${Bun.env.NODE_ENV || "development"}`);

43
cors-proxy/tsconfig.json Normal file
View File

@@ -0,0 +1,43 @@
{
// Visit https://aka.ms/tsconfig to read more about this file
"compilerOptions": {
// File Layout
// "rootDir": "./src",
// "outDir": "./dist",
// Environment Settings
// See also https://aka.ms/tsconfig/module
"module": "nodenext",
"target": "esnext",
"types": [],
// For nodejs:
// "lib": ["esnext"],
// "types": ["node"],
// and npm install -D @types/node
// Other Outputs
"sourceMap": true,
"declaration": true,
"declarationMap": true,
// Stricter Typechecking Options
"noUncheckedIndexedAccess": true,
// Style Options
// "noImplicitReturns": true,
// "noImplicitOverride": true,
// "noUnusedLocals": true,
// "noUnusedParameters": true,
// "noFallthroughCasesInSwitch": true,
// "noPropertyAccessFromIndexSignature": true,
// Recommended Options
"strict": true,
"jsx": "react-jsx",
"verbatimModuleSyntax": true,
"isolatedModules": true,
"noUncheckedSideEffectImports": true,
"moduleDetection": "force",
"skipLibCheck": true,
}
}