mirror of
https://github.com/streetwriters/notesnook-sync-server.git
synced 2026-02-12 11:12:44 +00:00
cors: add self hostable cors proxy
This commit is contained in:
13
.github/workflows/publish.yml
vendored
13
.github/workflows/publish.yml
vendored
@@ -23,12 +23,19 @@ jobs:
|
|||||||
repos:
|
repos:
|
||||||
- image: streetwriters/notesnook-sync
|
- image: streetwriters/notesnook-sync
|
||||||
file: ./Notesnook.API/Dockerfile
|
file: ./Notesnook.API/Dockerfile
|
||||||
|
context: .
|
||||||
|
|
||||||
|
- image: streetwriters/cors-proxy
|
||||||
|
file: ./cors-proxy/Dockerfile
|
||||||
|
context: ./cors-proxy/
|
||||||
|
|
||||||
- image: streetwriters/identity
|
- image: streetwriters/identity
|
||||||
file: ./Streetwriters.Identity/Dockerfile
|
file: ./Streetwriters.Identity/Dockerfile
|
||||||
|
context: .
|
||||||
|
|
||||||
- image: streetwriters/sse
|
- image: streetwriters/sse
|
||||||
file: ./Streetwriters.Messenger/Dockerfile
|
file: ./Streetwriters.Messenger/Dockerfile
|
||||||
|
context: .
|
||||||
permissions:
|
permissions:
|
||||||
packages: write
|
packages: write
|
||||||
contents: read
|
contents: read
|
||||||
@@ -42,7 +49,7 @@ jobs:
|
|||||||
- name: Docker Setup Buildx
|
- name: Docker Setup Buildx
|
||||||
uses: docker/setup-buildx-action@v3
|
uses: docker/setup-buildx-action@v3
|
||||||
with:
|
with:
|
||||||
platforms: linux/amd64,linux/arm64,linux/arm/v7,linux/arm/v8
|
platforms: linux/amd64,linux/arm64
|
||||||
|
|
||||||
- name: Log in to Docker Hub
|
- name: Log in to Docker Hub
|
||||||
uses: docker/login-action@v3
|
uses: docker/login-action@v3
|
||||||
@@ -71,10 +78,10 @@ jobs:
|
|||||||
id: push
|
id: push
|
||||||
uses: docker/build-push-action@v6
|
uses: docker/build-push-action@v6
|
||||||
with:
|
with:
|
||||||
context: .
|
context: ${{ matrix.repos.context }}
|
||||||
file: ${{ matrix.repos.file }}
|
file: ${{ matrix.repos.file }}
|
||||||
push: true
|
push: true
|
||||||
platforms: linux/amd64,linux/arm64,linux/arm/v7,linux/arm/v8
|
platforms: linux/amd64,linux/arm64
|
||||||
tags: ${{ steps.meta.outputs.tags }}
|
tags: ${{ steps.meta.outputs.tags }}
|
||||||
cache-from: ${{ matrix.repos.image }}:latest
|
cache-from: ${{ matrix.repos.image }}:latest
|
||||||
|
|
||||||
|
|||||||
19
cors-proxy/Dockerfile
Normal file
19
cors-proxy/Dockerfile
Normal 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
18
cors-proxy/bun.lock
Normal 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
23
cors-proxy/package.json
Normal 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
227
cors-proxy/src/index.ts
Normal 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
43
cors-proxy/tsconfig.json
Normal 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,
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user