feat: migrate from Next.js to pure Bun relay proxy
- Replace Next.js app router with standalone Bun.serve() entry point - Add middleware stack: rate limiter, body limiter, structured logger, SSRF protection - Add WebSocket bidirectional relay via x-relay-target header - Implement error classification (DNS→502, timeouts→504, SSRF→403, rate→429) - Remove all Next.js dependencies and config files - Update deploy workflow, tsconfig, wrangler config for Bun deployment Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5
parent
124dab98a2
commit
f58a92e2aa
@@ -1,119 +1,312 @@
|
||||
# 🚀 Edge Relay (Proxy Bun)
|
||||
# Edge Proxy Relay -- Pure Bun HTTP + WebSocket Relay
|
||||
|
||||
High-performance HTTP Proxy & Relay handler optimized for Vercel Edge Runtime, Cloudflare Workers, and Bun.
|
||||
[](https://bun.sh)
|
||||
|
||||
## 🌐 Live Deployment
|
||||
A high-performance HTTP and WebSocket relay proxy built entirely with Bun's standard library. Zero framework dependencies. No Next.js, no Express, no React.
|
||||
|
||||
| Provider | Endpoint |
|
||||
|----------|----------|
|
||||
| **Primary (Vercel)** | `https://proxy-bun.vercel.app` |
|
||||
| **Secondary (CF Workers)** | `https://opennext-app.superaseph.workers.dev` |
|
||||
| **Leapcell** | `https://proxy-bun-mytheclipse8647-orfq73fe.apn.leapcell.dev` |
|
||||
| **Interactive Docs** | `https://proxy-bun.vercel.app/docs` |
|
||||
Accepts requests with an `x-relay-target` header and forwards them to the upstream target. Supports both HTTP relay and WebSocket relay in a single `Bun.serve()` instance.
|
||||
|
||||
---
|
||||
|
||||
## 🛠 Cara Pakai
|
||||
## Quick Start
|
||||
|
||||
Proxy ini bekerja dengan menangkap request ke endpoint relay dan meneruskannya ke target yang ditentukan via headers.
|
||||
```bash
|
||||
bun install
|
||||
bun run dev # development with HMR
|
||||
bun start # production
|
||||
```
|
||||
|
||||
The server starts on `http://localhost:3000` by default.
|
||||
|
||||
---
|
||||
|
||||
## Environment Variables
|
||||
|
||||
| Variable | Default | Description |
|
||||
|----------|---------|-------------|
|
||||
| `PORT` | `3000` | Server listen port |
|
||||
| `RELAY_TIMEOUT_MS` | `30000` | Upstream fetch timeout in milliseconds |
|
||||
| `BODY_MAX_BYTES` | `1048576` | Maximum accepted request body size in bytes (1 MB) |
|
||||
| `RATE_LIMIT_MAX` | `100` | Maximum requests per sliding window per client IP |
|
||||
| `RATE_LIMIT_WINDOW_MS` | `60000` | Sliding window duration in milliseconds (1 minute) |
|
||||
|
||||
---
|
||||
|
||||
## Endpoints
|
||||
|
||||
| Path | Method | Description |
|
||||
|------|--------|-------------|
|
||||
| `/` | GET | Status page (HTML) |
|
||||
| `/health` | GET | Health check (JSON): `{ "status": "ok", "uptime": ..., "version": "1.0.0" }` |
|
||||
| `/docs` | GET | Interactive documentation page (HTML) |
|
||||
| `/*` | Any | HTTP relay (requires `x-relay-target`) |
|
||||
| `/*` | GET | WebSocket relay (requires `x-relay-target` with `ws://` or `wss://`, `Upgrade: websocket`) |
|
||||
|
||||
### CORS Preflight
|
||||
|
||||
Any `OPTIONS` request to any path returns a `204 No Content` response with permissive CORS headers (`Access-Control-Allow-Origin: *`).
|
||||
|
||||
---
|
||||
|
||||
## HTTP Relay
|
||||
|
||||
Include the `x-relay-target` header to specify the upstream URL. The request method, body, headers, and query parameters are forwarded transparently.
|
||||
|
||||
### Required Headers
|
||||
|
||||
| Header | Required | Default | Deskripsi |
|
||||
|--------|----------|---------|-----------|
|
||||
| `x-relay-target` | **Yes** | - | Base URL target (e.g. `https://api.openai.com`) |
|
||||
| `x-relay-path` | No | `/` | Path tambahan (e.g. `/v1/chat/completions`) |
|
||||
| Header | Required | Description |
|
||||
|--------|----------|-------------|
|
||||
| `x-relay-target` | Yes | Base URL of the upstream target (e.g. `https://api.openai.com`) |
|
||||
| `x-relay-path` | No | Path to append to the target URL (default: `/`) |
|
||||
|
||||
---
|
||||
### Examples
|
||||
|
||||
## 📖 Contoh Penggunaan
|
||||
**Simple GET relay:**
|
||||
|
||||
### 1. Simple GET Request
|
||||
Mengambil data dari JSONPlaceholder.
|
||||
```bash
|
||||
curl -H "x-relay-target: https://jsonplaceholder.typicode.com/posts/1" \
|
||||
https://proxy-bun.vercel.app/
|
||||
http://localhost:3000/
|
||||
```
|
||||
|
||||
### 2. POST with Body & Headers
|
||||
Meneruskan API Key dan data JSON ke target.
|
||||
**POST with body and authorization:**
|
||||
|
||||
```bash
|
||||
curl -X POST \
|
||||
-H "x-relay-target: https://api.example.com" \
|
||||
-H "x-relay-path: /v1/data" \
|
||||
-H "Authorization: Bearer YOUR_TOKEN" \
|
||||
-H "x-relay-target: https://api.openai.com" \
|
||||
-H "x-relay-path: /v1/chat/completions" \
|
||||
-H "Authorization: Bearer sk-..." \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"key": "value"}' \
|
||||
https://proxy-bun.vercel.app/
|
||||
-d '{"model": "gpt-4", "messages": [{"role": "user", "content": "Hello"}]}' \
|
||||
http://localhost:3000/
|
||||
```
|
||||
|
||||
### 3. Binary Data / Upload
|
||||
Mendukung upload file via `POST`/`PUT` (streaming).
|
||||
**Binary upload (streaming):**
|
||||
|
||||
```bash
|
||||
curl -X PUT \
|
||||
-H "x-relay-target: https://storage.com" \
|
||||
-H "x-relay-target: https://storage.example.com" \
|
||||
-H "x-relay-path: /upload/image.png" \
|
||||
--data-binary "@/path/to/image.png" \
|
||||
https://proxy-bun.vercel.app/
|
||||
http://localhost:3000/
|
||||
```
|
||||
|
||||
---
|
||||
### Supported Methods
|
||||
|
||||
## ⚙️ Fitur & Spesifikasi
|
||||
|
||||
### ⚡ Protocol Support
|
||||
- **HTTP/1.1 & HTTP/2** - Full support.
|
||||
- **Methods** - `GET`, `POST`, `PUT`, `PATCH`, `DELETE`, `HEAD`, `OPTIONS`.
|
||||
- **Streaming** - Mendukung response streaming (Server-Sent Events / SSE) secara native.
|
||||
- **CORS** - Otomatis menambahkan header `Access-Control-Allow-*` agar bisa diakses dari browser.
|
||||
|
||||
### 🛡️ Security & Header Handling
|
||||
Relay ini bersifat transparan kecuali untuk header berikut yang di-**strip** sebelum diteruskan ke target:
|
||||
- `host` (diganti dengan host target)
|
||||
- `x-relay-target`
|
||||
- `x-relay-path`
|
||||
|
||||
Semua header lain (seperti `Authorization`, `User-Agent`, `Cookie`, dsb) akan diteruskan apa adanya.
|
||||
|
||||
### 🧪 Error Codes
|
||||
| Status | Deskripsi |
|
||||
|--------|-----------|
|
||||
| `400` | Missing `x-relay-target` header. |
|
||||
| `403` | Target domain tidak valid (jika whitelist aktif). |
|
||||
| `502` | Target gagal dihubungi / Bad Gateway. |
|
||||
`GET`, `POST`, `PUT`, `PATCH`, `DELETE`, `HEAD`, `OPTIONS`. Response streaming (Server-Sent Events, large payloads) is supported natively.
|
||||
|
||||
---
|
||||
|
||||
## 📂 Struktur Project
|
||||
## WebSocket Relay
|
||||
|
||||
Set `x-relay-target` to a `ws://` or `wss://` URL and the server upgrades the connection and relays bidirectionally.
|
||||
|
||||
### Node.js Client
|
||||
|
||||
```ts
|
||||
import { WebSocket } from "ws";
|
||||
|
||||
const ws = new WebSocket("wss://your-proxy.example/relay", {
|
||||
headers: { "x-relay-target": "wss://echo-websocket.example" },
|
||||
});
|
||||
|
||||
ws.on("open", () => ws.send("Hello via relay!"));
|
||||
ws.on("message", (data) => console.log("Received:", data.toString()));
|
||||
ws.on("error", (err) => console.error("WebSocket error:", err));
|
||||
```
|
||||
|
||||
### Browser Client
|
||||
|
||||
```js
|
||||
const ws = new WebSocket("wss://your-proxy.example/relay", {
|
||||
headers: { "x-relay-target": "wss://echo-websocket.example" },
|
||||
});
|
||||
|
||||
ws.onopen = () => ws.send("Hello via relay!");
|
||||
ws.onmessage = (event) => console.log("Received:", event.data);
|
||||
ws.onerror = (err) => console.error("WebSocket error:", err);
|
||||
```
|
||||
|
||||
### With Bun's Built-in WebSocket
|
||||
|
||||
```ts
|
||||
const ws = new WebSocket(
|
||||
"wss://your-proxy.example/relay",
|
||||
{ headers: { "x-relay-target": "wss://echo-websocket.example" } },
|
||||
);
|
||||
|
||||
ws.onopen = () => ws.send("Hello via relay!");
|
||||
ws.onmessage = (e) => console.log("Got:", e.data);
|
||||
```
|
||||
|
||||
The relay handles text frames, binary frames (`Buffer`, `Uint8Array`, `ArrayBuffer`, `Blob`), and forwards close events with status codes.
|
||||
|
||||
---
|
||||
|
||||
## Architecture
|
||||
|
||||
```text
|
||||
src/
|
||||
├── app/
|
||||
│ ├── route.ts # Entry point proxy (Edge Handler)
|
||||
│ └── docs/ # UI Interactive Docs & Tester
|
||||
└── lib/
|
||||
├── relay-utils.ts # Logic filter header & request builder
|
||||
└── utils.ts # Helper UI
|
||||
├── index.ts # Entry point: Bun.serve() with routing, WS relay,
|
||||
│ # graceful shutdown, middleware orchestration
|
||||
├── lib/
|
||||
│ └── relay-utils.ts # URL normalization, SSRF protection, header
|
||||
│ # filtering, request/response building, error
|
||||
│ # classification, CORS preflight
|
||||
└── middleware/
|
||||
├── index.ts # Barrel exports
|
||||
├── rate-limiter.ts # In-memory sliding window rate limiter (per IP)
|
||||
├── logger.ts # Structured JSON logging with TTY colorization
|
||||
└── body-limiter.ts # Content-Length validation against configurable max
|
||||
```
|
||||
|
||||
## 🏗 Development
|
||||
### Request Flow
|
||||
|
||||
Gunakan [Bun](https://bun.sh) untuk performa terbaik.
|
||||
```
|
||||
Client Request
|
||||
|
|
||||
v
|
||||
Bun.serve() -- routes: /health, /docs, / --> static handlers
|
||||
|
|
||||
+--> OPTIONS? --> 204 CORS preflight response
|
||||
|
|
||||
+--> Upgrade: websocket + ws:// target? --> WebSocket relay (bidirectional)
|
||||
|
|
||||
+--> HTTP relay:
|
||||
1. Body size check (413 if exceeded)
|
||||
2. Rate limit check (429 if exceeded)
|
||||
3. Normalize target URL from x-relay-target header
|
||||
4. SSRF validation (403 if blocked)
|
||||
5. Filter request headers (strip relay, platform, hop-by-hop)
|
||||
6. Fetch upstream with timeout (504 on timeout, 502 on error)
|
||||
7. Filter response headers, attach CORS
|
||||
8. Return relayed response
|
||||
```
|
||||
|
||||
### WebSocket Relay Flow
|
||||
|
||||
```
|
||||
Client WebSocket Bun.serve() Upstream WebSocket
|
||||
| | |
|
||||
|-- upgrade req ---------------> |
|
||||
| (x-relay-target: wss://) | |
|
||||
| |--- open upstream ---->|
|
||||
| |<-- onopen ------------|
|
||||
|<-- open (101 Switching) ------ |
|
||||
| | |
|
||||
|-- send "hello" ------------->| |
|
||||
| |--- "hello" ---------->|
|
||||
| |<-- "echo" ------------|
|
||||
|<-- onmessage "echo" --------- |
|
||||
| | |
|
||||
|-- close -------------------->| |
|
||||
| |--- close upstream --->|
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Error Codes
|
||||
|
||||
| Status | Code | Meaning |
|
||||
|--------|------|---------|
|
||||
| 204 | -- | CORS preflight success (OPTIONS request) |
|
||||
| 400 | `INVALID_TARGET` | Missing or malformed `x-relay-target` header |
|
||||
| 403 | `SSRF_BLOCKED` | Target resolves to a private or internal IP range |
|
||||
| 413 | `BODY_TOO_LARGE` | Request body `Content-Length` exceeds `BODY_MAX_BYTES` |
|
||||
| 429 | `RATE_LIMITED` | Client IP has exceeded the rate limit |
|
||||
| 502 | `DNS_FAILURE` | DNS resolution failed for the target hostname |
|
||||
| 502 | `CONNECTION_REFUSED` | Upstream actively refused the connection |
|
||||
| 502 | `NETWORK_ERROR` | Generic network error (connection reset, unreachable, etc.) |
|
||||
| 504 | `TIMEOUT` | Upstream did not respond within `RELAY_TIMEOUT_MS` |
|
||||
|
||||
All error responses return JSON with `error`, `code`, and `message` fields, plus CORS headers.
|
||||
|
||||
---
|
||||
|
||||
## Security
|
||||
|
||||
### SSRF Protection
|
||||
|
||||
The proxy blocks requests to private and internal network ranges:
|
||||
|
||||
- IPv4 loopback (`127.x.x.x`), private ranges (`10.x.x.x`, `172.16-31.x.x`, `192.168.x.x`), link-local (`169.254.x.x`)
|
||||
- IPv6 loopback (`::1`), link-local (`fe80::`), unique local (`fc00::`/`fd00::`)
|
||||
- Common internal hostnames (`localhost`, `*.local`, `*.internal`, cloud metadata endpoints)
|
||||
|
||||
### Header Filtering
|
||||
|
||||
Before forwarding requests upstream, the proxy strips:
|
||||
|
||||
- Relay control headers (`x-relay-target`, `x-relay-path`, `host`)
|
||||
- Hop-by-hop headers (`connection`, `transfer-encoding`, etc.)
|
||||
- Platform metadata headers (Vercel `x-vercel-*`, Cloudflare `cf-*`, `x-forwarded-*`)
|
||||
- Sensitive headers (`cookie`, `set-cookie`, `via`)
|
||||
|
||||
### Rate Limiting
|
||||
|
||||
In-memory sliding window rate limiter keyed by client IP (default: 100 requests per minute). The `Retry-After` header is set on 429 responses. A periodic cleanup routine prunes expired entries from memory.
|
||||
|
||||
### Body Size Limiting
|
||||
|
||||
Requests with a `Content-Length` exceeding `BODY_MAX_BYTES` (default 1 MB) are rejected with a 413 response. Requests without `Content-Length` (streaming) are passed through.
|
||||
|
||||
### Connection Timeout
|
||||
|
||||
All upstream fetches are bounded by `RELAY_TIMEOUT_MS` (default 30 seconds) using `AbortSignal.timeout()`. Timeouts are classified as 504 responses.
|
||||
|
||||
### Graceful Shutdown
|
||||
|
||||
The server listens for `SIGTERM` and `SIGINT`, stops accepting new connections, and exits cleanly.
|
||||
|
||||
---
|
||||
|
||||
## Deployment
|
||||
|
||||
Deploy as a standalone Bun process. No framework adapter required.
|
||||
|
||||
```bash
|
||||
# Production
|
||||
bun src/index.ts
|
||||
|
||||
# With environment overrides
|
||||
PORT=8080 RELAY_TIMEOUT_MS=10000 bun src/index.ts
|
||||
```
|
||||
|
||||
### Deployment Targets
|
||||
|
||||
- **Any VPS / VM**: Run as a systemd service or under a process manager (e.g., `pm2`, `supervisord`)
|
||||
- **Railway / Fly.io / Render / Koyeb**: Set the build command to `bun install` and start command to `bun src/index.ts`
|
||||
- **Docker**: Use the official `oven/bun` image
|
||||
|
||||
```dockerfile
|
||||
FROM oven/bun:latest
|
||||
WORKDIR /app
|
||||
COPY package.json bun.lock .
|
||||
RUN bun install
|
||||
COPY . .
|
||||
EXPOSE 3000
|
||||
CMD ["bun", "src/index.ts"]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Development
|
||||
|
||||
```bash
|
||||
# Install dependencies
|
||||
bun install
|
||||
|
||||
# Run dev server
|
||||
bun dev
|
||||
# Start dev server with HMR
|
||||
bun run dev
|
||||
|
||||
# Run unit tests
|
||||
# Run tests
|
||||
bun test
|
||||
|
||||
# Static analysis
|
||||
bun run lint
|
||||
```
|
||||
|
||||
## 🚀 Deployment (GitHub Actions)
|
||||
Project ini otomatis dideploy ke Cloudflare Workers setiap ada push ke `master`.
|
||||
Konfigurasi workflow ada di `.github/workflows/deploy.yml`.
|
||||
|
||||
---
|
||||
🤖 **Powered by Bun + Next.js Edge Runtime**
|
||||
|
||||
## License
|
||||
|
||||
MIT
|
||||
|
||||
Reference in New Issue
Block a user