Spaces:
Running
Running
| import { json } from '@remix-run/cloudflare'; | |
| import type { ActionFunctionArgs, LoaderFunctionArgs } from '@remix-run/cloudflare'; | |
| // Allowed headers to forward to the target server | |
| const ALLOW_HEADERS = [ | |
| 'accept-encoding', | |
| 'accept-language', | |
| 'accept', | |
| 'access-control-allow-origin', | |
| 'authorization', | |
| 'cache-control', | |
| 'connection', | |
| 'content-length', | |
| 'content-type', | |
| 'dnt', | |
| 'pragma', | |
| 'range', | |
| 'referer', | |
| 'user-agent', | |
| 'x-authorization', | |
| 'x-http-method-override', | |
| 'x-requested-with', | |
| ]; | |
| // Headers to expose from the target server's response | |
| const EXPOSE_HEADERS = [ | |
| 'accept-ranges', | |
| 'age', | |
| 'cache-control', | |
| 'content-length', | |
| 'content-language', | |
| 'content-type', | |
| 'date', | |
| 'etag', | |
| 'expires', | |
| 'last-modified', | |
| 'pragma', | |
| 'server', | |
| 'transfer-encoding', | |
| 'vary', | |
| 'x-github-request-id', | |
| 'x-redirected-url', | |
| ]; | |
| // Handle all HTTP methods | |
| export async function action({ request, params }: ActionFunctionArgs) { | |
| return handleProxyRequest(request, params['*']); | |
| } | |
| export async function loader({ request, params }: LoaderFunctionArgs) { | |
| return handleProxyRequest(request, params['*']); | |
| } | |
| async function handleProxyRequest(request: Request, path: string | undefined) { | |
| try { | |
| if (!path) { | |
| return json({ error: 'Invalid proxy URL format' }, { status: 400 }); | |
| } | |
| // Handle CORS preflight request | |
| if (request.method === 'OPTIONS') { | |
| return new Response(null, { | |
| status: 200, | |
| headers: { | |
| 'Access-Control-Allow-Origin': '*', | |
| 'Access-Control-Allow-Methods': 'POST, GET, OPTIONS', | |
| 'Access-Control-Allow-Headers': ALLOW_HEADERS.join(', '), | |
| 'Access-Control-Expose-Headers': EXPOSE_HEADERS.join(', '), | |
| 'Access-Control-Max-Age': '86400', | |
| }, | |
| }); | |
| } | |
| // Extract domain and remaining path | |
| const parts = path.match(/([^\/]+)\/?(.*)/); | |
| if (!parts) { | |
| return json({ error: 'Invalid path format' }, { status: 400 }); | |
| } | |
| const domain = parts[1]; | |
| const remainingPath = parts[2] || ''; | |
| // Reconstruct the target URL with query parameters | |
| const url = new URL(request.url); | |
| const targetURL = `https://${domain}/${remainingPath}${url.search}`; | |
| console.log('Target URL:', targetURL); | |
| // Filter and prepare headers | |
| const headers = new Headers(); | |
| // Only forward allowed headers | |
| for (const header of ALLOW_HEADERS) { | |
| if (request.headers.has(header)) { | |
| headers.set(header, request.headers.get(header)!); | |
| } | |
| } | |
| // Set the host header | |
| headers.set('Host', domain); | |
| // Set Git user agent if not already present | |
| if (!headers.has('user-agent') || !headers.get('user-agent')?.startsWith('git/')) { | |
| headers.set('User-Agent', 'git/@isomorphic-git/cors-proxy'); | |
| } | |
| console.log('Request headers:', Object.fromEntries(headers.entries())); | |
| // Prepare fetch options | |
| const fetchOptions: RequestInit = { | |
| method: request.method, | |
| headers, | |
| redirect: 'follow', | |
| }; | |
| // Add body for non-GET/HEAD requests | |
| if (!['GET', 'HEAD'].includes(request.method)) { | |
| fetchOptions.body = request.body; | |
| fetchOptions.duplex = 'half'; | |
| /* | |
| * Note: duplex property is removed to ensure TypeScript compatibility | |
| * across different environments and versions | |
| */ | |
| } | |
| // Forward the request to the target URL | |
| const response = await fetch(targetURL, fetchOptions); | |
| console.log('Response status:', response.status); | |
| // Create response headers | |
| const responseHeaders = new Headers(); | |
| // Add CORS headers | |
| responseHeaders.set('Access-Control-Allow-Origin', '*'); | |
| responseHeaders.set('Access-Control-Allow-Methods', 'POST, GET, OPTIONS'); | |
| responseHeaders.set('Access-Control-Allow-Headers', ALLOW_HEADERS.join(', ')); | |
| responseHeaders.set('Access-Control-Expose-Headers', EXPOSE_HEADERS.join(', ')); | |
| // Copy exposed headers from the target response | |
| for (const header of EXPOSE_HEADERS) { | |
| // Skip content-length as we'll use the original response's content-length | |
| if (header === 'content-length') { | |
| continue; | |
| } | |
| if (response.headers.has(header)) { | |
| responseHeaders.set(header, response.headers.get(header)!); | |
| } | |
| } | |
| // If the response was redirected, add the x-redirected-url header | |
| if (response.redirected) { | |
| responseHeaders.set('x-redirected-url', response.url); | |
| } | |
| console.log('Response headers:', Object.fromEntries(responseHeaders.entries())); | |
| // Return the response with the target's body stream piped directly | |
| return new Response(response.body, { | |
| status: response.status, | |
| statusText: response.statusText, | |
| headers: responseHeaders, | |
| }); | |
| } catch (error) { | |
| console.error('Proxy error:', error); | |
| return json( | |
| { | |
| error: 'Proxy error', | |
| message: error instanceof Error ? error.message : 'Unknown error', | |
| url: path ? `https://${path}` : 'Invalid URL', | |
| }, | |
| { status: 500 }, | |
| ); | |
| } | |
| } | |