forked from localtunnel/server
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathserver.ts
More file actions
162 lines (136 loc) · 4.07 KB
/
server.ts
File metadata and controls
162 lines (136 loc) · 4.07 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
import { type ServerWebSocket, serve } from 'bun'
import type { Client, TunnelInit, TunnelRequest, TunnelResponse } from './types'
import { fromBase64, page, toBase64, uid } from './utils'
const port = Number(Bun.env.PORT) || 1234
const scheme = Bun.env.SCHEME || 'http'
const domain = Bun.env.DOMAIN || `localhost:${port}`
const domainHost = domain.replace(/:\d+$/, '')
const clients = new Map<string, ServerWebSocket<Client>>()
const pending = new Map<
string,
{
resolve: (res: TunnelResponse) => void
timer: Timer
}
>()
const TIMEOUT_MS = 30_000
const landingHtml = page(
'bunl',
`<h1>bunl</h1>
<p>Expose localhost to the world.</p>
<p><code>bun x bunl -p 3000</code></p>`
)
function notFoundHtml(subdomain: string) {
return page(
'Not Found',
`<h1>Not Found</h1>
<p>No tunnel is connected for <code>${subdomain}</code>.</p>
<p>Make sure your client is running.</p>`
)
}
const timeoutHtml = page(
'Gateway Timeout',
`<h1>Gateway Timeout</h1>
<p>The tunnel client didn't respond in time.</p>`
)
serve<Client>({
fetch: async (req, server) => {
const reqUrl = new URL(req.url)
if (reqUrl.searchParams.has('new')) {
const requested = reqUrl.searchParams.get('subdomain')
let id = requested || uid()
if (clients.has(id)) id = uid()
const upgraded = server.upgrade(req, { data: { id } })
if (upgraded) return undefined
return new Response('WebSocket upgrade failed', { status: 500 })
}
const host = (req.headers.get('host') || reqUrl.hostname).replace(/:\d+$/, '')
const subdomain = host.endsWith(`.${domainHost}`) ? host.slice(0, -(domainHost.length + 1)) : ''
if (!subdomain) {
return new Response(landingHtml, {
headers: { 'content-type': 'text/html; charset=utf-8' }
})
}
const client = clients.get(subdomain)
if (!client) {
return new Response(notFoundHtml(subdomain), {
headers: { 'content-type': 'text/html; charset=utf-8' },
status: 404
})
}
const id = crypto.randomUUID()
const { method } = req
const pathname = reqUrl.pathname + reqUrl.search
const rawBody = await req.arrayBuffer()
const body = rawBody.byteLength > 0 ? toBase64(rawBody) : ''
const headers: Record<string, string> = {}
req.headers.forEach((v, k) => {
headers[k] = v
})
const message: TunnelRequest = {
body,
headers,
id,
method,
pathname,
type: 'request'
}
const response = await new Promise<TunnelResponse>((resolve, reject) => {
const timer = setTimeout(() => {
pending.delete(id)
reject(new Error('Tunnel request timed out'))
}, TIMEOUT_MS)
pending.set(id, { resolve, timer })
client.send(JSON.stringify(message))
}).catch((): TunnelResponse => {
return {
body: Buffer.from(timeoutHtml).toString('base64'),
headers: { 'content-type': 'text/html; charset=utf-8' },
id,
status: 504,
statusText: 'Gateway Timeout',
type: 'response'
}
})
const resBody = response.body ? fromBase64(response.body) : null
const resHeaders = { ...response.headers }
delete resHeaders['content-encoding']
delete resHeaders['transfer-encoding']
if (resBody) {
resHeaders['content-length'] = String(resBody.byteLength)
}
return new Response(resBody as Uint8Array<ArrayBuffer> | null, {
headers: resHeaders,
status: response.status,
statusText: response.statusText
})
},
port,
websocket: {
close(ws) {
console.log(`\x1b[31m- ${ws.data.id}\x1b[0m (${clients.size - 1} connected)`)
clients.delete(ws.data.id)
},
message(_ws, raw) {
const msg = JSON.parse(
typeof raw === 'string' ? raw : new TextDecoder().decode(raw)
) as TunnelResponse
if (msg.type !== 'response' || !msg.id) return
const entry = pending.get(msg.id)
if (!entry) return
clearTimeout(entry.timer)
pending.delete(msg.id)
entry.resolve(msg)
},
open(ws) {
clients.set(ws.data.id, ws)
console.log(`\x1b[32m+ ${ws.data.id}\x1b[0m (${clients.size} connected)`)
const init: TunnelInit = {
type: 'init',
url: `${scheme}://${ws.data.id}.${domain}`
}
ws.send(JSON.stringify(init))
}
}
})
console.log(`bunl server listening on :${port} (${scheme}://*.${domain})`)