This commit is contained in:
Chris Wanstrath 2025-09-23 19:03:37 -07:00
parent 3fc9bfad4a
commit e4b0ebc3e1
2 changed files with 51 additions and 15 deletions

View File

@ -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"

View File

@ -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<string, string>,
@ -16,23 +18,38 @@ type Response = {
body: string
}
const app = new Hono()
let connection: any
type Connection = { app: string, ws: any }
let connections: Record<string, Connection> = {}
const pending = new Map<string, (res: any) => 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<Response>(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,