( sock: ClientSocket, st: ConnState, connectLine: string, wsUrl: string, authHeader: string, wsAuthHeader: string, )
| 342 | } |
| 343 | |
| 344 | function openTunnel( |
| 345 | sock: ClientSocket, |
| 346 | st: ConnState, |
| 347 | connectLine: string, |
| 348 | wsUrl: string, |
| 349 | authHeader: string, |
| 350 | wsAuthHeader: string, |
| 351 | ): void { |
| 352 | // core/websocket/stream.go picks JSON vs binary-proto from the upgrade |
| 353 | // request's Content-Type header (defaults to JSON). Without application/proto |
| 354 | // the server protojson.Unmarshals our hand-encoded binary chunks and fails |
| 355 | // silently with EOF. |
| 356 | const headers = { |
| 357 | 'Content-Type': 'application/proto', |
| 358 | Authorization: wsAuthHeader, |
| 359 | } |
| 360 | let ws: WebSocketLike |
| 361 | if (nodeWSCtor) { |
| 362 | ws = new nodeWSCtor(wsUrl, { |
| 363 | headers, |
| 364 | agent: getWebSocketProxyAgent(wsUrl), |
| 365 | ...getWebSocketTLSOptions(), |
| 366 | }) as unknown as WebSocketLike |
| 367 | } else { |
| 368 | ws = new globalThis.WebSocket(wsUrl, { |
| 369 | // @ts-expect-error — Bun extension; not in lib.dom WebSocket types |
| 370 | headers, |
| 371 | proxy: getWebSocketProxyUrl(wsUrl), |
| 372 | tls: getWebSocketTLSOptions() || undefined, |
| 373 | }) |
| 374 | } |
| 375 | ws.binaryType = 'arraybuffer' |
| 376 | st.ws = ws |
| 377 | |
| 378 | ws.onopen = () => { |
| 379 | // First chunk carries the CONNECT line plus Proxy-Authorization so the |
| 380 | // server can auth the tunnel and know the target host:port. Server |
| 381 | // responds with its own "HTTP/1.1 200" over the tunnel; we just pipe it. |
| 382 | const head = |
| 383 | `${connectLine}\r\n` + `Proxy-Authorization: ${authHeader}\r\n` + `\r\n` |
| 384 | ws.send(encodeChunk(Buffer.from(head, 'utf8'))) |
| 385 | // Flush anything that arrived while the WS handshake was in flight — |
| 386 | // trailing bytes from the CONNECT packet and any data() callbacks that |
| 387 | // fired before onopen. |
| 388 | st.wsOpen = true |
| 389 | for (const buf of st.pending) { |
| 390 | forwardToWs(ws, buf) |
| 391 | } |
| 392 | st.pending = [] |
| 393 | // Not all WS implementations expose ping(); empty chunk works as an |
| 394 | // application-level keepalive the server can ignore. |
| 395 | st.pinger = setInterval(sendKeepalive, PING_INTERVAL_MS, ws) |
| 396 | } |
| 397 | |
| 398 | ws.onmessage = ev => { |
| 399 | const raw = |
| 400 | ev.data instanceof ArrayBuffer |
| 401 | ? new Uint8Array(ev.data) |
no test coverage detected