@@ -8,21 +8,149 @@ import {
88
99const matchPath = ( ( path : string ) => path === '/echo' ) ;
1010
11+ interface ParsedFrame {
12+ stream_id : number ;
13+ type : string ;
14+ flags : string [ ] ;
15+ length : number ;
16+ payload_hex : string ;
17+ }
18+
19+ interface FrameOutput extends ParsedFrame {
20+ decoded_headers ?: { [ key : string ] : string | string [ ] } ;
21+ }
22+
23+ function frameToOutput ( frameBuffer : Buffer ) : ParsedFrame {
24+ const length = ( frameBuffer [ 0 ] << 16 ) | ( frameBuffer [ 1 ] << 8 ) | frameBuffer [ 2 ] ;
25+ const typeNum = frameBuffer [ 3 ] ;
26+ const flagsByte = frameBuffer [ 4 ] ;
27+ const streamId = ( ( frameBuffer [ 5 ] & 0x7f ) << 24 ) | ( frameBuffer [ 6 ] << 16 ) |
28+ ( frameBuffer [ 7 ] << 8 ) | frameBuffer [ 8 ] ;
29+
30+ const FRAME_TYPES : { [ key : number ] : string } = {
31+ 0x0 : 'DATA' , 0x1 : 'HEADERS' , 0x2 : 'PRIORITY' , 0x3 : 'RST_STREAM' ,
32+ 0x4 : 'SETTINGS' , 0x5 : 'PUSH_PROMISE' , 0x6 : 'PING' ,
33+ 0x7 : 'GOAWAY' , 0x8 : 'WINDOW_UPDATE' , 0x9 : 'CONTINUATION'
34+ } ;
35+
36+ const FRAME_FLAGS : { [ type : number ] : { [ flag : number ] : string } } = {
37+ 0x0 : { 0x1 : 'END_STREAM' , 0x8 : 'PADDED' } ,
38+ 0x1 : { 0x1 : 'END_STREAM' , 0x4 : 'END_HEADERS' , 0x8 : 'PADDED' , 0x20 : 'PRIORITY' } ,
39+ 0x4 : { 0x1 : 'ACK' } ,
40+ 0x5 : { 0x4 : 'END_HEADERS' , 0x8 : 'PADDED' } ,
41+ 0x6 : { 0x1 : 'ACK' } ,
42+ 0x9 : { 0x4 : 'END_HEADERS' }
43+ } ;
44+
45+ const flags : string [ ] = [ ] ;
46+ const flagDefs = FRAME_FLAGS [ typeNum ] || { } ;
47+ for ( const [ flagValue , flagName ] of Object . entries ( flagDefs ) ) {
48+ if ( flagsByte & parseInt ( flagValue ) ) {
49+ flags . push ( flagName ) ;
50+ }
51+ }
52+
53+ const payload = frameBuffer . subarray ( 9 , 9 + length ) ;
54+
55+ return {
56+ stream_id : streamId ,
57+ type : FRAME_TYPES [ typeNum ] || `UNKNOWN_${ typeNum } ` ,
58+ flags,
59+ length,
60+ payload_hex : payload . toString ( 'hex' )
61+ } ;
62+ }
63+
64+ function writeFrame ( res : HttpResponse , frame : ParsedFrame , req ?: HttpRequest ) {
65+ const output : FrameOutput = { ...frame } ;
66+
67+ if ( req && ( frame . type === 'HEADERS' || frame . type === 'CONTINUATION' ) ) {
68+ if ( frame . flags . includes ( 'END_HEADERS' ) ) {
69+ output . decoded_headers = req . headers as { [ key : string ] : string | string [ ] } ;
70+ }
71+ }
72+
73+ ( res as any ) . write ( JSON . stringify ( output ) + '\n' ) ;
74+ }
75+
1176async function handle ( req : HttpRequest , res : HttpResponse ) {
1277 if ( req . httpVersion === '2.0' ) {
13- res . writeHead ( 400 ) ;
14- res . end ( 'Echo endpoint does not yet support HTTP/2' ) ;
78+ const stream = ( req as any ) . stream ;
79+ const session = stream ?. session ;
80+ const jsStreamSocket = session ?. socket ;
81+ const capturingStream = ( jsStreamSocket as any ) ?. stream ;
82+
83+ if ( ! capturingStream ?. addStreamCallback ) {
84+ res . writeHead ( 500 ) ;
85+ res . end ( 'Echo endpoint requires data capturing stream' ) ;
86+ return ;
87+ }
88+
89+ const requestStreamId = stream ?. id as number | undefined ;
90+ if ( requestStreamId === undefined ) {
91+ res . writeHead ( 500 ) ;
92+ res . end ( 'Could not determine stream ID' ) ;
93+ return ;
94+ }
95+
96+ res . writeHead ( 200 , { 'Content-Type' : 'text/plain' } ) ;
97+
98+ let responseEnded = false ;
99+
100+ const writeFrameBuffer = ( frameBuffer : Buffer , isGlobal : boolean ) => {
101+ if ( responseEnded ) return ;
102+ const frame = frameToOutput ( frameBuffer ) ;
103+ writeFrame ( res , frame , isGlobal ? undefined : req ) ;
104+ } ;
105+
106+ const { globalFrames, streamFrames } = capturingStream . addStreamCallback (
107+ requestStreamId ,
108+ ( frameBuffer : Buffer ) => {
109+ const frame = frameToOutput ( frameBuffer ) ;
110+ if ( frame . stream_id === 0 ) {
111+ writeFrame ( res , frame ) ;
112+ } else if ( frame . stream_id === requestStreamId ) {
113+ writeFrame ( res , frame , req ) ;
114+ }
115+ }
116+ ) ;
117+
118+ for ( const frameBuffer of globalFrames ) {
119+ writeFrameBuffer ( frameBuffer , true ) ;
120+ }
121+
122+ for ( const frameBuffer of streamFrames ) {
123+ writeFrameBuffer ( frameBuffer , false ) ;
124+ }
125+
126+ req . resume ( ) ;
127+
128+ req . on ( 'end' , ( ) => {
129+ responseEnded = true ;
130+ capturingStream . removeStreamCallback ( requestStreamId ) ;
131+ res . end ( ) ;
132+ } ) ;
133+
134+ req . on ( 'error' , ( ) => {
135+ responseEnded = true ;
136+ capturingStream . removeStreamCallback ( requestStreamId ) ;
137+ res . end ( ) ;
138+ } ) ;
139+
15140 return ;
16141 }
17- await streamConsumers . buffer ( req ) ; // Wait for all request data
18- const input = Buffer . concat ( req . socket . receivedData ?? [ ] ) ;
142+
143+ // HTTP/1.x: return raw request data
144+ await streamConsumers . buffer ( req ) ;
145+ const rawData = Buffer . concat ( req . socket . receivedData ?? [ ] ) ;
19146 res . writeHead ( 200 , {
20- 'Content-Length' : Buffer . byteLength ( input )
147+ 'Content-Length' : Buffer . byteLength ( rawData )
21148 } ) ;
22- res . end ( input ) ;
149+ res . end ( rawData ) ;
23150}
24151
25152export const echo : HttpEndpoint = {
26153 matchPath,
27- handle
28- }
154+ handle,
155+ needsRawData : true
156+ }
0 commit comments