@@ -6,6 +6,9 @@ import type {
66 HttpResponseEndMessage ,
77 HttpResponseMessage ,
88 HttpResponseStartMessage ,
9+ WsCloseMessage ,
10+ WsFrameMessage ,
11+ WsOpenMessage ,
912} from '@auger/shared' ;
1013import { buildWsUrl } from '../utils' ;
1114
@@ -17,6 +20,71 @@ export type HttpCommandOptions = {
1720 subdomain ?: string ;
1821} ;
1922
23+ type LocalSocketEntry = {
24+ socket : WebSocket ;
25+ open : boolean ;
26+ closingFromServer : boolean ;
27+ queuedFrames : Array < { payload : Uint8Array ; isBinary : boolean } > ;
28+ } ;
29+
30+ const textEncoder = new TextEncoder ( ) ;
31+ const textDecoder = new TextDecoder ( ) ;
32+
33+ function normalizeCloseCode ( code : number | undefined ) : number {
34+ if ( code === undefined ) return 1000 ;
35+ if ( ! Number . isInteger ( code ) ) return 1000 ;
36+ if ( code < 1000 || code > 4999 ) return 1000 ;
37+ return code ;
38+ }
39+
40+ async function messageDataToText ( data : unknown ) : Promise < string > {
41+ if ( typeof data === 'string' ) return data ;
42+ if ( data instanceof ArrayBuffer ) return Buffer . from ( data ) . toString ( 'utf8' ) ;
43+ if ( ArrayBuffer . isView ( data ) ) {
44+ return Buffer . from ( data . buffer , data . byteOffset , data . byteLength ) . toString ( 'utf8' ) ;
45+ }
46+ if ( data instanceof Blob ) {
47+ return Buffer . from ( await data . arrayBuffer ( ) ) . toString ( 'utf8' ) ;
48+ }
49+ return String ( data ) ;
50+ }
51+
52+ async function messageDataToPayload (
53+ data : unknown
54+ ) : Promise < { payload : Uint8Array ; isBinary : boolean } | null > {
55+ if ( typeof data === 'string' ) {
56+ return {
57+ payload : textEncoder . encode ( data ) ,
58+ isBinary : false ,
59+ } ;
60+ }
61+
62+ if ( data instanceof ArrayBuffer ) {
63+ return {
64+ payload : new Uint8Array ( data ) ,
65+ isBinary : true ,
66+ } ;
67+ }
68+
69+ if ( ArrayBuffer . isView ( data ) ) {
70+ return {
71+ payload : new Uint8Array (
72+ data . buffer . slice ( data . byteOffset , data . byteOffset + data . byteLength )
73+ ) ,
74+ isBinary : true ,
75+ } ;
76+ }
77+
78+ if ( data instanceof Blob ) {
79+ return {
80+ payload : new Uint8Array ( await data . arrayBuffer ( ) ) ,
81+ isBinary : true ,
82+ } ;
83+ }
84+
85+ return null ;
86+ }
87+
2088async function handleHttpRequest (
2189 ws : WebSocket ,
2290 localPort : number ,
@@ -111,11 +179,121 @@ export async function runHttpCommand(options: HttpCommandOptions): Promise<void>
111179 ? `${ options . subdomain } :${ options . localPort } `
112180 : `${ options . localPort } ` ;
113181 let ws : WebSocket | null = null ;
182+ const localSockets = new Map < string , LocalSocketEntry > ( ) ;
114183 let consecutiveFailures = 0 ;
115184 let reconnectScheduled = false ;
116185 let fatalError = false ;
117186
187+ const sendControl = (
188+ payload :
189+ | HelloMessage
190+ | HttpResponseMessage
191+ | HttpResponseStartMessage
192+ | HttpResponseChunkMessage
193+ | HttpResponseEndMessage
194+ | WsFrameMessage
195+ | WsCloseMessage
196+ ) => {
197+ if ( ws ?. readyState === WebSocket . OPEN ) {
198+ ws . send ( toMessage ( payload ) ) ;
199+ }
200+ } ;
201+
202+ const closeAllLocalSockets = ( code : number , reason : string ) => {
203+ for ( const [ id , entry ] of localSockets . entries ( ) ) {
204+ entry . closingFromServer = true ;
205+ localSockets . delete ( id ) ;
206+ entry . socket . close ( code , reason ) ;
207+ }
208+ } ;
209+
210+ const flushQueuedFrames = ( entry : LocalSocketEntry ) => {
211+ while ( entry . queuedFrames . length > 0 ) {
212+ const frame = entry . queuedFrames . shift ( ) ;
213+ if ( ! frame ) continue ;
214+ if ( frame . isBinary ) {
215+ entry . socket . send ( frame . payload ) ;
216+ } else {
217+ entry . socket . send ( textDecoder . decode ( frame . payload ) ) ;
218+ }
219+ }
220+ } ;
221+
222+ const handleWsOpen = ( message : WsOpenMessage ) => {
223+ const localUrl = new URL ( message . path , `ws://127.0.0.1:${ options . localPort } ` ) . toString ( ) ;
224+ const localSocket =
225+ message . protocols . length > 0
226+ ? new WebSocket ( localUrl , message . protocols )
227+ : new WebSocket ( localUrl ) ;
228+ localSocket . binaryType = 'arraybuffer' ;
229+
230+ const entry : LocalSocketEntry = {
231+ socket : localSocket ,
232+ open : false ,
233+ closingFromServer : false ,
234+ queuedFrames : [ ] ,
235+ } ;
236+ localSockets . set ( message . id , entry ) ;
237+
238+ localSocket . addEventListener ( 'open' , ( ) => {
239+ entry . open = true ;
240+ flushQueuedFrames ( entry ) ;
241+ } ) ;
242+
243+ localSocket . addEventListener ( 'message' , async ( event ) => {
244+ const payload = await messageDataToPayload ( event . data ) ;
245+ if ( ! payload ) return ;
246+ sendControl ( {
247+ type : 'ws_frame' ,
248+ id : message . id ,
249+ dataBase64 : encodeBase64 ( payload . payload ) ,
250+ isBinary : payload . isBinary ,
251+ } ) ;
252+ } ) ;
253+
254+ localSocket . addEventListener ( 'close' , ( event ) => {
255+ localSockets . delete ( message . id ) ;
256+ if ( entry . closingFromServer ) return ;
257+ sendControl ( {
258+ type : 'ws_close' ,
259+ id : message . id ,
260+ code : event . code ,
261+ reason : event . reason ,
262+ } ) ;
263+ } ) ;
264+ } ;
265+
266+ const handleWsFrame = ( message : WsFrameMessage ) => {
267+ const entry = localSockets . get ( message . id ) ;
268+ if ( ! entry ) return ;
269+
270+ const payload = decodeBase64 ( message . dataBase64 ) ;
271+ if ( ! entry . open ) {
272+ entry . queuedFrames . push ( {
273+ payload,
274+ isBinary : message . isBinary ,
275+ } ) ;
276+ return ;
277+ }
278+
279+ if ( message . isBinary ) {
280+ entry . socket . send ( payload ) ;
281+ } else {
282+ entry . socket . send ( textDecoder . decode ( payload ) ) ;
283+ }
284+ } ;
285+
286+ const handleWsClose = ( message : WsCloseMessage ) => {
287+ const entry = localSockets . get ( message . id ) ;
288+ if ( ! entry ) return ;
289+
290+ entry . closingFromServer = true ;
291+ localSockets . delete ( message . id ) ;
292+ entry . socket . close ( normalizeCloseCode ( message . code ) , message . reason ?? '' ) ;
293+ } ;
294+
118295 const scheduleReconnect = ( reason : string ) => {
296+ closeAllLocalSockets ( 1011 , 'Tunnel connection lost' ) ;
119297 if ( fatalError ) {
120298 process . exit ( 1 ) ;
121299 }
@@ -136,6 +314,7 @@ export async function runHttpCommand(options: HttpCommandOptions): Promise<void>
136314
137315 const connect = ( ) => {
138316 ws = new WebSocket ( wsUrl ) ;
317+ ws . binaryType = 'arraybuffer' ;
139318
140319 ws . addEventListener ( 'open' , ( ) => {
141320 consecutiveFailures = 0 ;
@@ -147,14 +326,11 @@ export async function runHttpCommand(options: HttpCommandOptions): Promise<void>
147326 requestedSubdomain : options . subdomain ,
148327 } ;
149328
150- ws ?. send ( toMessage ( hello ) ) ;
329+ sendControl ( hello ) ;
151330 } ) ;
152331
153332 ws . addEventListener ( 'message' , async ( event ) => {
154- const data =
155- typeof event . data === 'string'
156- ? event . data
157- : Buffer . from ( event . data as ArrayBuffer ) . toString ( 'utf8' ) ;
333+ const data = await messageDataToText ( event . data ) ;
158334 const message = parseMessage ( data ) ;
159335
160336 if ( message . type === 'error' ) {
@@ -175,6 +351,21 @@ export async function runHttpCommand(options: HttpCommandOptions): Promise<void>
175351
176352 if ( message . type === 'http_request' ) {
177353 await handleHttpRequest ( ws as WebSocket , options . localPort , message , label ) ;
354+ return ;
355+ }
356+
357+ if ( message . type === 'ws_open' ) {
358+ handleWsOpen ( message ) ;
359+ return ;
360+ }
361+
362+ if ( message . type === 'ws_frame' ) {
363+ handleWsFrame ( message ) ;
364+ return ;
365+ }
366+
367+ if ( message . type === 'ws_close' ) {
368+ handleWsClose ( message ) ;
178369 }
179370 } ) ;
180371
0 commit comments