import { Hono } from "hono" import { upgradeWebSocket, websocket } from "hono/bun" import { uniqueNamesGenerator, adjectives, animals } from "unique-names-generator" type Request = { id: string app: string method: string path: string headers: Record, body: string } type Response = { id: string status: number headers: Record, body: string } type Success = { subdomain: string } type Connection = { app: string, ws: any } let connections: Record = {} const pending = new Map void> const app = new Hono app.get("/health", c => c.text("ok")) app.get("/tunnels", c => { return c.html(
    {Object.keys(connections).map(key =>
  • {key} ({connections[key]?.app})
  • )}
) }) app.get("/tunnel/:app", c => { const url = new URL(c.req.url) const port = url.port === "80" ? "" : `:${url.port}` const hostname = url.hostname return c.redirect(`http://${c.req.param("app")}.${hostname}${port}`) }) app.get("/tunnel", c => { const app = c.req.query("app") if (!app) return c.text("need ?app name", 502) const subdomain = c.req.query("subdomain") || "" if (subdomain && connections[subdomain]) return c.text("subdomain taken", 502) const name = subdomain || randomName() return upgradeWebSocket(c, { async onOpen(_event, ws) { connections[name] = { app, ws } console.log(`connection opened: ${name} -> ${app}`) send(ws, { subdomain: name }) }, onClose: (_event, ws) => { console.log("connection closed:", name) delete connections[name] }, async onMessage(event, _ws) { const msg = JSON.parse(event.data.toString()) const resolve = pending.get(msg.id) if (resolve) { resolve(msg) pending.delete(msg.id) } }, }) }) app.get("*", async c => { const url = new URL(c.req.url) const localhost = url.hostname.endsWith("localhost") const domains = url.hostname.split(".") let subdomain = "" if (domains.length > (localhost ? 1 : 2)) subdomain = domains[0]! const connection = connections[subdomain] if (!connection) return c.text("Bad Gateway", 502) const id = randomID() const headers = Object.fromEntries(c.req.raw.headers) const body = await c.req.text() const app = connection.app const result = await new Promise(resolve => { pending.set(id, resolve) send(connection.ws, { id, app, method: c.req.method, path: c.req.path, headers, body }) }) return new Response(result.body, { status: result.status, headers: result.headers, }) }) function send(connection: any, msg: Request | Success) { console.log("sending", msg) connection.send(JSON.stringify(msg)) } function randomID(): string { return Math.random().toString(36).slice(2, 10) } function randomName(): string { return uniqueNamesGenerator({ dictionaries: [adjectives, animals], separator: "-", style: "lowerCase", }) } export default { port: process.env.PORT || 3100, websocket, fetch: app.fetch, }