sneaker/src/server.tsx

138 lines
3.4 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
}
type Success = {
subdomain: string
}
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("ok"))
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.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<Response>(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,
}