149 lines
3.7 KiB
TypeScript
149 lines
3.7 KiB
TypeScript
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<string, string>,
|
|
body: string
|
|
}
|
|
|
|
type Response = {
|
|
id: string
|
|
status: number
|
|
headers: Record<string, string>,
|
|
body: string
|
|
isBinary?: boolean
|
|
}
|
|
|
|
type Success = {
|
|
subdomain: string
|
|
}
|
|
|
|
export const GIT_SHA = process.env.RENDER_GIT_COMMIT ?? (await Bun.$`git rev-parse HEAD`.text()).trim()
|
|
|
|
type Connection = { app: string, ws: any }
|
|
let connections: Record<string, Connection> = {}
|
|
const pending = new Map<string, (res: any) => void>
|
|
|
|
const app = new Hono
|
|
|
|
app.get("/health", c => c.text(GIT_SHA))
|
|
|
|
app.get("/tunnels", c => {
|
|
return c.html(<ul>
|
|
{Object.keys(connections).map(key =>
|
|
<li><a href={`/tunnel/${key}`}>{key}</a> ({connections[key]?.app})</li>
|
|
)}
|
|
</ul>)
|
|
})
|
|
|
|
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.all("*", 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<Response>(resolve => {
|
|
pending.set(id, resolve)
|
|
send(connection.ws, {
|
|
id,
|
|
app,
|
|
method: c.req.method,
|
|
path: c.req.path,
|
|
headers,
|
|
body
|
|
})
|
|
})
|
|
|
|
if (result.isBinary) {
|
|
const buffer = Buffer.from(result.body, 'base64')
|
|
return new Response(buffer, {
|
|
status: result.status,
|
|
headers: result.headers,
|
|
})
|
|
}
|
|
|
|
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,
|
|
} |