From e4b0ebc3e1d901a03d616155cc5274e4e96db99c Mon Sep 17 00:00:00 2001 From: Chris Wanstrath Date: Tue, 23 Sep 2025 19:03:37 -0700 Subject: [PATCH] apps --- packages/sneaker/package.json | 3 +- packages/sneaker/src/server.tsx | 63 +++++++++++++++++++++++++-------- 2 files changed, 51 insertions(+), 15 deletions(-) diff --git a/packages/sneaker/package.json b/packages/sneaker/package.json index eb2956c..70049f1 100644 --- a/packages/sneaker/package.json +++ b/packages/sneaker/package.json @@ -9,7 +9,8 @@ "subdomain:dev": "bun run --hot src/server.tsx" }, "dependencies": { - "hono": "catalog:" + "hono": "catalog:", + "unique-names-generator": "^4.7.1" }, "devDependencies": { "@types/bun": "latest" diff --git a/packages/sneaker/src/server.tsx b/packages/sneaker/src/server.tsx index 07b399b..a9032d1 100644 --- a/packages/sneaker/src/server.tsx +++ b/packages/sneaker/src/server.tsx @@ -1,8 +1,10 @@ import { Hono } from "hono" import { upgradeWebSocket, websocket } from "hono/bun" +import { uniqueNamesGenerator, adjectives, animals, colors } from "unique-names-generator" type Request = { id: string + app: string method: string path: string headers: Record, @@ -16,23 +18,38 @@ type Response = { body: string } -const app = new Hono() -let connection: any +type Connection = { app: string, ws: any } +let connections: Record = {} const pending = new Map void> -function send(msg: Request) { +function send(connection: any, msg: Request) { console.log("sending", msg) - connection?.send(JSON.stringify(msg)) + connection.send(JSON.stringify(msg)) } -app.get("/tunnel", upgradeWebSocket(async c => { - return { +const app = new Hono + +app.get("/tunnel", c => { + const app = c.req.query("app") + if (!app) { + return c.text("need ?app name", 502) + } + + return upgradeWebSocket(c, { async onOpen(_event, ws) { - console.log("connection opened") - connection = ws + const name = randomName() + connections[name] = { app, ws } + console.log(`connection opened: ${name} -> ${app}`) }, - onClose: (_event, _ws) => connection = undefined, - async onMessage(event, ws) { + onClose: (_event, ws) => { + for (const name of Object.keys(connections)) + if (connections[name]?.ws === ws) { + console.log("connection closed:", name) + delete connections[name] + break + } + }, + async onMessage(event, _ws) { const msg = JSON.parse(event.data.toString()) const resolve = pending.get(msg.id) if (resolve) { @@ -40,21 +57,32 @@ app.get("/tunnel", upgradeWebSocket(async c => { 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("No tunnel", 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({ + send(connection.ws, { id, + app, method: c.req.method, path: c.req.path, headers, @@ -68,11 +96,18 @@ app.get("*", async c => { }) }) -// Generate a random 8 character string 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,