@stacksjs/rpx 0.11.19 → 0.11.21
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/acme-challenge.d.ts +13 -0
- package/dist/auth.d.ts +21 -0
- package/dist/bin/cli.js +252 -200
- package/dist/cert-inspect.d.ts +5 -3
- package/dist/{chunk-83dqq28c.js → chunk-1108y1dk.js} +1 -1
- package/dist/{chunk-0f32jmrb.js → chunk-c9brkawa.js} +1 -1
- package/dist/{chunk-w888yhnp.js → chunk-cqkz06bg.js} +1 -1
- package/dist/chunk-qs36t2rf.js +224 -0
- package/dist/daemon.d.ts +4 -1
- package/dist/https.d.ts +1 -0
- package/dist/index.d.ts +33 -1
- package/dist/index.js +7 -7
- package/dist/proxy-handler.d.ts +18 -1
- package/dist/proxy-pool.d.ts +5 -0
- package/dist/redirect.d.ts +40 -0
- package/dist/registry.d.ts +2 -1
- package/dist/site-resolver.d.ts +106 -0
- package/dist/site-splash.d.ts +19 -0
- package/dist/site-supervisor.d.ts +56 -0
- package/dist/start.d.ts +31 -8
- package/dist/types.d.ts +64 -0
- package/package.json +6 -7
- package/dist/chunk-rs8gqpax.js +0 -174
- package/src/cert-inspect.ts +0 -69
- package/src/colors.ts +0 -13
- package/src/config.ts +0 -45
- package/src/daemon-runner.ts +0 -180
- package/src/daemon.ts +0 -1209
- package/src/dns-state.ts +0 -116
- package/src/dns.ts +0 -568
- package/src/host-match.ts +0 -52
- package/src/host-routes.ts +0 -147
- package/src/hosts.ts +0 -283
- package/src/https.ts +0 -905
- package/src/index.ts +0 -161
- package/src/logger.ts +0 -19
- package/src/macos-trust.ts +0 -175
- package/src/on-demand.ts +0 -264
- package/src/origin-guard.ts +0 -127
- package/src/port-manager.ts +0 -183
- package/src/process-manager.ts +0 -164
- package/src/proxy-handler.ts +0 -387
- package/src/proxy-pool.ts +0 -1003
- package/src/registry.ts +0 -366
- package/src/sni.ts +0 -93
- package/src/start.ts +0 -1421
- package/src/static-files.ts +0 -201
- package/src/types.ts +0 -267
- package/src/utils.ts +0 -243
package/src/proxy-handler.ts
DELETED
|
@@ -1,387 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* The request handlers used by the shared :443 server. Both the in-process
|
|
3
|
-
* multi-proxy mode in `start.ts` and the long-running daemon delegate to this
|
|
4
|
-
* module so routing semantics stay in one place.
|
|
5
|
-
*
|
|
6
|
-
* Routes are looked up via a caller-supplied `getRoute(hostname)` callback.
|
|
7
|
-
* The callback indirection lets each caller use whatever data structure makes
|
|
8
|
-
* sense (a fixed Map at startup, or a hot-swappable registry view) without
|
|
9
|
-
* coupling this module to either.
|
|
10
|
-
*
|
|
11
|
-
* Three transports are supported per route:
|
|
12
|
-
* - HTTP(S) proxying via `fetch()` to an upstream `host:port`.
|
|
13
|
-
* - WebSocket proxying via `server.upgrade()` + an upstream `WebSocket`.
|
|
14
|
-
* - Static file serving from a local directory (`route.static`).
|
|
15
|
-
*/
|
|
16
|
-
import type { ServerWebSocket } from 'bun'
|
|
17
|
-
import type { ResolvedStaticRoute } from './static-files'
|
|
18
|
-
import type { PathRewrite } from './types'
|
|
19
|
-
import { FALLBACK, POOL_BUSY, proxyViaPool, TIMEOUT } from './proxy-pool'
|
|
20
|
-
import { serveStaticFile } from './static-files'
|
|
21
|
-
import { debugLog, resolvePathRewrite } from './utils'
|
|
22
|
-
|
|
23
|
-
export interface ProxyRoute {
|
|
24
|
-
/**
|
|
25
|
-
* Upstream `host:port` to forward requests to (e.g. `localhost:5173`).
|
|
26
|
-
* Optional when `static` is set.
|
|
27
|
-
*/
|
|
28
|
-
sourceHost?: string
|
|
29
|
-
/** Strip `.html` suffix and 301 to clean URLs. */
|
|
30
|
-
cleanUrls?: boolean
|
|
31
|
-
/** Set the `origin` header to the target. */
|
|
32
|
-
changeOrigin?: boolean
|
|
33
|
-
/** Per-route path rewrites (vite/nginx-style prefix routing). */
|
|
34
|
-
pathRewrites?: PathRewrite[]
|
|
35
|
-
/** When set, serve files from a local directory instead of proxying. */
|
|
36
|
-
static?: ResolvedStaticRoute
|
|
37
|
-
/**
|
|
38
|
-
* Path prefix this route is mounted under (e.g. `/docs`). Used together with
|
|
39
|
-
* {@link stripBasePathPrefix} to map request paths to the target. `/` (the
|
|
40
|
-
* host default) is a no-op.
|
|
41
|
-
*/
|
|
42
|
-
basePath?: string
|
|
43
|
-
/**
|
|
44
|
-
* Whether to strip {@link basePath} from the request pathname before
|
|
45
|
-
* resolving the target.
|
|
46
|
-
*
|
|
47
|
-
* - Static routes default to `true`: a directory mounted at `/docs` serves
|
|
48
|
-
* its own `index.html` for `/docs` and `<root>/guide` for `/docs/guide`.
|
|
49
|
-
* - Proxy routes default to `false`: most apps own their namespace (an app
|
|
50
|
-
* mounted at `/api` expects to still see `/api/...`), matching rpx's
|
|
51
|
-
* `PathRewrite.stripPrefix` default and nginx `proxy_pass` (no trailing
|
|
52
|
-
* slash) behavior.
|
|
53
|
-
*
|
|
54
|
-
* When unset, the per-transport default above applies.
|
|
55
|
-
*/
|
|
56
|
-
stripBasePathPrefix?: boolean
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
/**
|
|
60
|
-
* Resolve a route for an incoming request. `pathname` enables path-based
|
|
61
|
-
* routing within a host (e.g. `/api/*` → app, `/docs*` → static dir). Callers
|
|
62
|
-
* that only route by host can ignore the second argument — it's optional so
|
|
63
|
-
* existing host-only `getRoute` callbacks remain valid.
|
|
64
|
-
*/
|
|
65
|
-
export type GetRoute = (hostname: string, pathname: string) => ProxyRoute | undefined
|
|
66
|
-
|
|
67
|
-
export type ProxyFetchHandler = (req: Request, server?: ProxyServer) => Promise<Response | undefined>
|
|
68
|
-
|
|
69
|
-
/** Minimal shape of the Bun server needed for WebSocket upgrades. */
|
|
70
|
-
export interface ProxyServer {
|
|
71
|
-
// `unknown` data + standard HeadersInit so it structurally accepts Bun's
|
|
72
|
-
// `Server<WebSocketData>` for any data generic (the daemon/start callers
|
|
73
|
-
// parameterize differently) without resorting to `any`.
|
|
74
|
-
upgrade: (req: Request, options?: { data?: unknown, headers?: Bun.HeadersInit }) => boolean
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
/** Data attached to an upgraded client socket so the ws handler can dial upstream. */
|
|
78
|
-
interface WsData {
|
|
79
|
-
targetUrl: string
|
|
80
|
-
forwardHeaders: Record<string, string>
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
/** Per-socket state: the upstream client + a buffer for early client frames. */
|
|
84
|
-
interface WsState {
|
|
85
|
-
upstream: WebSocket
|
|
86
|
-
upstreamOpen: boolean
|
|
87
|
-
pending: Array<string | ArrayBufferLike | Uint8Array>
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
const HOP_BY_HOP = new Set([
|
|
91
|
-
'connection',
|
|
92
|
-
'keep-alive',
|
|
93
|
-
'proxy-authenticate',
|
|
94
|
-
'proxy-authorization',
|
|
95
|
-
'te',
|
|
96
|
-
'trailer',
|
|
97
|
-
'transfer-encoding',
|
|
98
|
-
'upgrade',
|
|
99
|
-
'sec-websocket-key',
|
|
100
|
-
'sec-websocket-version',
|
|
101
|
-
'sec-websocket-extensions',
|
|
102
|
-
])
|
|
103
|
-
|
|
104
|
-
function extractHostname(req: Request): string {
|
|
105
|
-
const hostHeader = req.headers.get('host') || ''
|
|
106
|
-
// Strip port (`stacks.localhost:443` → `stacks.localhost`) without allocating
|
|
107
|
-
// an array (`split`) on every request.
|
|
108
|
-
const colon = hostHeader.indexOf(':')
|
|
109
|
-
return colon === -1 ? hostHeader : hostHeader.slice(0, colon)
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
/**
|
|
113
|
-
* Strip the route's mount prefix (`basePath`) from a request pathname so a
|
|
114
|
-
* target mounted under `/docs` sees `/` for `/docs` and `/guide` for
|
|
115
|
-
* `/docs/guide`. A `/` (or empty) base strips nothing. The result always keeps
|
|
116
|
-
* a leading `/`.
|
|
117
|
-
*/
|
|
118
|
-
export function stripBasePath(pathname: string, basePath?: string): string {
|
|
119
|
-
if (!basePath || basePath === '/')
|
|
120
|
-
return pathname
|
|
121
|
-
if (pathname === basePath)
|
|
122
|
-
return '/'
|
|
123
|
-
if (pathname.startsWith(`${basePath}/`)) {
|
|
124
|
-
const rest = pathname.slice(basePath.length)
|
|
125
|
-
return rest === '' ? '/' : rest
|
|
126
|
-
}
|
|
127
|
-
return pathname
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
/**
|
|
131
|
-
* Resolve the upstream target (`host` + `path`) for a request against a route,
|
|
132
|
-
* applying any matching path rewrite. Takes the already-extracted `pathname` so
|
|
133
|
-
* the hot path never re-parses the request URL.
|
|
134
|
-
*/
|
|
135
|
-
function resolveTarget(pathname: string, route: ProxyRoute, verbose?: boolean): { targetHost: string, targetPath: string } {
|
|
136
|
-
let targetHost = route.sourceHost ?? ''
|
|
137
|
-
// Proxy backends preserve their mount prefix by default (most apps own their
|
|
138
|
-
// `/api` namespace), opting in to stripping via `stripBasePathPrefix`.
|
|
139
|
-
// Explicit `pathRewrites` still apply on top of this.
|
|
140
|
-
const stripBase = route.stripBasePathPrefix ?? false
|
|
141
|
-
let targetPath = stripBase ? stripBasePath(pathname, route.basePath) : pathname
|
|
142
|
-
|
|
143
|
-
const rewriteMatch = resolvePathRewrite(targetPath, route.pathRewrites)
|
|
144
|
-
if (rewriteMatch) {
|
|
145
|
-
targetHost = rewriteMatch.targetHost
|
|
146
|
-
targetPath = rewriteMatch.targetPath
|
|
147
|
-
debugLog('request', `Path rewrite: ${pathname} → ${targetHost}${targetPath}`, verbose)
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
return { targetHost, targetPath }
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
/**
|
|
154
|
-
* Extract the origin-form path and query from a request URL without the cost of
|
|
155
|
-
* constructing a `URL`. `req.url` is always absolute-form here
|
|
156
|
-
* (`http://host/path?q`), so we slice from the first `/` after the authority.
|
|
157
|
-
* The raw (un-decoded) path is forwarded verbatim, which is what a proxy wants.
|
|
158
|
-
*/
|
|
159
|
-
function splitPathQuery(rawUrl: string): { pathname: string, search: string } {
|
|
160
|
-
const schemeEnd = rawUrl.indexOf('://')
|
|
161
|
-
const pathStart = schemeEnd === -1 ? rawUrl.indexOf('/') : rawUrl.indexOf('/', schemeEnd + 3)
|
|
162
|
-
if (pathStart === -1)
|
|
163
|
-
return { pathname: '/', search: '' }
|
|
164
|
-
const q = rawUrl.indexOf('?', pathStart)
|
|
165
|
-
if (q === -1)
|
|
166
|
-
return { pathname: rawUrl.slice(pathStart), search: '' }
|
|
167
|
-
return { pathname: rawUrl.slice(pathStart, q), search: rawUrl.slice(q) }
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
/**
|
|
171
|
-
* Build a Bun.serve-compatible `fetch` handler that routes requests based on
|
|
172
|
-
* the `Host` header. Returns 404 when no route matches and 502 on upstream
|
|
173
|
-
* failures. When a request is a WebSocket upgrade and `server` is supplied, it
|
|
174
|
-
* is upgraded (returns `undefined` so Bun completes the handshake) and the
|
|
175
|
-
* traffic is handled by the `websocket` handler from {@link createProxyWebSocketHandler}.
|
|
176
|
-
*/
|
|
177
|
-
export function createProxyFetchHandler(getRoute: GetRoute, verbose?: boolean): ProxyFetchHandler {
|
|
178
|
-
const inner = async (req: Request, server?: ProxyServer): Promise<Response | undefined> => {
|
|
179
|
-
const { pathname, search } = splitPathQuery(req.url)
|
|
180
|
-
const hostname = extractHostname(req)
|
|
181
|
-
|
|
182
|
-
const route = getRoute(hostname, pathname)
|
|
183
|
-
if (!route) {
|
|
184
|
-
debugLog('request', `No route found for host: ${hostname}`, verbose)
|
|
185
|
-
return new Response(`No proxy configured for ${hostname}`, { status: 404 })
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
// Static file serving short-circuits everything else. Strip the route's
|
|
189
|
-
// mount prefix (default for static) so a dir mounted at `/docs` serves its
|
|
190
|
-
// own root for `/docs`.
|
|
191
|
-
if (route.static) {
|
|
192
|
-
const strip = route.stripBasePathPrefix ?? true
|
|
193
|
-
const staticPath = strip ? stripBasePath(pathname, route.basePath) : pathname
|
|
194
|
-
return serveStaticFile(staticPath, route.static)
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
// WebSocket upgrade: hand the socket to Bun and dial the upstream on open.
|
|
198
|
-
if (req.headers.get('upgrade')?.toLowerCase() === 'websocket') {
|
|
199
|
-
if (!server || !route.sourceHost)
|
|
200
|
-
return new Response('WebSocket upgrade not supported here', { status: 400 })
|
|
201
|
-
|
|
202
|
-
const { targetHost, targetPath } = resolveTarget(pathname, route, verbose)
|
|
203
|
-
const targetUrl = `ws://${targetHost}${targetPath}${search}`
|
|
204
|
-
|
|
205
|
-
const forwardHeaders: Record<string, string> = {}
|
|
206
|
-
for (const [k, v] of req.headers) {
|
|
207
|
-
if (!HOP_BY_HOP.has(k.toLowerCase()) && k.toLowerCase() !== 'host')
|
|
208
|
-
forwardHeaders[k] = v
|
|
209
|
-
}
|
|
210
|
-
forwardHeaders.host = targetHost
|
|
211
|
-
forwardHeaders['x-forwarded-for'] = '127.0.0.1'
|
|
212
|
-
forwardHeaders['x-forwarded-proto'] = 'https'
|
|
213
|
-
forwardHeaders['x-forwarded-host'] = hostname
|
|
214
|
-
|
|
215
|
-
const data: WsData = { targetUrl, forwardHeaders }
|
|
216
|
-
const ok = server.upgrade(req, { data })
|
|
217
|
-
if (ok) {
|
|
218
|
-
debugLog('ws', `upgraded ${hostname}${targetPath} → ${targetUrl}`, verbose)
|
|
219
|
-
return undefined
|
|
220
|
-
}
|
|
221
|
-
return new Response('WebSocket upgrade failed', { status: 400 })
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
if (!route.sourceHost)
|
|
225
|
-
return new Response(`No upstream configured for ${hostname}`, { status: 502 })
|
|
226
|
-
|
|
227
|
-
// Strip `.html` and 301 to the clean URL when enabled — before any upstream
|
|
228
|
-
// work, since the redirect doesn't depend on the origin response.
|
|
229
|
-
if (route.cleanUrls && pathname.endsWith('.html')) {
|
|
230
|
-
const cleanPath = pathname.replace(/\.html$/, '')
|
|
231
|
-
return new Response(null, {
|
|
232
|
-
status: 301,
|
|
233
|
-
headers: { Location: cleanPath },
|
|
234
|
-
})
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
const { targetHost, targetPath } = resolveTarget(pathname, route, verbose)
|
|
238
|
-
const originOverride = route.changeOrigin ? `http://${route.sourceHost}` : undefined
|
|
239
|
-
|
|
240
|
-
// Forward through the pooled raw-socket transport: a reused keepalive pool
|
|
241
|
-
// per upstream (like nginx's `keepalive`) that stays flat under load, where
|
|
242
|
-
// fetch()'s connection churn exhausts ephemeral ports and collapses (~15x).
|
|
243
|
-
// Forwarded headers (host, x-forwarded-*, origin) are serialized inline with
|
|
244
|
-
// the passthrough headers — no intermediate Headers copy on the hot path. The
|
|
245
|
-
// pool declines what it doesn't handle (streaming uploads, Expect, upgrades)
|
|
246
|
-
// via FALLBACK, and bodyless requests can retry through fetch() as a backstop.
|
|
247
|
-
const hasBody = req.body != null && req.method !== 'GET' && req.method !== 'HEAD'
|
|
248
|
-
try {
|
|
249
|
-
return await proxyViaPool({
|
|
250
|
-
hostPort: targetHost,
|
|
251
|
-
method: req.method,
|
|
252
|
-
path: `${targetPath}${search}`,
|
|
253
|
-
reqHeaders: req.headers,
|
|
254
|
-
forwardedHost: hostname,
|
|
255
|
-
originOverride,
|
|
256
|
-
body: req.body,
|
|
257
|
-
})
|
|
258
|
-
}
|
|
259
|
-
catch (err) {
|
|
260
|
-
// Upstream stalled past the configured timeout → 504 (no fetch retry — it
|
|
261
|
-
// would just stall again).
|
|
262
|
-
if (err === TIMEOUT) {
|
|
263
|
-
debugLog('request', `Upstream timeout for ${hostname}`, verbose)
|
|
264
|
-
return new Response('Gateway Timeout', { status: 504 })
|
|
265
|
-
}
|
|
266
|
-
// Pool saturated: every connection to this upstream is busy and the wait
|
|
267
|
-
// for a free slot timed out. Fail fast and loud (503) instead of parking
|
|
268
|
-
// the request forever — a parked request with no response is what made the
|
|
269
|
-
// listener appear "wedged" in production.
|
|
270
|
-
if (err === POOL_BUSY) {
|
|
271
|
-
debugLog('request', `Upstream pool saturated for ${hostname}`, verbose)
|
|
272
|
-
return new Response('Service Unavailable', { status: 503, headers: { 'retry-after': '1' } })
|
|
273
|
-
}
|
|
274
|
-
if (err === FALLBACK || !hasBody) {
|
|
275
|
-
try {
|
|
276
|
-
const headers = new Headers(req.headers)
|
|
277
|
-
headers.set('host', targetHost)
|
|
278
|
-
headers.set('x-forwarded-for', '127.0.0.1')
|
|
279
|
-
headers.set('x-forwarded-proto', 'https')
|
|
280
|
-
headers.set('x-forwarded-host', hostname)
|
|
281
|
-
if (originOverride !== undefined)
|
|
282
|
-
headers.set('origin', originOverride)
|
|
283
|
-
return await fetch(`http://${targetHost}${targetPath}${search}`, {
|
|
284
|
-
method: req.method,
|
|
285
|
-
headers,
|
|
286
|
-
body: req.body,
|
|
287
|
-
redirect: 'manual',
|
|
288
|
-
})
|
|
289
|
-
}
|
|
290
|
-
catch (fetchErr) {
|
|
291
|
-
debugLog('request', `Proxy error for ${hostname}: ${fetchErr}`, verbose)
|
|
292
|
-
return new Response(`Proxy Error: ${fetchErr}`, { status: 502 })
|
|
293
|
-
}
|
|
294
|
-
}
|
|
295
|
-
debugLog('request', `Proxy error for ${hostname}: ${err}`, verbose)
|
|
296
|
-
return new Response(`Proxy Error: ${err}`, { status: 502 })
|
|
297
|
-
}
|
|
298
|
-
}
|
|
299
|
-
|
|
300
|
-
// A reverse proxy must never drop a connection on an unexpected throw: an
|
|
301
|
-
// async fetch handler that *rejects* makes Bun close the socket with no
|
|
302
|
-
// response (the client sees an empty reply) and log a stack trace per hit.
|
|
303
|
-
// Wrap the whole pipeline so a routing bug, a malformed request, or a static
|
|
304
|
-
// helper that throws always degrades to a 502 instead of a dropped connection.
|
|
305
|
-
return async (req: Request, server?: ProxyServer): Promise<Response | undefined> => {
|
|
306
|
-
try {
|
|
307
|
-
return await inner(req, server)
|
|
308
|
-
}
|
|
309
|
-
catch (err) {
|
|
310
|
-
debugLog('request', `Unhandled proxy handler error: ${err}`, verbose)
|
|
311
|
-
return new Response('Bad Gateway', { status: 502 })
|
|
312
|
-
}
|
|
313
|
-
}
|
|
314
|
-
}
|
|
315
|
-
|
|
316
|
-
/**
|
|
317
|
-
* Build the `websocket` handler block for Bun.serve. It opens an upstream
|
|
318
|
-
* `WebSocket` per client socket, buffers client→upstream frames until the
|
|
319
|
-
* upstream connection is open, and pipes messages, closes and errors in both
|
|
320
|
-
* directions with a clean teardown.
|
|
321
|
-
*/
|
|
322
|
-
export function createProxyWebSocketHandler(verbose?: boolean) {
|
|
323
|
-
const state = new WeakMap<ServerWebSocket<WsData>, WsState>()
|
|
324
|
-
|
|
325
|
-
return {
|
|
326
|
-
open(ws: ServerWebSocket<WsData>): void {
|
|
327
|
-
const { targetUrl, forwardHeaders } = ws.data
|
|
328
|
-
let upstream: WebSocket
|
|
329
|
-
try {
|
|
330
|
-
// Bun's WebSocket accepts a `headers` option (control-channel auth etc.)
|
|
331
|
-
// that the DOM lib's constructor type doesn't model — describe that
|
|
332
|
-
// extended constructor precisely instead of falling back to `any`.
|
|
333
|
-
type BunWebSocketCtor = new (_url: string | URL, _options?: { headers?: Record<string, string> }) => WebSocket
|
|
334
|
-
upstream = new (WebSocket as unknown as BunWebSocketCtor)(targetUrl, { headers: forwardHeaders })
|
|
335
|
-
}
|
|
336
|
-
catch (err) {
|
|
337
|
-
debugLog('ws', `failed to open upstream ${targetUrl}: ${err}`, verbose)
|
|
338
|
-
ws.close(1011, 'upstream connect failed')
|
|
339
|
-
return
|
|
340
|
-
}
|
|
341
|
-
upstream.binaryType = 'arraybuffer'
|
|
342
|
-
const st: WsState = { upstream, upstreamOpen: false, pending: [] }
|
|
343
|
-
state.set(ws, st)
|
|
344
|
-
|
|
345
|
-
upstream.addEventListener('open', () => {
|
|
346
|
-
st.upstreamOpen = true
|
|
347
|
-
for (const frame of st.pending)
|
|
348
|
-
upstream.send(frame)
|
|
349
|
-
st.pending = []
|
|
350
|
-
})
|
|
351
|
-
upstream.addEventListener('message', (ev: MessageEvent) => {
|
|
352
|
-
// Forward both binary (ArrayBuffer) and text frames to the client.
|
|
353
|
-
// `binaryType` is 'arraybuffer', so `ev.data` is `string | ArrayBuffer`.
|
|
354
|
-
ws.send(ev.data as string | ArrayBuffer)
|
|
355
|
-
})
|
|
356
|
-
upstream.addEventListener('close', (ev: CloseEvent) => {
|
|
357
|
-
try { ws.close(ev.code || 1000, ev.reason || '') }
|
|
358
|
-
catch { /* already closing */ }
|
|
359
|
-
})
|
|
360
|
-
upstream.addEventListener('error', () => {
|
|
361
|
-
debugLog('ws', `upstream error for ${targetUrl}`, verbose)
|
|
362
|
-
try { ws.close(1011, 'upstream error') }
|
|
363
|
-
catch { /* already closing */ }
|
|
364
|
-
})
|
|
365
|
-
},
|
|
366
|
-
|
|
367
|
-
message(ws: ServerWebSocket<WsData>, message: string | Buffer): void {
|
|
368
|
-
const st = state.get(ws)
|
|
369
|
-
if (!st)
|
|
370
|
-
return
|
|
371
|
-
const frame = typeof message === 'string' ? message : new Uint8Array(message)
|
|
372
|
-
if (st.upstreamOpen)
|
|
373
|
-
st.upstream.send(frame)
|
|
374
|
-
else
|
|
375
|
-
st.pending.push(frame)
|
|
376
|
-
},
|
|
377
|
-
|
|
378
|
-
close(ws: ServerWebSocket<WsData>, code: number, reason: string): void {
|
|
379
|
-
const st = state.get(ws)
|
|
380
|
-
if (!st)
|
|
381
|
-
return
|
|
382
|
-
state.delete(ws)
|
|
383
|
-
try { st.upstream.close(code || 1000, reason || '') }
|
|
384
|
-
catch { /* already closed */ }
|
|
385
|
-
},
|
|
386
|
-
}
|
|
387
|
-
}
|