add client, pass along query params
This commit is contained in:
parent
f70a92b14a
commit
25e46dd8a5
1
bun.lock
1
bun.lock
|
|
@ -1,5 +1,6 @@
|
||||||
{
|
{
|
||||||
"lockfileVersion": 1,
|
"lockfileVersion": 1,
|
||||||
|
"configVersion": 0,
|
||||||
"workspaces": {
|
"workspaces": {
|
||||||
"": {
|
"": {
|
||||||
"name": "sneaker",
|
"name": "sneaker",
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,10 @@
|
||||||
"module": "src/server.tsx",
|
"module": "src/server.tsx",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"private": true,
|
"private": true,
|
||||||
|
"exports": {
|
||||||
|
".": "./src/server.tsx",
|
||||||
|
"./client": "./src/client.ts"
|
||||||
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "bun --hot src/server.tsx",
|
"dev": "bun --hot src/server.tsx",
|
||||||
"prod": "env NODE_ENV=production bun src/server.tsx",
|
"prod": "env NODE_ENV=production bun src/server.tsx",
|
||||||
|
|
|
||||||
173
src/client.ts
Normal file
173
src/client.ts
Normal file
|
|
@ -0,0 +1,173 @@
|
||||||
|
export interface TunnelOptions {
|
||||||
|
server: string
|
||||||
|
app: string
|
||||||
|
target: string
|
||||||
|
subdomain?: string
|
||||||
|
reconnect?: boolean
|
||||||
|
onOpen?: (subdomain: string) => void
|
||||||
|
onClose?: () => void
|
||||||
|
onRequest?: (req: TunnelRequest) => void
|
||||||
|
onError?: (error: Error) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Tunnel {
|
||||||
|
readonly subdomain: string | null
|
||||||
|
close(): void
|
||||||
|
}
|
||||||
|
|
||||||
|
export type TunnelRequest = {
|
||||||
|
id: string
|
||||||
|
app: string
|
||||||
|
method: string
|
||||||
|
path: string
|
||||||
|
headers: Record<string, string>
|
||||||
|
body: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export type TunnelResponse = {
|
||||||
|
id: string
|
||||||
|
status: number
|
||||||
|
headers: Record<string, string>
|
||||||
|
body: string
|
||||||
|
isBinary?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
const BACKOFF = [1000, 2000, 4000, 8000, 16000, 30000]
|
||||||
|
|
||||||
|
const isText = (contentType: string | null): boolean => {
|
||||||
|
if (!contentType) return true
|
||||||
|
const ct = contentType.toLowerCase()
|
||||||
|
return (
|
||||||
|
ct.startsWith("text/") ||
|
||||||
|
ct.includes("json") ||
|
||||||
|
ct.includes("xml") ||
|
||||||
|
ct.includes("javascript") ||
|
||||||
|
ct.includes("css") ||
|
||||||
|
ct.includes("html") ||
|
||||||
|
ct.includes("urlencoded")
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function connect(options: TunnelOptions): Tunnel {
|
||||||
|
const {
|
||||||
|
server,
|
||||||
|
app,
|
||||||
|
target,
|
||||||
|
reconnect = true,
|
||||||
|
onOpen,
|
||||||
|
onClose,
|
||||||
|
onRequest,
|
||||||
|
onError,
|
||||||
|
} = options
|
||||||
|
|
||||||
|
let subdomain: string | null = null
|
||||||
|
let ws: WebSocket | null = null
|
||||||
|
let closed = false
|
||||||
|
let attempts = 0
|
||||||
|
let reconnectTimer: Timer | null = null
|
||||||
|
|
||||||
|
function open() {
|
||||||
|
const params = new URLSearchParams({ app })
|
||||||
|
if (options.subdomain) params.set("subdomain", options.subdomain)
|
||||||
|
|
||||||
|
const url = `${server}/tunnel?${params}`
|
||||||
|
ws = new WebSocket(url)
|
||||||
|
|
||||||
|
ws.onopen = () => {
|
||||||
|
attempts = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
ws.onmessage = async (event) => {
|
||||||
|
try {
|
||||||
|
const msg = JSON.parse(event.data as string)
|
||||||
|
|
||||||
|
if (msg.subdomain) {
|
||||||
|
subdomain = msg.subdomain as string
|
||||||
|
onOpen?.(subdomain)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (msg.id) {
|
||||||
|
onRequest?.(msg as TunnelRequest)
|
||||||
|
const res = await proxy(msg as TunnelRequest)
|
||||||
|
ws?.send(JSON.stringify(res))
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
onError?.(err instanceof Error ? err : new Error(String(err)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ws.onclose = () => {
|
||||||
|
subdomain = null
|
||||||
|
onClose?.()
|
||||||
|
if (!closed && reconnect) scheduleReconnect()
|
||||||
|
}
|
||||||
|
|
||||||
|
ws.onerror = () => {
|
||||||
|
onError?.(new Error("WebSocket error"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function proxy(req: TunnelRequest): Promise<TunnelResponse> {
|
||||||
|
try {
|
||||||
|
const url = `${target}${req.path}`
|
||||||
|
const hasBody = req.method !== "GET" && req.method !== "HEAD" && req.body
|
||||||
|
|
||||||
|
const response = await fetch(url, {
|
||||||
|
method: req.method,
|
||||||
|
headers: req.headers,
|
||||||
|
body: hasBody ? req.body : undefined,
|
||||||
|
})
|
||||||
|
|
||||||
|
const contentType = response.headers.get("content-type")
|
||||||
|
const headers: Record<string, string> = {}
|
||||||
|
response.headers.forEach((value, key) => {
|
||||||
|
headers[key] = value
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!isText(contentType)) {
|
||||||
|
const buffer = await response.arrayBuffer()
|
||||||
|
return {
|
||||||
|
id: req.id,
|
||||||
|
status: response.status,
|
||||||
|
headers,
|
||||||
|
body: Buffer.from(buffer).toString("base64"),
|
||||||
|
isBinary: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: req.id,
|
||||||
|
status: response.status,
|
||||||
|
headers,
|
||||||
|
body: await response.text(),
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
return {
|
||||||
|
id: req.id,
|
||||||
|
status: 502,
|
||||||
|
headers: { "content-type": "text/plain" },
|
||||||
|
body: `Tunnel error: ${err instanceof Error ? err.message : String(err)}`,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function scheduleReconnect() {
|
||||||
|
const delay = BACKOFF[Math.min(attempts, BACKOFF.length - 1)]!
|
||||||
|
attempts++
|
||||||
|
reconnectTimer = setTimeout(open, delay)
|
||||||
|
}
|
||||||
|
|
||||||
|
open()
|
||||||
|
|
||||||
|
return {
|
||||||
|
get subdomain() {
|
||||||
|
return subdomain
|
||||||
|
},
|
||||||
|
close() {
|
||||||
|
closed = true
|
||||||
|
if (reconnectTimer) clearTimeout(reconnectTimer)
|
||||||
|
ws?.close()
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -104,7 +104,7 @@ app.all("*", async c => {
|
||||||
id,
|
id,
|
||||||
app,
|
app,
|
||||||
method: c.req.method,
|
method: c.req.method,
|
||||||
path: c.req.path,
|
path: url.pathname + url.search,
|
||||||
headers,
|
headers,
|
||||||
body
|
body
|
||||||
})
|
})
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user