| |
| import { serve } from 'bun'; |
|
|
| const PORT = process.env.PORT || 8002; |
|
|
| |
| |
| const hopByHopHeaders = [ |
| 'connection', |
| 'keep-alive', |
| 'proxy-authenticate', |
| 'proxy-authorization', |
| 'te', |
| 'trailers', |
| 'transfer-encoding', |
| 'upgrade', |
| |
| ]; |
|
|
| console.log(`Starting URL Path Proxy Server on http://localhost:${PORT}...`); |
|
|
| serve({ |
| port: PORT, |
| async fetch(req: Request): Promise<Response> { |
| const incomingUrl = new URL(req.url); |
| const timestamp = new Date().toISOString(); |
| console.log(`[${timestamp}] Incoming: ${req.method} ${incomingUrl.pathname}${incomingUrl.search}`); |
| console.log(`[${timestamp}] Request headers:`, Object.fromEntries(req.headers.entries())); |
|
|
| |
| |
| if (incomingUrl.pathname.startsWith('/socket.io/')) { |
| return new Response('Socket.IO requests are not supported by this proxy.', { |
| status: 400, |
| headers: { 'Content-Type': 'text/plain' } |
| }); |
| } |
|
|
| |
| if (incomingUrl.pathname === '/favicon.ico') { |
| return new Response('No favicon available', { |
| status: 404, |
| headers: { 'Content-Type': 'text/plain' } |
| }); |
| } |
|
|
| |
| |
| if (incomingUrl.pathname.match(/\.(ooo|ttt|xxx)$/) || |
| incomingUrl.pathname.startsWith('/xxx-devportal/') || |
| incomingUrl.pathname.startsWith('/xxx-navigator/') || |
| incomingUrl.pathname.substring(1) === 'not-a-valid-url') { |
| return new Response(`Resource not found: ${incomingUrl.pathname}`, { |
| status: 404, |
| headers: { 'Content-Type': 'text/plain' } |
| }); |
| } |
|
|
| |
| |
| |
| |
| |
|
|
| |
| let targetUrlString = incomingUrl.pathname.substring(1); |
|
|
| |
| if (incomingUrl.search) { |
| targetUrlString += incomingUrl.search; |
| } |
|
|
| if (!targetUrlString) { |
| return new Response( |
| "Usage: Send request to http://<proxy_host>:<proxy_port>/<full_target_url>\n" + |
| "Example: http://localhost:8000/https://api.example.com/data?id=123", |
| { |
| status: 400, |
| headers: { 'Content-Type': 'text/plain' } |
| } |
| ); |
| } |
|
|
| |
| try { |
| targetUrlString = decodeURIComponent(targetUrlString); |
| } catch (e) { |
| console.error(`[${timestamp}] Failed to decode target URL component: ${targetUrlString}`, e); |
| return new Response("Invalid URL encoding in the path parameter.", { status: 400 }); |
| } |
|
|
| |
| let targetUrl: URL; |
| try { |
| targetUrl = new URL(targetUrlString); |
| |
| if (!['http:', 'https:'].includes(targetUrl.protocol)) { |
| throw new Error('Invalid protocol'); |
| } |
| } catch (e) { |
| console.error(`[${timestamp}] Invalid Target URL Parsed: ${targetUrlString}`, e); |
| return new Response(`Invalid target URL provided in path: ${targetUrlString}`, { status: 400 }); |
| } |
|
|
| |
| const proxyRequestHeaders = new Headers(); |
| req.headers.forEach((value, key) => { |
| const lowerKey = key.toLowerCase(); |
| |
| if (!hopByHopHeaders.includes(lowerKey) && lowerKey !== 'host') { |
| proxyRequestHeaders.set(key, value); |
| } |
| }); |
|
|
| |
| |
| const clientIp = req.headers.get('x-forwarded-for')?.split(',')[0].trim() || 'unknown'; |
| proxyRequestHeaders.set('X-Forwarded-For', clientIp); |
| |
| proxyRequestHeaders.set('X-Forwarded-Proto', incomingUrl.protocol.slice(0, -1)); |
| |
| proxyRequestHeaders.set('X-Forwarded-Host', incomingUrl.host); |
|
|
| |
| proxyRequestHeaders.set('Host', targetUrl.host); |
|
|
| console.log(`[${timestamp}] Proxying to: ${targetUrl.toString()}`); |
| |
| console.log("Headers sent to target:", Object.fromEntries(proxyRequestHeaders.entries())); |
| console.log("Request body:", req.body); |
|
|
|
|
| |
| try { |
| |
| let requestBody = null; |
|
|
| |
| if (req.method !== 'GET' && req.method !== 'HEAD') { |
| |
| const clonedReq = req.clone(); |
|
|
| |
| try { |
| |
| const contentType = req.headers.get('content-type') || ''; |
| if (contentType.includes('application/json')) { |
| let bodyText = await clonedReq.text(); |
| |
| if (!bodyText && req.body) { |
| try { |
| |
| const tempReq = new Request(req.url, { |
| method: req.method, |
| headers: req.headers, |
| body: req.body, |
| duplex: 'half' |
| }); |
| bodyText = await tempReq.text(); |
| } catch (e) { |
| console.error(`[${timestamp}] Error reading body directly:`, e); |
| } |
| } |
| console.log(`[${timestamp}] Request body (JSON):`, bodyText); |
|
|
| |
| try { |
| const jsonBody = JSON.parse(bodyText); |
| if (jsonBody.stream === true) { |
| console.log(`[${timestamp}] Detected OpenAI streaming request`); |
| |
| if (!proxyRequestHeaders.has('Accept')) { |
| proxyRequestHeaders.set('Accept', 'text/event-stream'); |
| } |
| } |
| } catch (jsonError) { |
| |
| } |
|
|
| requestBody = bodyText; |
| } else { |
| |
| const bodyBuffer = await clonedReq.arrayBuffer(); |
| console.log(`[${timestamp}] Request body (binary): ${bodyBuffer.byteLength} bytes`); |
| requestBody = bodyBuffer; |
| } |
| } catch (bodyError) { |
| console.error(`[${timestamp}] Error reading request body:`, bodyError); |
| |
| } |
| } |
|
|
| const proxyResponse = await fetch(targetUrl.toString(), { |
| method: req.method, |
| headers: proxyRequestHeaders, |
| body: requestBody, |
| redirect: 'manual', |
| |
| |
| duplex: 'half', |
| }); |
|
|
| console.log(`[${timestamp}] Target response: ${proxyResponse.status} ${proxyResponse.statusText}`); |
| console.log(`[${timestamp}] Response headers:`, Object.fromEntries(proxyResponse.headers.entries())); |
|
|
| |
| const clientResponseHeaders = new Headers(); |
| proxyResponse.headers.forEach((value, key) => { |
| const lowerKey = key.toLowerCase(); |
| |
| if (!hopByHopHeaders.includes(lowerKey)) { |
| clientResponseHeaders.set(key, value); |
| } |
| }); |
|
|
| |
| clientResponseHeaders.append('Via', `1.1 bun-url-proxy`); |
|
|
| |
| |
| const responseContentType = proxyResponse.headers.get('content-type') || ''; |
| const transferEncoding = proxyResponse.headers.get('transfer-encoding') || ''; |
| |
| const isStreaming = responseContentType.includes('text/event-stream') || |
| transferEncoding.includes('chunked') || |
| responseContentType.includes('application/x-ndjson') || |
| |
| (targetUrl.toString().includes('/chat/completions') && |
| req.url.includes('stream=true')); |
|
|
| if (isStreaming) { |
| console.log(`[${timestamp}] Detected streaming response: ${responseContentType}`); |
| |
| return new Response(proxyResponse.body, { |
| status: proxyResponse.status, |
| statusText: proxyResponse.statusText, |
| headers: clientResponseHeaders, |
| }); |
| } else { |
| |
| return new Response(proxyResponse.body, { |
| status: proxyResponse.status, |
| statusText: proxyResponse.statusText, |
| headers: clientResponseHeaders, |
| }); |
| } |
|
|
| } catch (error: any) { |
| console.error(`[${timestamp}] Proxy Request Error for ${targetUrl.toString()}:`, error.message, error.cause ? `Cause: ${error.cause}` : ''); |
| |
| |
| return new Response(`Proxy error: Could not connect to target server at ${targetUrl.host}. ${error.message}`, { status: 502 }); |
| } |
| }, |
| |
| error(error: Error): Response { |
| console.error("Server Error:", error); |
| return new Response("Internal Server Error", { status: 500 }); |
| }, |
| }); |
|
|
| console.log(`URL Path Proxy Server is ready and listening on http://localhost:${PORT}`); |
| console.log(`Example Usage: curl http://localhost:${PORT}/https://httpbin.org/get`); |
| console.log(`Example Usage with Port: curl http://localhost:${PORT}/https://api.real.io:3000/v1/models`); |
|
|
|
|
| |
| |
| |
| |
| |
| |